Skip to content

PS-11203 Vector Index Syntax - #5987

Open
percona-mhansson wants to merge 41 commits into
percona:vector-mvpfrom
percona-mhansson:vector-mvp-syntax
Open

PS-11203 Vector Index Syntax#5987
percona-mhansson wants to merge 41 commits into
percona:vector-mvpfrom
percona-mhansson:vector-mvp-syntax

Conversation

@percona-mhansson

@percona-mhansson percona-mhansson commented May 29, 2026

Copy link
Copy Markdown
Contributor

Stacked on #6000

@percona-mhansson percona-mhansson changed the title Vector mvp syntax PS-11203 Vector Index Syntax May 29, 2026
@percona-mhansson
percona-mhansson force-pushed the vector-mvp-syntax branch 3 times, most recently from c055ddc to bac48eb Compare June 9, 2026 13:47
@percona-mhansson
percona-mhansson force-pushed the vector-mvp-syntax branch 2 times, most recently from a985587 to 96481cb Compare June 10, 2026 14:24
@percona-mhansson
percona-mhansson force-pushed the vector-mvp-syntax branch 4 times, most recently from f0e62ee to 3c65f43 Compare June 23, 2026 14:24
Every line written to GCS_DEBUG_TRACE now carries an ISO 8601 UTC
timestamp with microsecond precision at the front of the line:

  [YYYY-MM-DDTHH:MM:SS.uuuuuuZ] [MYSQL_GCS_DEBUG] [GCS] <message>
Every line written to GCS_DEBUG_TRACE now carries an ISO 8601 UTC
timestamp with microsecond precision at the front of the line:

  [YYYY-MM-DDTHH:MM:SS.uuuuuuZ] [MYSQL_GCS_DEBUG] [GCS] <message>
eng "Vector index can only be created in tables with a BIGINT UNSIGNED primary key."

ER_ONLY_SINGLE_VECTOR_INDEX_ALLOWED
eng "A table can have at most one vector index."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this the case? I've seen examples where having two indexed vector columns make a lot of sense. Is this only for the MVP?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread sql/handler.h
inline constexpr const decltype(handlerton::flags)
HTON_SECONDARY_SUPPORTS_TEMPORARY_TABLE(1 << 25);


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please revert these changes to the end of the file as they are not related to this commit

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. These were actually part of the underlying PR, so I'll update that one, too.

);
SHOW CREATE TABLE t1;
SHOW INDEXES FROM t1;

@catalinbp catalinbp Jul 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't have any test cases where you try creating a VECTOR index on a table without BIGINT UNSIGNED, also one where you create the table with such primary key but then you alter the table by droping it or perhaps change data type(though I think the second part of this idea should not be possible)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread storage/innobase/handler/vec0vec.cc Outdated
}
hnsw_param.M = std::atoi(p.value.str);
} else if (my_strcasecmp(system_charset_info, p.key.str, "metric") == 0) {
if (my_strcasecmp(system_charset_info, p.value.str, "euclidean") == 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how will this tie into the distance functions? does it need to support all the metrics?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wonder what visibility do we have from the optimizer if an index is defined on one metric and we query it using distance function using another metric. Shouldn't this be more "static" since there are only a few available options?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. I changed the code to make it more obvious how to add new metrics. If you have a list of metrics already, feel free to drop them!

Not sure what you mean by static? You mean coded into the Bison parser?

Comment thread sql/sql_table.cc Outdated
Comment thread sql/sql_yacc.yy Outdated
}
;

index_construction_parameter:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it's a good idea to expect IDENT_QUOTED instead of IDENT. As far as I know the former accepts non-ASCII bytes/multi-byte. This should be a simple config and I don't expect anything "exotic" here, it's easier to start with something restricted, and relax requirements later

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some ugly lexer hack around IDENT, I never managed to get it to work

mysql> CREATE TABLE t1 (   id BIGINT UNSIGNED PRIMARY KEY,   v1 VECTOR( 1234 ),   VECTOR KEY( v1 ) TYPE hnsw WITH ( m=a ) );
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'm=a ) )' at line 1

The rest of the string is eaten up by the IDENT above

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing to ident for now. That seems to be what people do

Comment thread sql/sql_table.cc Outdated
assert((key_info->flags & flags_before_switch) == flags_before_switch);
if (key->generated) key_info->flags |= HA_GENERATED_KEY;

// Serialize vector index construction params (WITH clause).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CREATE TABLE t2 ( a INT, KEY k ( a ) TYPE hnsw WITH ( M = 6 ) ); should not be accepted

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't now 😄

mysql> CREATE TABLE t2 ( a INT, KEY k ( a ) TYPE hnsw WITH ( M = 6 ) );
ERROR 7034 (HY000): A VECTOR index may only contain a vector type column.

catalinbp and others added 12 commits July 24, 2026 12:20
Implement SQL DISTANCE(vector, vector, metric) and the VECTOR_DISTANCE()
synonym for vector similarity queries. Supported metrics:
EUCLIDEAN (L2), EUCLIDEAN_SQUARED, MANHATTAN (L1), COSINE, and DOT.

Core library (vector-common/vector_distance.*):
- Runtime SIMD dispatch across Scalar, SSE4.2/NEON, AVX2, AVX-512F, and
  SVE2 tiers; per-kernel target attributes, no global -march=native
- Dim-aware wide/narrow dispatch (dims >= 16 use widest tier; smaller
  vectors use 128-bit tier to avoid AVX setup overhead)
- Unaligned load intrinsics throughout: VECTOR data may be misaligned;
  on modern CPUs unaligned and aligned loads have identical throughput
  when data is aligned; aligned loads would fault on misaligned inputs
  without a performance benefit.  Loads that span a 64-byte cache-line
  boundary may still be slower (two line fetches); that depends on
  runtime address, not on loadu vs load, and is not avoided by switching
  to aligned intrinsics
- Float32 SIMD accumulation with double-precision horizontal sum and
  scalar tail: preserves correctness for large dimensions and extreme
  values (e.g. 2e38 Euclidean distance) without sacrificing SIMD width
https://perconadev.atlassian.net/browse/PS-10102

When component_keyring_kmip is loaded but keyring initialization fails
(e.g. KMIP server not running or misconfigured), g_keyring_operations
remains a null unique_ptr. Any subsequent call to a keyring service
method (generate, store, remove, read, encrypt, decrypt, iterator)
dereferences this null pointer, triggering an assertion in
unique_ptr::operator*() and aborting the server.

Add a null guard at the start of every service method that dereferences
g_keyring_operations, returning true (error) immediately when the
keyring has not been successfully initialized.
https://perconadev.atlassian.net/browse/PS-10102

Various fixes of component_keyring_kmip MTR tests to run successfully on PyKMIP and CosmianKMS servers.
Some tests are excluded from parallel run.
The CosmianKMS KMIP server now is main for testing, but PyKMIP server is still supported
Add shutdown protection on the level  into
the shared auth/plugin lifecycle so all authentication plugins are covered.

- Add a global auth-plugin shutdown barrier API
  (`sql/auth/auth_plugin_shutdown.h` + implementation in
  `sql/auth/sql_authentication.cc`).
- Guard auth plugin callback entry points with RAII operation tracking.
- Start barrier shutdown/drain from `plugin_shutdown()` before plugin
  deinitialization (`sql/sql_plugin.cc`).
- Remove plugin-local quiesce state/wait logic from
  `sql/auth/sha2_password.cc` now that teardown protection is centralized.
- Bound auth-plugin shutdown barrier wait with a 2s timeout
  (`AUTH_PLUGIN_SHUTDOWN_TIMEOUT_MS`) to avoid indefinite shutdown hangs.
- Keep normal shutdown flow progressing even when auth operations fail to
  drain in time.

Regression coverage:
- Add `percona.bug_ps11143_threadpool_auth_shutdown` to reproduce the
  threadpool auth-vs-shutdown race and verify clean shutdown.
- Include expected result file and test cleanup to preserve MTR state.

Verified:
- percona.bug_ps11143_threadpool_auth_shutdown
- percona.signal_handling_threadpool
- percona.threadpool_stats
- percona.kill_idle_trx_threadpool
- percona.threadpool_debug
https://perconadev.atlassian.net/browse/PS-10102

When component_keyring_kmip is loaded but keyring initialization fails
(e.g. KMIP server not running or misconfigured), g_keyring_operations
remains a null unique_ptr. Any subsequent call to a keyring service
method (generate, store, remove, read, encrypt, decrypt, iterator)
dereferences this null pointer, triggering an assertion in
unique_ptr::operator*() and aborting the server.

Add a null guard at the start of every service method that dereferences
g_keyring_operations, returning true (error) immediately when the
keyring has not been successfully initialized.
https://perconadev.atlassian.net/browse/PS-10102

Various fixes of component_keyring_kmip MTR tests to run successfully on PyKMIP and CosmianKMS servers.
Some tests are excluded from parallel run.
The CosmianKMS KMIP server now is main for testing, but PyKMIP server is still supported
Add shutdown protection on the level  into
the shared auth/plugin lifecycle so all authentication plugins are covered.

- Add a global auth-plugin shutdown barrier API
  (`sql/auth/auth_plugin_shutdown.h` + implementation in
  `sql/auth/sql_authentication.cc`).
- Guard auth plugin callback entry points with RAII operation tracking.
- Start barrier shutdown/drain from `plugin_shutdown()` before plugin
  deinitialization (`sql/sql_plugin.cc`).
- Remove plugin-local quiesce state/wait logic from
  `sql/auth/sha2_password.cc` now that teardown protection is centralized.
- Bound auth-plugin shutdown barrier wait with a 2s timeout
  (`AUTH_PLUGIN_SHUTDOWN_TIMEOUT_MS`) to avoid indefinite shutdown hangs.
- Keep normal shutdown flow progressing even when auth operations fail to
  drain in time.

Regression coverage:
- Add `percona.bug_ps11143_threadpool_auth_shutdown` to reproduce the
  threadpool auth-vs-shutdown race and verify clean shutdown.
- Include expected result file and test cleanup to preserve MTR state.

Verified:
- percona.bug_ps11143_threadpool_auth_shutdown
- percona.signal_handling_threadpool
- percona.threadpool_stats
- percona.kill_idle_trx_threadpool
- percona.threadpool_debug
- Job-level repository guard on both jobs so forks with Actions enabled
  stop inheriting the hourly cron, which fails without the canonical
  repo's secrets and OIDC trust and emails the fork owner every hour.
- Job-level repository guard on both jobs so forks with Actions enabled
  stop inheriting the hourly cron, which fails without the canonical
  repo's secrets and OIDC trust and emails the fork owner every hour.
inikep and others added 15 commits July 28, 2026 12:17
…rcona/8.4'@(b6d91cf PS-11078 [8.4]: Skip orphan-sweep runs on forks (percona#6087)))
…port from 8.4

Remove native password handling code that references PLUGIN_MYSQL_NATIVE_PASSWORD
and PLUGIN_SHA256_PASSWORD constants which are not available in 10.x+.

This code block was ported from 8.4 branch during cherrypick but the referenced
constants and native password plugin were removed in 10.x architecture. The code
was handling legacy 5.6 database layout upgrades, which is no longer needed or
supported in 10.x+.

Fixes compilation errors:
- 'is_old_db_layout' was not declared in this scope
- 'PLUGIN_MYSQL_NATIVE_PASSWORD' was not declared in this scope; did you mean 'PLUGIN_SHA256_PASSWORD'?

The cherrypick properly removed the parameter from function signatures in 10.x,
but this code block that used it was incorrectly left behind.

Please squash it with the prev. PS-11143 commit
…t always restarted the LRU scan

https://perconadev.atlassian.net/browse/PS-11446

LRUItr::start() is meant to leave the scan hand pointer (m_hp) in
place while it is still within the "old" (cold) sublist of the LRU
list, and only rewind it to the tail of the LRU list once the pointer
has advanced past the old/young boundary into the young (hot) sublist.

The boundary check was inverted: `m_hp->old` is true while the
pointer is still in the old region, so the scan was rewound on every
call instead of only at the old -> young transition. This defeated
the intended optimization by repeatedly restarting the scan from the
tail rather than letting it continue.

Fix the condition to `!m_hp->old`, so the pointer is only reset once
it has crossed into the young sublist.
…o ON

Post-push fix for PS-10595.

innodb_buffer_pool_populate defaulted to OFF, deferring buffer pool
page faults to first access at runtime. Testing showed this default
causes a measurable performance regression as pages get faulted in
on demand during normal operation instead of being pre-populated
at startup.

Flip the default to ON so pre-population happens automatically on
startup, avoiding the regression without requiring users to set the
variable explicitly.

https://perconadev.atlassian.net/browse/PS-10595
…r_read()

https://perconadev.atlassian.net/browse/PS-11444

On IO-bound workloads every physical read serialized on the pool-wide LRU
list mutex, because buf_page_init_for_read() held it across the page-hash
latch acquisition, the descriptor initialization and the 16 KB frame memset.
This narrows that scope and adjusts the surrounding paths accordingly.

1. Narrow the LRU list mutex in buf_page_init_for_read() to cover only the
   LRU list insert. The page-hash insert, the read io-fix, the frame X-lock
   and the memset now run under the page-hash cell X-latch alone. Page-hash
   membership is from now on serialized by the hash cell latch, not by the
   LRU list mutex.

2. This exposes a short window in which a page is reachable through the page
   hash but not yet linked into the LRU list. It is made safe by the read
   io-fix, which is published before the page becomes hash-reachable and
   cleared only after the LRU-add: threads that find the page through the
   hash back off on the io-fix, readers wait on the frame X-lock, and
   eviction cannot see it. In particular buf_page_make_young_if_needed()
   skips such a page, so promotion never manipulates a not-yet-linked node.

3. Introduce a latching rule: no thread may wait for a block frame rw-lock
   while holding a buffer pool LRU list mutex. The paths that latch a frame
   under the LRU list mutex use the rw_lock_*_nowait() variants; the rule is
   enforced in debug builds by rw_lock_assert_wait_allowed().

4. Keep the page-hash cell X-latch across the delete + re-insert of the
   keep-zip path of buf_LRU_free_page() (new keep_hash_lock parameter), so a
   page id is never observably absent from the page hash while it is still
   logically in the buffer pool. Without this a concurrent read could insert
   a duplicate descriptor for the same page id. This also closes the PS-9837
   window at the root.

5. Drop the LRU list mutex from buf_pool_watch_set(). It was taken only to
   avoid the keep-zip gap of point 4; with that gap now closed by the
   continuous hash cell latch, the hash cell latches purge already holds are
   sufficient, and purge no longer contends on the LRU list mutex.

6. Add debug assertions guarding the new invariants and a stress test
   (innodb_zip.lru_mutex_narrow_stress_debug) that widens the windows via
   debug sync points and drives the attacking compressed-table workload.
https://perconadev.atlassian.net/browse/PS-11445

Minimal restore of the pre-removal per-buffer-pool LRU manager thread
(reverted 97d7eba, PS-9071: Merge MySQL 8.3.0 - remove
multithreaded asynchronous LRU flusher), completed and reconciled with
the current 8.4 codebase:

- Removes the recv_writer thread and its mutex/PFS key/latch level.
  It was reintroduced by 97d7eba as part of adopting upstream's
  recovery-time LRU flushing model; with the dedicated per-instance
  LRU manager threads restored, recv_writer's role is redundant and
  its removal is required for the restore to be self-consistent
  (recv_sys_t's writer_mutex/flush_type fields don't survive the
  revert, since later 8.4 development touched this area too, so
  leaving recv_writer's now-dangling references in place would not
  compile).
- Reuses buf_flush_page_cleaner_disabled_debug for the new thread
  instead of adding a new sysvar, keeping the patch smaller (the
  flush/evict/scan stats rework is a separate commit).
- Completes the thread lifecycle: joins the LRU manager threads on
  page-cleaner shutdown (missing after a plain revert) and fixes the
  m_lru_managers array to be freed with the allocator that matches
  how it is allocated.
- Skips the dblwr::force_flush() call in buf_flush_end() when a batch
  flushed no pages (e.g. an LRU batch that only evicted clean pages),
  since nothing was written to the doublewrite buffer for it to flush
  and there is no reason to take the dblwr instance mutex.
- Drops dead per-slot LRU accounting from the page cleaner (superseded
  by the restored thread) and an unrelated dead-code leftover in
  Flush_observer (inc_estimate/m_estimate/m_lsn).
- Updates MTR results/tests across affected suites for the restored
  thread's effect on thread lists and shutdown/flush ordering.
https://perconadev.atlassian.net/browse/PS-11445

The restored implementation pauses the LRU manager thread around
buf_pool_invalidate_instance() purely by resetting the run_lru event.

That's racy: resetting run_lru only makes the *next* os_event_wait()
park, so a manager thread that had already returned from the wait
(or was in its pre-batch sleep) when invalidation began could still
start an LRU batch concurrently with the teardown.

Close that window with a flushing_allowed flag:
- buf_pool_invalidate_instance() clears it on teardown,
- buf_flush_start() checks it when starting a new flush.

Updates to the flag are protected by the flush_state_mutex.

When starting a new flush, the flag is checked and the flush
is started only if allowed and atomically with the check.

Therefore after setting the flag to false, the only possible flushes
are those that started before the flag was set, so it is then enough
to wait for any pending flushes.
https://perconadev.atlassian.net/browse/PS-11445

1. Introduce a knob that allows to enable LRU manager threads.

2. Aggregate multi-instance flush/evict/scan stats for LRU flushing
   for both LRU threads and page cleaners.

With a per-buffer-pool-instance LRU manager thread, several instances
can call into the LRU batch path concurrently. We want to avoid the
need to synchronize on stats update between them. Therefore each of
them returns the statistics and the aggregation happens in a single
thread - the buf_flush_page_coordinator thread.
https://perconadev.atlassian.net/browse/PS-11445

The per-pool sleep-time adaptation only considered pages the LRU
manager thread had flushed as "made progress" (lru_n_flushed). A batch
that only evicted clean pages - which refills the free list exactly as
a flush does - was treated the same as a batch that made no progress
at all, causing the manager to back off its sleep time even while it
was successfully keeping the free list topped up.

Renames the counter to lru_n_processed and feeds it n_flushed +
n_evicted, so eviction-only batches correctly count as progress for
the free-list-fullness heuristic in buf_lru_manager_adapt_sleep_time().
The flush-specific counters (srv_stats.buf_pool_flushed,
lru_manager_stat) are unaffected and continue to count flushes only.
https://perconadev.atlassian.net/browse/PS-11447

Previously, whenever an LRU batch flush was already in progress on a
buffer pool instance and the doublewrite buffer was enabled, a user
thread looking for a free block always waited for that in-progress LRU
flush to finish instead of ever attempting its own single-page flush,
or even a scan for pages that can be freed immediately.

This was decreasing TPS for low concurrency when user thread had to
wait for the whole batch as opposed to quickly solving the issue itself.

This waiting mechanism is now enabled if and only if lru threads
are enabled (innodb_lru_threads = ON), so disabled by default.

Also, when this mechanism is enabled, a thread will wait only when
there is already an in-flight single-page flush, i.e. the concurrent
single-page flush count is capped at 1 per buffer pool instance.

Also, before awaiting the LRU batch, user thread is scanning LRU list,
searching for a page that could be freed immediately (without flushing).

Additional, "not await twice" fix ensures a given buf_LRU_get_free_block()
call waits for an in-progress flush at most once, so it does not keep
waiting indefinitely on repeated loop iterations (to avoid starvation).

New buffer_LRU_% monitor counters (disabled by default):
- buffer_LRU_single_page_flush_count
- buffer_LRU_flush_await_count
Cut Azure Pipelines usage on pull requests, and make the full compiler matrix
something CI can ask for explicitly.

- Drop the CI trigger (`trigger: none`); pushes no longer queue builds. The
  nightly 1:00 AM UTC schedule for 8.4 and PR validation are unaffected.
- Skip PR validation while a pull request is a draft (`pr: drafts: false`);
  builds start when it is marked ready for review.
- Replace the `clang-22 Debug INVERTED` matrix leg with `gcc-16 Debug
  INVERTED`, dropping the now-inert `UBUNTU_CODE_NAME` (it only feeds the
  apt.llvm.org repository line, which is guarded on clang).
- Build only clang-22 RelWithDebInfo, clang-22 Debug, gcc-16 RelWithDebInfo,
  gcc-16 Debug and gcc-16 Debug INVERTED by default. The other 31 legs keep a
  per-leg compile-time guard, now on a queue-time parameter:

      ${{ if or(parameters.fullCI, eq(variables['Build.Reason'], 'Schedule')) }}

This replaces the "fullci" branch-name escape hatch, which could not work. A
guard is evaluated during template expansion, and the branch is not knowable
then. Measured on a pull request from branch PS-11473-8.4-fullci, the
compile-time values are:

  Build.Reason                     'PullRequest'
  Build.SourceBranch               'refs/pull/6116/merge'
  Build.SourceBranchName           'merge'
  System.PullRequest.SourceBranch  ''            (runtime: the branch name)

So Build.SourceBranchName is always "merge" on a pull request and the old
expression never fired there; it only ever worked for non-PR runs, which its
Build.Reason term already covered.

A parameter is resolved when the run is created and therefore is visible to
${{ }}. It makes the pipeline callable from GitHub Actions in either mode:

  POST https://dev.azure.com/it0639/percona-server/_apis/pipelines/<id>/runs?api-version=7.1
  {
    "resources": { "repositories": { "self": { "refName": "refs/pull/<n>/merge" } } },
    "templateParameters": { "fullCI": true }
  }

Queued without templateParameters it builds the five default configs; with
fullCI true, all 36. The guard tests the parameter rather than "not a pull
request" because an API-created run reports Build.Reason=Manual, which would
otherwise make every queued run build everything. Schedule is kept so the
nightly still covers all 36; a manual queue from the Azure UI now gets five
unless the checkbox is ticked.

Guards stay per-leg rather than grouped under a single ${{ if }}: that is the
shape this pipeline used before and is known to expand.

Verified by expanding both branches of the guard: the default run yields the
five configs and a fullCI run yields the same 36 legs as before, with the
job's steps, variables, pool and timeout unchanged.
…arset error

== Root Cause ==

mysql_reconnect() shallow-copies the caller's MYSQL options struct into
a stack-allocated tmp_mysql before calling mysql_real_connect():

  tmp_mysql.options = mysql->options;

If mysql->options.extension was NULL at that point (e.g. a connection
created without any extended options), the shallow copy left
tmp_mysql.options.extension = NULL as well.

mysql_real_connect() calls ENSURE_EXTENSIONS_PRESENT(&mysql->options)
near its start (client.cc:6607).  When extension is NULL the macro
allocates a new st_mysql_options_extention (232 bytes via calloc /
my_raw_malloc) and stores it in tmp_mysql.options.extension.

If mysql_set_character_set() subsequently fails (lines 7595-7605), the
error path executes:

  memset(&tmp_mysql.options, 0, sizeof(tmp_mysql.options));
  mysql_close(&tmp_mysql);

The memset zeroes tmp_mysql.options.extension to NULL *without freeing
it*.  mysql_close() then skips the extension cleanup because the pointer
is already zero.  The 232-byte allocation is permanently lost, along
with the connect_attributes map (My_hash) that may have been allocated
inside it (an additional 744 bytes across 7 blocks in the observed case).

The same code path is reached during the inner mysql_reconnect() call
that occurs when mysql_close(&tmp_mysql) sends COM_QUIT and
cli_advanced_command() triggers a second reconnect attempt because the
socket is already in an error state (client.cc:1434).

== Valgrind Report (Build 816, PXC 9.7.1-1 RelWithDebInfo Ubuntu Noble) ==

  976 (232 direct, 744 indirect) bytes in 1 blocks are definitely lost

  at calloc (vgpreload_memcheck)
  by my_raw_malloc            (my_malloc.cc:321)
  by my_internal_malloc       (my_malloc.cc:371)
  by mysql_real_connect       (client.cc:6607)   <- ENSURE_EXTENSIONS_PRESENT
  by mysql_reconnect          (client.cc:7583)
  by cli_advanced_command     (client.cc:1434)
  by mysql_close              (client.cc:8006)   <- COM_QUIT path
  by mysql_reconnect          (client.cc:7601)   <- charset-error cleanup
  by connect_to_master_via_namespace (rpl_replica.cc:8792)
  by try_to_reconnect              (rpl_replica.cc:5578)
  by handle_slave_io               (rpl_replica.cc:5828)

  PID 116239.  232 bytes directly lost + 744 bytes indirectly lost
  (7 connect-attribute strings inside the extension).

== Fix ==

Call ENSURE_EXTENSIONS_PRESENT(&mysql->options) before the shallow copy
so that mysql->options.extension is guaranteed to be non-NULL:

  ENSURE_EXTENSIONS_PRESENT(&mysql->options);   /* <-- new */
  tmp_mysql.options = mysql->options;

After this change:
  • tmp_mysql.options.extension always aliases mysql->options.extension
    (same pointer, not a new allocation).
  • ENSURE_EXTENSIONS_PRESENT inside mysql_real_connect is a no-op.
  • The memset in the charset-error path still zeroes the pointer in
    tmp_mysql.options, but the actual extension struct remains alive
    and reachable through mysql->options.extension — nothing is leaked.
  • mysql_close_free_options() called during normal mysql_close(mysql)
    will free the extension via the mysql pointer, as before.

ENSURE_EXTENSIONS_PRESENT is already defined and used throughout
client.cc; no new headers are required.

== Failing Test (Build 816) ==

  rpl.rpl_tlsv13  (PID 116239)

== Developer Notes ==

* The leak only triggers when mysql->options.extension is NULL at the
  time of the reconnect.  Connections that have had any extended option
  set (SSL, compression, connect-attrs, etc.) already have a non-NULL
  extension and are unaffected.

* The inner reconnect via cli_advanced_command (client.cc:1434) bypasses
  the mysql->reconnect guard that mysql_close sets to false (client.cc:
  8004) because line 1434 calls mysql_reconnect() unconditionally on any
  net_write_command failure that is not ER_NET_PACKET_TOO_LARGE.

* The memset-before-mysql_close pattern at lines 7589 and 7600 is
  intentional (it prevents mysql_close_free_options from double-freeing
  strings that were shallow-copied from the original mysql handle).
  The fix does not change that pattern; it only ensures there is nothing
  new to leak.
… path

== Root Cause ==

In Gcs_xcom_control::try_send_add_node_request_to_seeds(), a connection
established by connect_to_peer() was only closed inside the
  if (!finalized && connected)
block.  If m_view_control->is_finalized() became true between the
connect_to_peer() return and the condition check (a TOCTOU race during
retry_do_join), the block was skipped entirely.

free_connection() only calls free() on the connection_descriptor struct
itself (node_connection.h:90) — it does NOT call SSL_free() or close the
socket.  The SSL object allocated by SSL_new() in
timed_connect_ssl_msec() (xcom_network_provider_ssl_native_lib.cc:694),
plus all memory allocated internally by SSL_connect() (session state,
cipher context, etc.), was permanently lost.

== Valgrind Report (Build 816, PXC 9.7.1-1 RelWithDebInfo Ubuntu Noble) ==

  96,664 (7,640 direct, 89,024 indirect) bytes in 1 blocks are
  definitely lost

  at malloc (vgpreload_memcheck)
  by CRYPTO_zalloc
  by SSL_new                   (libssl.so.3)
  by timed_connect_ssl_msec    (xcom_network_provider_ssl_native_lib.cc:694)
  by Xcom_network_provider::open_connection              (xcom_network_provider.cc:332)
  by Network_provider_manager::open_xcom_connection      (network_provider_manager.cc:241)
  by Gcs_xcom_control::connect_to_peer                   (gcs_xcom_control_interface.cc:623)
  by Gcs_xcom_control::try_send_add_node_request_to_seeds(gcs_xcom_control_interface.cc:563)
  by Gcs_xcom_control::send_add_node_request             (gcs_xcom_control_interface.cc:543)
  by Gcs_xcom_control::retry_do_join                     (gcs_xcom_control_interface.cc:477)
  by Gcs_xcom_control::do_join                           (gcs_xcom_control_interface.cc:291)

  Observed in 9 of 11 affected mysqld PIDs (PIDs 16098, 164923, 61642 …).
  Each leak is 7,640 bytes direct + ~89–98 KB indirect per occurrence.

== Fix ==

Add an else-if branch that calls xcom_client_close_connection(con)
whenever connected == true but finalized == true.  This ensures the
SSL object and socket are always released exactly once:

  • !finalized && connected  → existing path: add_node + close
  • finalized  && connected  → new path:         close only
  • !connected              → close already done inside connect_to_peer
                               (disable_nagle failure path) or ssl_fd
                               is null (connect failure); nothing to do

== Failing Tests (Build 816) ==

  group_replication.gr_ssl_options
  group_replication.gr_ssl_tls13_runtime_valid_configuration
  group_replication.gr_recovery_tlsv13_*
  group_replication.gr_rejoin_bootstrap
  group_replication.gr_rejoin_no_bootstrap
  group_replication.gr_clone_integration_*
  group_replication.gr_acf_receiver_*
  group_replication.gr_flush_logs
  group_replication.gr_primary_mode_group_operations_22_1
  group_replication.gr_reset_slave_channel

== Developer Notes ==

* free_connection() (node_connection.h:88) is intentionally a bare
  free() — it does not own the SSL or socket lifetime.  Only
  xcom_client_close_connection() / close_xcom_connection() drives the
  provider's close_connection() which calls ssl_free_con() -> SSL_free().

* The race is narrow but reproducible under Valgrind (slow process) or
  high-load retry scenarios where is_finalized() transitions while the
  SSL handshake is in progress.

* No behaviour change for the normal path (not finalized): the existing
  xcom_client_close_connection() call inside the if-block is untouched.
Vector indexes have the type (algorithm) SE_SPECIFIC, and we add a column option
in the data dictionary saying `vector_index=1;` which gets picked up by
dedicated code in the data dictionary and the handler part of InnoDB.

In the SQL layer, the vector index is very much a thing; there is an
`HA_KEY_ALG_VECTOR`, an `HA_VECTOR` and a `KEYTYPE_VECTOR`.

Extra SQL is added to display the type of a vector index as VECTOR rather than
SE_SPECIFIC.
Adding the syntax `TYPE <ident> WITH ( <ident> = <ident>...) ` to index creation
syntax. E.g.:

`CREATE INDEX <name> ( <table> ) TYPE hnsw WITH ( M = 6 )`

The syntax in the `WITH` list, christened Index Construction Parameters in this
commit, must be verified by the storage engine. There were no hooks for this in
InnoDB so one has been added in check_engine().  We have to do it fairly early
so that we can prevent table creation in the DD in case of errors.  Hence, the
index type and parameter list are validated inside the storage engine using a
new interface validate_engine_attributes().

The index construction parameters are serialized as a string of key-value pairs
inside the index's `option` field. The serialization and de-serialization happen
entirely inside the Data Dictionary.

To do: We will probably still need a hook to handle the case of an
already-existing table with invalid attributes; there are no hooks for this.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants