diff --git a/mysql-test/suite/tidesdb/r/tidesdb_pessimistic_shared.result b/mysql-test/suite/tidesdb/r/tidesdb_pessimistic_shared.result index c35c6205..000f4421 100644 --- a/mysql-test/suite/tidesdb/r/tidesdb_pessimistic_shared.result +++ b/mysql-test/suite/tidesdb/r/tidesdb_pessimistic_shared.result @@ -8,8 +8,8 @@ connect s1, localhost, root,,; connect s2, localhost, root,,; connect s3, localhost, root,,; # -# TEST 1: S / S compatible under REPEATABLE-READ -# Both s1 and s2 acquire S on the same row, neither blocks. +# TEST 1: two plain reads under REPEATABLE-READ run concurrently, +# neither takes a lock and neither blocks. # connection s1; SET SESSION transaction_isolation = 'REPEATABLE-READ'; @@ -23,7 +23,7 @@ BEGIN; SELECT bal FROM acct WHERE id = 1; bal 100 -# Both holders of S read successfully -- no deadlock, no block. +# Both read successfully -- no lock, no block. connection default; SELECT bal FROM acct WHERE id = 1; bal @@ -33,9 +33,9 @@ COMMIT; connection s2; COMMIT; # -# TEST 2: X waits for S readers, then proceeds -# s1 + s2 hold S; s3 fires UPDATE that must wait until -# both readers release. +# TEST 2: a plain read does NOT block a writer. s1 keeps an open +# REPEATABLE-READ snapshot on the row while s3 updates it and +# commits immediately -- no lock wait. # connection s1; SET SESSION transaction_isolation = 'REPEATABLE-READ'; @@ -43,20 +43,11 @@ BEGIN; SELECT bal FROM acct WHERE id = 1; bal 100 -connection s2; -SET SESSION transaction_isolation = 'REPEATABLE-READ'; -BEGIN; -SELECT bal FROM acct WHERE id = 1; -bal -100 connection s3; BEGIN; UPDATE acct SET bal = bal + 50 WHERE id = 1; -connection s1; -COMMIT; -connection s2; COMMIT; -connection s3; +connection s1; COMMIT; connection default; # 100 + 50 = 150 @@ -64,44 +55,33 @@ SELECT bal FROM acct WHERE id = 1; bal 150 # -# TEST 3: writer fairness -- new S blocks behind a waiting X -# s1 holds S; s2 fires UPDATE (X-waiting); s3 fires a -# SELECT under REPEATABLE-READ that wants S. s3 must -# NOT jump ahead of s2's queued X. +# TEST 3: write-intent still locks. s1 holds SELECT ... FOR UPDATE +# (X on the row); s3's UPDATE must wait until s1 commits. # -UPDATE acct SET bal = 200 WHERE id = 1; +connection default; connection s1; -SET SESSION transaction_isolation = 'REPEATABLE-READ'; BEGIN; -SELECT bal FROM acct WHERE id = 1; +SELECT bal FROM acct WHERE id = 1 FOR UPDATE; bal -200 -connection default; -connection s2; +150 +connection s3; BEGIN; UPDATE acct SET bal = bal + 1 WHERE id = 1; connection default; -connection s3; -SET SESSION transaction_isolation = 'REPEATABLE-READ'; -BEGIN; -SELECT bal FROM acct WHERE id = 1; connection s1; COMMIT; -connection s2; -COMMIT; connection s3; COMMIT; connection default; -# s2 incremented 200 -> 201; s3 then read either 200 or 201 (both valid) +# 150 + 1 = 151 SELECT bal FROM acct WHERE id = 1; bal -201 +151 # -# TEST 4: READ-COMMITTED reads take no lock -# s1 holds an uncommitted X via UPDATE; s2 under RC reads -# the latest committed value without blocking. +# TEST 4: reads take no lock even with an uncommitted X held. s1 holds +# an uncommitted UPDATE (X); s2 under READ-COMMITTED reads the +# latest committed value without blocking. # -UPDATE acct SET bal = 300 WHERE id = 1; connection s1; BEGIN; UPDATE acct SET bal = bal + 100 WHERE id = 1; @@ -110,15 +90,15 @@ SET SESSION transaction_isolation = 'READ-COMMITTED'; BEGIN; SELECT bal FROM acct WHERE id = 1; bal -300 +151 COMMIT; connection s1; COMMIT; connection default; -# 300 + 100 = 400 +# 151 + 100 = 251 SELECT bal FROM acct WHERE id = 1; bal -400 +251 # # Cleanup # diff --git a/mysql-test/suite/tidesdb/r/tidesdb_pessimistic_upgrade.result b/mysql-test/suite/tidesdb/r/tidesdb_pessimistic_upgrade.result index e68977ce..84360d85 100644 --- a/mysql-test/suite/tidesdb/r/tidesdb_pessimistic_upgrade.result +++ b/mysql-test/suite/tidesdb/r/tidesdb_pessimistic_upgrade.result @@ -7,7 +7,7 @@ INSERT INTO u VALUES (1, 100); connect a, localhost, root,,; connect b, localhost, root,,; # -# Scenario 1, sole holder upgrades cleanly. +# Scenario 1, a single transaction reads then writes the same row. # connection a; SET SESSION transaction_isolation = 'REPEATABLE-READ'; @@ -18,11 +18,14 @@ v UPDATE u SET v = v + 10 WHERE id = 1; COMMIT; connection default; +# 100 + 10 = 110 SELECT * FROM u WHERE id = 1; id v 1 110 # -# Scenario 2, two S holders, one tries to upgrade, must be rejected. +# Scenario 2, two transactions read the row, then one writes it. The +# plain reads take no lock, so the write acquires X and commits +# -- no upgrade, no deadlock. # connection a; SET SESSION transaction_isolation = 'REPEATABLE-READ'; @@ -38,15 +41,14 @@ v 110 connection a; UPDATE u SET v = v + 1 WHERE id = 1; -ERROR 40001: Deadlock found when trying to get lock; try restarting transaction -ROLLBACK; +COMMIT; connection b; COMMIT; connection default; -# Row 1 unchanged from scenario 2. +# 110 + 1 = 111 SELECT * FROM u WHERE id = 1; id v -1 110 +1 111 disconnect a; disconnect b; connection default; diff --git a/mysql-test/suite/tidesdb/r/tidesdb_vector.result b/mysql-test/suite/tidesdb/r/tidesdb_vector.result index 6ac008c6..f3c1564c 100644 --- a/mysql-test/suite/tidesdb/r/tidesdb_vector.result +++ b/mysql-test/suite/tidesdb/r/tidesdb_vector.result @@ -9,7 +9,7 @@ VECTOR INDEX (v) ) ENGINE=TidesDB; INSERT INTO docs VALUES (1, 'origin-x', Vec_FromText('[1.0, 0.0, 0.0, 0.0]')); INSERT INTO docs VALUES (2, 'origin-y', Vec_FromText('[0.0, 1.0, 0.0, 0.0]')); -INSERT INTO docs VALUES (3, 'origin-z', Vec_FromText('[0.0, 0.0, 1.0, 0.0]')); +INSERT INTO docs VALUES (3, 'origin-z', Vec_FromText('[0.0, 0.0, 1.0, 0.4]')); INSERT INTO docs VALUES (4, 'near-x', Vec_FromText('[0.9, 0.1, 0.0, 0.0]')); INSERT INTO docs VALUES (5, 'center', Vec_FromText('[0.5, 0.5, 0.5, 0.5]')); # diff --git a/mysql-test/suite/tidesdb/t/tidesdb_concurrency.test b/mysql-test/suite/tidesdb/t/tidesdb_concurrency.test index 2f8ef8b6..56eea9ce 100644 --- a/mysql-test/suite/tidesdb/t/tidesdb_concurrency.test +++ b/mysql-test/suite/tidesdb/t/tidesdb_concurrency.test @@ -435,7 +435,12 @@ while ($i <= 200) --enable_query_log -# Commit both +# Commit both concurrently. Under optimistic MVCC two transactions committing +# at the same moment can lose to a retryable conflict (1180/1213) even on +# disjoint keys, so the commit is allowed to return one of those and the losing +# transaction is replayed -- exactly the deadlock-retry an application must do, +# and what the sibling concurrency tests already codify. The replay is +# uncontended, so the 200-row set is preserved either way. connection bulk1; send COMMIT; @@ -443,10 +448,44 @@ connection bulk2; send COMMIT; connection bulk1; +--error 0,ER_LOCK_DEADLOCK,ER_ERROR_DURING_COMMIT reap; +let $err1= $mysql_errno; connection bulk2; +--error 0,ER_LOCK_DEADLOCK,ER_ERROR_DURING_COMMIT reap; +let $err2= $mysql_errno; + +if ($err1) +{ + connection bulk1; + BEGIN; + --disable_query_log + let $i= 1; + while ($i <= 100) + { + eval INSERT INTO stress_bulk VALUES ($i, CONCAT('b1_', $i)); + inc $i; + } + --enable_query_log + COMMIT; +} + +if ($err2) +{ + connection bulk2; + BEGIN; + --disable_query_log + let $i= 101; + while ($i <= 200) + { + eval INSERT INTO stress_bulk VALUES ($i, CONCAT('b2_', $i)); + inc $i; + } + --enable_query_log + COMMIT; +} # Bulk3: read while data settles connection bulk3; diff --git a/mysql-test/suite/tidesdb/t/tidesdb_pessimistic_shared.test b/mysql-test/suite/tidesdb/t/tidesdb_pessimistic_shared.test index 161ab1fe..2efbf560 100644 --- a/mysql-test/suite/tidesdb/t/tidesdb_pessimistic_shared.test +++ b/mysql-test/suite/tidesdb/t/tidesdb_pessimistic_shared.test @@ -1,16 +1,13 @@ --source include/have_tidesdb.inc # -# Test: pessimistic S/X lock manager +# Plain reads are non-locking, write-intent still takes X (InnoDB parity). # -# Verifies the four invariants of the two-mode lock manager: -# 1. S / S compatible -- two readers under REPEATABLE-READ on the same -# row both hold S concurrently with no blocking -# 2. X waits for S readers -- an UPDATE blocks while readers hold S, -# then proceeds after every S releases -# 3. Writer fairness -- a new S blocks while an X is queued, so a -# stream of readers can't starve a writer -# 4. RC / SNAPSHOT reads take no lock -- a plain SELECT under -# READ-COMMITTED returns immediately even with an X held on the row +# With pessimistic_locking ON the engine no longer takes shared row locks on +# plain reads at any isolation level. Isolation comes from the library's MVCC +# snapshot, so a plain SELECT never blocks and never blocks a writer, exactly +# like InnoDB's consistent non-locking reads. Only write-intent statements +# (UPDATE, DELETE, SELECT ... FOR UPDATE) take an X row lock. This test proves +# both halves. # # pessimistic_locking is ON by default in this engine, no .opt needed. # @@ -29,10 +26,9 @@ connect (s2, localhost, root,,); connect (s3, localhost, root,,); --echo # ---echo # TEST 1: S / S compatible under REPEATABLE-READ ---echo # Both s1 and s2 acquire S on the same row, neither blocks. +--echo # TEST 1: two plain reads under REPEATABLE-READ run concurrently, +--echo # neither takes a lock and neither blocks. --echo # - connection s1; SET SESSION transaction_isolation = 'REPEATABLE-READ'; BEGIN; @@ -43,7 +39,7 @@ SET SESSION transaction_isolation = 'REPEATABLE-READ'; BEGIN; SELECT bal FROM acct WHERE id = 1; ---echo # Both holders of S read successfully -- no deadlock, no block. +--echo # Both read successfully -- no lock, no block. connection default; SELECT bal FROM acct WHERE id = 1; @@ -53,33 +49,21 @@ connection s2; COMMIT; --echo # ---echo # TEST 2: X waits for S readers, then proceeds ---echo # s1 + s2 hold S; s3 fires UPDATE that must wait until ---echo # both readers release. +--echo # TEST 2: a plain read does NOT block a writer. s1 keeps an open +--echo # REPEATABLE-READ snapshot on the row while s3 updates it and +--echo # commits immediately -- no lock wait. --echo # - connection s1; SET SESSION transaction_isolation = 'REPEATABLE-READ'; BEGIN; SELECT bal FROM acct WHERE id = 1; -connection s2; -SET SESSION transaction_isolation = 'REPEATABLE-READ'; -BEGIN; -SELECT bal FROM acct WHERE id = 1; - connection s3; BEGIN; ---send UPDATE acct SET bal = bal + 50 WHERE id = 1 - -# Release readers; X should proceed after the second one commits. -connection s1; -COMMIT; -connection s2; +UPDATE acct SET bal = bal + 50 WHERE id = 1; COMMIT; -connection s3; ---reap +connection s1; COMMIT; connection default; @@ -87,79 +71,46 @@ connection default; SELECT bal FROM acct WHERE id = 1; --echo # ---echo # TEST 3: writer fairness -- new S blocks behind a waiting X ---echo # s1 holds S; s2 fires UPDATE (X-waiting); s3 fires a ---echo # SELECT under REPEATABLE-READ that wants S. s3 must ---echo # NOT jump ahead of s2's queued X. +--echo # TEST 3: write-intent still locks. s1 holds SELECT ... FOR UPDATE +--echo # (X on the row); s3's UPDATE must wait until s1 commits. --echo # - -UPDATE acct SET bal = 200 WHERE id = 1; - -connection s1; -SET SESSION transaction_isolation = 'REPEATABLE-READ'; -BEGIN; -SELECT bal FROM acct WHERE id = 1; - -# Snapshot the lock-wait counter so we can detect s2 entering cond_wait. connection default; --disable_query_log -let $waits_before_s2 = `SELECT VARIABLE_VALUE+0 FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME='TIDESDB_LOCK_WAITS'`; +let $waits_before = `SELECT VARIABLE_VALUE+0 FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME='TIDESDB_LOCK_WAITS'`; --enable_query_log -connection s2; +connection s1; +BEGIN; +SELECT bal FROM acct WHERE id = 1 FOR UPDATE; + +connection s3; BEGIN; --send UPDATE acct SET bal = bal + 1 WHERE id = 1 -# Without this barrier s3's SELECT can reach the lock manager before s2's -# UPDATE does, take S alongside s1, and prevent s2 from ever being -# promoted (writer fairness only applies when the X request is already -# queued). Wait until Tidesdb_lock_waits increments to confirm s2 is -# parked in the wait queue before firing s3. +# Wait until s3's UPDATE is parked behind s1's X before releasing s1. connection default; let $wait_condition = - SELECT (VARIABLE_VALUE+0) > $waits_before_s2 + SELECT (VARIABLE_VALUE+0) > $waits_before FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME = 'TIDESDB_LOCK_WAITS'; --source include/wait_condition.inc -connection s3; -SET SESSION transaction_isolation = 'REPEATABLE-READ'; -BEGIN; ---send SELECT bal FROM acct WHERE id = 1 - connection s1; COMMIT; -connection s2; ---reap -COMMIT; - -# The bal value s3 reads is intentionally not recorded. Under -# REPEATABLE-READ the snapshot is taken at first data access, and that -# moment is racey relative to s2's commit. If s3's thread reaches -# external_lock before s2 commits the read returns 200; if it reaches it -# after, the read returns 201. Both are valid REPEATABLE-READ. What -# the test actually proves -- writer fairness, that s3's S grant was -# deferred behind s2's queued X -- is demonstrated by the --send / -# --reap ordering above, not by the value. connection s3; ---disable_result_log --reap ---enable_result_log COMMIT; connection default; ---echo # s2 incremented 200 -> 201; s3 then read either 200 or 201 (both valid) +--echo # 150 + 1 = 151 SELECT bal FROM acct WHERE id = 1; --echo # ---echo # TEST 4: READ-COMMITTED reads take no lock ---echo # s1 holds an uncommitted X via UPDATE; s2 under RC reads ---echo # the latest committed value without blocking. +--echo # TEST 4: reads take no lock even with an uncommitted X held. s1 holds +--echo # an uncommitted UPDATE (X); s2 under READ-COMMITTED reads the +--echo # latest committed value without blocking. --echo # - -UPDATE acct SET bal = 300 WHERE id = 1; - connection s1; BEGIN; UPDATE acct SET bal = bal + 100 WHERE id = 1; @@ -174,7 +125,7 @@ connection s1; COMMIT; connection default; ---echo # 300 + 100 = 400 +--echo # 151 + 100 = 251 SELECT bal FROM acct WHERE id = 1; --echo # diff --git a/mysql-test/suite/tidesdb/t/tidesdb_pessimistic_upgrade.test b/mysql-test/suite/tidesdb/t/tidesdb_pessimistic_upgrade.test index 92f17b88..583df183 100644 --- a/mysql-test/suite/tidesdb/t/tidesdb_pessimistic_upgrade.test +++ b/mysql-test/suite/tidesdb/t/tidesdb_pessimistic_upgrade.test @@ -1,10 +1,11 @@ --source include/have_tidesdb.inc # -# S to X upgrade behaviour. The lock manager allows a sole S holder to -# upgrade in place. When another trx also holds S the upgrade cannot -# succeed without blocking on the trx's own S grant, so the manager -# rejects the request with HA_ERR_LOCK_DEADLOCK rather than create a -# self-deadlock. Two scenarios verify both branches. +# Read-then-write within a transaction (InnoDB parity). +# +# Plain reads take no lock, so a transaction that reads a row and then updates +# it just acquires an X lock at the write -- there is no shared lock to upgrade +# and no self-deadlock. Concurrent plain readers on the same row hold nothing, +# so a writer among them acquires X and proceeds without a deadlock. # call mtr.add_suppression("\\[TIDESDB\\].*TDB_ERR_CONFLICT"); @@ -20,7 +21,7 @@ connect (a, localhost, root,,); connect (b, localhost, root,,); --echo # ---echo # Scenario 1, sole holder upgrades cleanly. +--echo # Scenario 1, a single transaction reads then writes the same row. --echo # connection a; SET SESSION transaction_isolation = 'REPEATABLE-READ'; @@ -30,10 +31,13 @@ UPDATE u SET v = v + 10 WHERE id = 1; COMMIT; connection default; +--echo # 100 + 10 = 110 SELECT * FROM u WHERE id = 1; --echo # ---echo # Scenario 2, two S holders, one tries to upgrade, must be rejected. +--echo # Scenario 2, two transactions read the row, then one writes it. The +--echo # plain reads take no lock, so the write acquires X and commits +--echo # -- no upgrade, no deadlock. --echo # connection a; SET SESSION transaction_isolation = 'REPEATABLE-READ'; @@ -45,18 +49,15 @@ SET SESSION transaction_isolation = 'REPEATABLE-READ'; BEGIN; SELECT v FROM u WHERE id = 1; -# a holds S, b holds S. a tries to upgrade to X. Allowed-when-sole -# rule fails, manager returns HA_ERR_LOCK_DEADLOCK. connection a; ---error ER_LOCK_DEADLOCK UPDATE u SET v = v + 1 WHERE id = 1; -ROLLBACK; +COMMIT; connection b; COMMIT; connection default; ---echo # Row 1 unchanged from scenario 2. +--echo # 110 + 1 = 111 SELECT * FROM u WHERE id = 1; disconnect a; diff --git a/mysql-test/suite/tidesdb/t/tidesdb_vector.test b/mysql-test/suite/tidesdb/t/tidesdb_vector.test index ce067248..3fd40050 100644 --- a/mysql-test/suite/tidesdb/t/tidesdb_vector.test +++ b/mysql-test/suite/tidesdb/t/tidesdb_vector.test @@ -23,9 +23,16 @@ CREATE TABLE docs ( VECTOR INDEX (v) ) ENGINE=TidesDB; +# origin-y and origin-z are deliberately at different distances from the query +# point [1,0,0,0]: a plain unit-y and unit-z would both sit at exactly sqrt(2), +# an exact tie whose order in a LIMIT-k result is not defined and resolves +# differently run to run and across platforms. origin-z carries a small fourth +# component so it is unambiguously farther, keeping every kNN result below +# deterministic without a secondary sort key (which would disable the vector +# index). INSERT INTO docs VALUES (1, 'origin-x', Vec_FromText('[1.0, 0.0, 0.0, 0.0]')); INSERT INTO docs VALUES (2, 'origin-y', Vec_FromText('[0.0, 1.0, 0.0, 0.0]')); -INSERT INTO docs VALUES (3, 'origin-z', Vec_FromText('[0.0, 0.0, 1.0, 0.0]')); +INSERT INTO docs VALUES (3, 'origin-z', Vec_FromText('[0.0, 0.0, 1.0, 0.4]')); INSERT INTO docs VALUES (4, 'near-x', Vec_FromText('[0.9, 0.1, 0.0, 0.0]')); INSERT INTO docs VALUES (5, 'center', Vec_FromText('[0.5, 0.5, 0.5, 0.5]')); diff --git a/tidesdb/ha_tidesdb.cc b/tidesdb/ha_tidesdb.cc index e0e9380a..f90c9be0 100644 --- a/tidesdb/ha_tidesdb.cc +++ b/tidesdb/ha_tidesdb.cc @@ -1170,26 +1170,24 @@ static bool stmt_savepoint_rollback(tidesdb_trx_t *trx) return true; } -/* Pick the lock mode for a row materialised on a read path, or report - that no lock is needed. - - write_intent ........ X (covers SELECT FOR UPDATE / UPDATE / DELETE) - - REPEATABLE_READ / SERIALIZABLE ... S (prevents concurrent modification - of read rows within the txn; phantom prevention is incomplete - because we have no range/gap locks, only row locks) - - READ_COMMITTED / SNAPSHOT ... no lock (MVCC snapshot suffices) */ +/* Pick the lock mode for a row materialised on a read path, or report that no + lock is needed. Only write-intent statements lock, and they take X. That + covers UPDATE, DELETE, SELECT FOR UPDATE and SELECT IN SHARE MODE, all of + which reach this path with write_intent already set by store_lock. Plain + reads never lock at any isolation level. The library enforces the level from + its MVCC snapshot on its own, tracking the read set under REPEATABLE_READ and + SERIALIZABLE and aborting a conflicting transaction at commit, which the + plugin surfaces as HA_ERR_LOCK_DEADLOCK for MariaDB to retry. A shared lock + on every read row would only duplicate that guarantee while charging an + allocation for each row a scan walks past. */ static inline bool tdb_lock_mode_for_read(THD *thd, bool write_intent, tdb_lock_mode_t *mode) { + (void)thd; if (write_intent) { *mode = TDB_LOCK_MODE_X; return true; } - int iso = thd ? thd_tx_isolation(thd) : ISO_READ_COMMITTED; - if (iso == ISO_REPEATABLE_READ || iso == ISO_SERIALIZABLE) - { - *mode = TDB_LOCK_MODE_S; - return true; - } return false; } @@ -2820,22 +2818,24 @@ static MYSQL_SYSVAR_BOOL(print_all_conflicts, srv_print_all_conflicts, PLUGIN_VA "(similar to innodb_print_all_deadlocks)", NULL, NULL, 0); -static MYSQL_SYSVAR_BOOL(pessimistic_locking, srv_pessimistic_locking, PLUGIN_VAR_RQCMDARG, - "Enable plugin-level row locks for SELECT ... FOR UPDATE, " - "UPDATE, DELETE, and INSERT on user-defined primary keys. " - "ON (default): write-intent statements acquire per-row X locks " - "and plain reads under REPEATABLE_READ / SERIALIZABLE acquire " - "S locks; multiple S holders coexist, S blocks while an X is " - "waiting (writer fairness). Deadlock detection via wait-for " - "graph traversal; bounded by tidesdb_lock_wait_timeout_ms. " - "Locks held until COMMIT or ROLLBACK. Both explicit and " - "autocommit transactions participate. Locks can be acquired " - "on non-existing keys (e.g. SFU on a missing row blocks INSERT " - "of that key). " - "OFF: pure optimistic MVCC -- concurrent writers on the same " - "row are detected at COMMIT time (TDB_ERR_CONFLICT) and the " - "application must retry", - NULL, NULL, 1); +static MYSQL_SYSVAR_BOOL( + pessimistic_locking, srv_pessimistic_locking, PLUGIN_VAR_RQCMDARG, + "Enable plugin-level row locks for SELECT ... FOR UPDATE, " + "UPDATE, DELETE, and INSERT on user-defined primary keys. " + "ON (default): write-intent statements acquire per-row X locks " + "so concurrent writers on the same key wait rather than aborting " + "at commit. Plain reads never lock; their isolation comes from " + "the library's MVCC snapshot and commit-time conflict detection. " + "Multiple X waiters queue with writer fairness. Deadlock detection via wait-for " + "graph traversal; bounded by tidesdb_lock_wait_timeout_ms. " + "Locks held until COMMIT or ROLLBACK. Both explicit and " + "autocommit transactions participate. Locks can be acquired " + "on non-existing keys (e.g. SFU on a missing row blocks INSERT " + "of that key). " + "OFF: pure optimistic MVCC -- concurrent writers on the same " + "row are detected at COMMIT time (TDB_ERR_CONFLICT) and the " + "application must retry", + NULL, NULL, 1); static MYSQL_SYSVAR_ULONG(fts_min_word_len, srv_fts_min_word_len, PLUGIN_VAR_RQCMDARG, "Minimum word length (in characters) for full-text indexing. " @@ -6747,9 +6747,11 @@ int ha_tidesdb::fetch_row_by_pk(tidesdb_txn_t *txn, const uchar *pk, uint pk_len /* Pessimistic row lock for point reads. Covers both the direct PK lookup path (HA_READ_KEY_EXACT) and the secondary-index resolved-PK path (sec idx returns [prefix][pk]; caller passes the suffix here). - Mode is X for write-intent, S under RR/SR for plain reads; RC/SI - reads take no lock (snapshot suffices). Re-entrant -- a no-op when - the caller already holds the lock in a compatible-or-stronger mode. */ + Only write-intent statements lock here, taking X. Plain reads take no + lock at any isolation level because the library's MVCC snapshot and + commit-time conflict detection already enforce it. Re-entrant -- a + no-op when the caller already holds the lock in a compatible-or-stronger + mode. */ if (unlikely(srv_pessimistic_locking) && cached_trx_) { tdb_lock_mode_t mode; @@ -6872,9 +6874,10 @@ int ha_tidesdb::iter_read_current(uchar *buf) current_pk_len_ = (uint)(key_size - KEY_NAMESPACE_LEN); memcpy(current_pk_buf_, key + KEY_NAMESPACE_LEN, current_pk_len_); - /* Pessimistic row lock for range/prefix scans. Mode chosen by - write-intent + session isolation; covers SELECT ... FOR UPDATE - plus plain SELECT under RR/SR. + /* Pessimistic row lock for range/prefix scans. Only write-intent + statements lock, so this covers SELECT ... FOR UPDATE and SELECT + ... IN SHARE MODE; plain SELECT walks the scan without locking and + relies on the library's snapshot isolation. Under UPDATE/DELETE we deliberately skip the lock here so a secondary-index scan with ICP does not X-lock every PK it walks @@ -11801,8 +11804,8 @@ static long long srv_stat_compaction_count; tidesdb_show_status can read them directly. Their definitions live up there. */ -#define TIDESQL_VERSION_STR "4.6.0" -#define TIDESQL_VERSION_HEX 0x40600 +#define TIDESQL_VERSION_STR "4.6.1" +#define TIDESQL_VERSION_HEX 0x40601 static const char *srv_stat_version = TIDESQL_VERSION_STR; static long long srv_stat_version_hex = TIDESQL_VERSION_HEX;