From cf5ca3a88aca1c3eb2d96abc2d36a83fcbc1e5b7 Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Thu, 5 Mar 2026 15:57:43 -0500 Subject: [PATCH 1/8] #66 corrected mtr extended; data_length and index_length returning 0 for all tidesdb tables in information_schema.tables. the info() handler was using total_data_size from tidesdb_get_stats() which only counts sstable klog+vlog on disk and ignores the active memtable. for tables that have not flushed yet (or when the library does not track per-sstable sizes) this was always zero. the fix adds memtable_size to the data size for both the main column family and each secondary index column family. when both sstable and memtable sizes are still zero but total_keys is positive, it falls back to an estimate using total_keys * (avg_key_size + avg_value_size). the same logic is applied to index_file_length by summing across all secondary index column families. --- .../tidesdb/r/tidesdb_info_schema.result | 27 ++++++ .../suite/tidesdb/t/tidesdb_info_schema.opt | 1 + .../suite/tidesdb/t/tidesdb_info_schema.test | 85 +++++++++++++++++++ tidesdb/ha_tidesdb.cc | 21 ++++- 4 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 mysql-test/suite/tidesdb/r/tidesdb_info_schema.result create mode 100644 mysql-test/suite/tidesdb/t/tidesdb_info_schema.opt create mode 100644 mysql-test/suite/tidesdb/t/tidesdb_info_schema.test diff --git a/mysql-test/suite/tidesdb/r/tidesdb_info_schema.result b/mysql-test/suite/tidesdb/r/tidesdb_info_schema.result new file mode 100644 index 0000000..2594f3a --- /dev/null +++ b/mysql-test/suite/tidesdb/r/tidesdb_info_schema.result @@ -0,0 +1,27 @@ +# ---- setup ---- +CREATE TABLE t1 ( +id INT PRIMARY KEY, +val VARCHAR(200) +) ENGINE=TidesDB; +INSERT INTO t1 VALUES (1, REPEAT('a', 100)); +INSERT INTO t1 VALUES (2, REPEAT('b', 100)); +INSERT INTO t1 VALUES (3, REPEAT('c', 100)); +# ---- data_length must be non-zero ---- +OK: DATA_LENGTH > 0 +# ---- table_rows must reflect inserted rows ---- +OK: TABLE_ROWS >= 3 +# ---- add secondary index and check index_length ---- +ALTER TABLE t1 ADD INDEX idx_val (val); +Warnings: +Note 1071 Specified key was too long; max key length is 255 bytes +SELECT COUNT(*) FROM t1; +COUNT(*) +3 +OK: INDEX_LENGTH > 0 +# ---- verify after bulk insert ---- +SELECT COUNT(*) FROM t1; +COUNT(*) +200 +OK: DATA_LENGTH > 0 after bulk insert +# ---- cleanup ---- +DROP TABLE t1; diff --git a/mysql-test/suite/tidesdb/t/tidesdb_info_schema.opt b/mysql-test/suite/tidesdb/t/tidesdb_info_schema.opt new file mode 100644 index 0000000..8374626 --- /dev/null +++ b/mysql-test/suite/tidesdb/t/tidesdb_info_schema.opt @@ -0,0 +1 @@ +--plugin-maturity=unknown diff --git a/mysql-test/suite/tidesdb/t/tidesdb_info_schema.test b/mysql-test/suite/tidesdb/t/tidesdb_info_schema.test new file mode 100644 index 0000000..d8af452 --- /dev/null +++ b/mysql-test/suite/tidesdb/t/tidesdb_info_schema.test @@ -0,0 +1,85 @@ +# +# TidesDB information_schema.TABLES size reporting +# Verify DATA_LENGTH and INDEX_LENGTH are non-zero after inserts +# + +--echo # ---- setup ---- +CREATE TABLE t1 ( + id INT PRIMARY KEY, + val VARCHAR(200) +) ENGINE=TidesDB; + +INSERT INTO t1 VALUES (1, REPEAT('a', 100)); +INSERT INTO t1 VALUES (2, REPEAT('b', 100)); +INSERT INTO t1 VALUES (3, REPEAT('c', 100)); + +--echo # ---- data_length must be non-zero ---- +let $data_len = `SELECT DATA_LENGTH FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't1'`; +if (!$data_len) +{ + --echo FAIL: DATA_LENGTH is 0 +} +if ($data_len) +{ + --echo OK: DATA_LENGTH > 0 +} + +--echo # ---- table_rows must reflect inserted rows ---- +let $rows = `SELECT TABLE_ROWS FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't1'`; +if ($rows < 3) +{ + --echo FAIL: TABLE_ROWS < 3 +} +if ($rows >= 3) +{ + --echo OK: TABLE_ROWS >= 3 +} + +--echo # ---- add secondary index and check index_length ---- +ALTER TABLE t1 ADD INDEX idx_val (val); + +# force stats refresh (2s cache) +--sleep 3 + +# touch the table so info() is called fresh +SELECT COUNT(*) FROM t1; + +let $idx_len = `SELECT INDEX_LENGTH FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't1'`; +if (!$idx_len) +{ + --echo FAIL: INDEX_LENGTH is 0 +} +if ($idx_len) +{ + --echo OK: INDEX_LENGTH > 0 +} + +--echo # ---- verify after bulk insert ---- +--disable_query_log +let $i = 4; +while ($i <= 200) +{ + eval INSERT INTO t1 VALUES ($i, REPEAT('x', 100)); + inc $i; +} +--enable_query_log + +--sleep 3 +SELECT COUNT(*) FROM t1; + +let $data_len2 = `SELECT DATA_LENGTH FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't1'`; +if (!$data_len2) +{ + --echo FAIL: DATA_LENGTH is 0 after bulk insert +} +if ($data_len2) +{ + --echo OK: DATA_LENGTH > 0 after bulk insert +} + +--echo # ---- cleanup ---- +DROP TABLE t1; diff --git a/tidesdb/ha_tidesdb.cc b/tidesdb/ha_tidesdb.cc index 522a43c..fe7be66 100644 --- a/tidesdb/ha_tidesdb.cc +++ b/tidesdb/ha_tidesdb.cc @@ -3042,7 +3042,16 @@ int ha_tidesdb::info(uint flag) if (tidesdb_get_stats(share->cf, &st) == TDB_SUCCESS && st) { share->cached_records.store(st->total_keys, std::memory_order_relaxed); - share->cached_data_size.store(st->total_data_size, std::memory_order_relaxed); + + /* total_data_size only counts SSTable klog+vlog; memtable_size + holds the active memtable footprint. Sum both so that + DATA_LENGTH in information_schema.TABLES is non-zero even + before the first flush. When both are 0 (library gap), + fall back to total_keys * avg entry size. */ + uint64_t data_sz = st->total_data_size + (uint64_t)st->memtable_size; + if (data_sz == 0 && st->total_keys > 0) + data_sz = (uint64_t)(st->total_keys * (st->avg_key_size + st->avg_value_size)); + share->cached_data_size.store(data_sz, std::memory_order_relaxed); uint32_t mrl = (uint32_t)(st->avg_key_size + st->avg_value_size); if (mrl == 0) mrl = table->s->reclength; share->cached_mean_rec_len.store(mrl, std::memory_order_relaxed); @@ -3057,7 +3066,11 @@ int ha_tidesdb::info(uint flag) tidesdb_stats_t *ist = NULL; if (tidesdb_get_stats(share->idx_cfs[i], &ist) == TDB_SUCCESS && ist) { - idx_total += ist->total_data_size; + uint64_t isz = ist->total_data_size + (uint64_t)ist->memtable_size; + if (isz == 0 && ist->total_keys > 0) + isz = (uint64_t)(ist->total_keys * + (ist->avg_key_size + ist->avg_value_size)); + idx_total += isz; tidesdb_free_stats(ist); } } @@ -4118,8 +4131,8 @@ maria_declare_plugin(tidesdb){ PLUGIN_LICENSE_GPL, tidesdb_init_func, tidesdb_deinit_func, - 0x30305, + 0x30306, NULL, tidesdb_system_variables, - "3.3.5", + "3.3.6", MariaDB_PLUGIN_MATURITY_EXPERIMENTAL} maria_declare_plugin_end; From 9727a30ee17160966fe1d7f048acfe262e259fe3 Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Thu, 5 Mar 2026 16:13:31 -0500 Subject: [PATCH 2/8] #65 corrected mtr extended; create_time and update_time returning null for all tidesdb tables in information_schema.tables. create_time is now seeded from the .frm file mtime at first open and stored in tidesdb_share. update_time is bumped atomically once per write statement in external_lock(F_UNLCK) when stmt_txn_dirty is true. both are reported under HA_STATUS_TIME in info(), which is the flag information_schema uses. the tidesdb_info_schema mtr test now covers data_length, index_length, table_rows, create_time, and update_time. --- .../tidesdb/r/tidesdb_info_schema.result | 23 ++++--- .../suite/tidesdb/t/tidesdb_info_schema.test | 66 +++++++++++++++---- tidesdb/ha_tidesdb.cc | 18 +++++ tidesdb/ha_tidesdb.h | 4 ++ 4 files changed, 90 insertions(+), 21 deletions(-) diff --git a/mysql-test/suite/tidesdb/r/tidesdb_info_schema.result b/mysql-test/suite/tidesdb/r/tidesdb_info_schema.result index 2594f3a..ac88568 100644 --- a/mysql-test/suite/tidesdb/r/tidesdb_info_schema.result +++ b/mysql-test/suite/tidesdb/r/tidesdb_info_schema.result @@ -1,27 +1,34 @@ # ---- setup ---- -CREATE TABLE t1 ( +CREATE TABLE t_info_schema ( id INT PRIMARY KEY, val VARCHAR(200) ) ENGINE=TidesDB; -INSERT INTO t1 VALUES (1, REPEAT('a', 100)); -INSERT INTO t1 VALUES (2, REPEAT('b', 100)); -INSERT INTO t1 VALUES (3, REPEAT('c', 100)); +INSERT INTO t_info_schema VALUES (1, REPEAT('a', 100)); +INSERT INTO t_info_schema VALUES (2, REPEAT('b', 100)); +INSERT INTO t_info_schema VALUES (3, REPEAT('c', 100)); # ---- data_length must be non-zero ---- OK: DATA_LENGTH > 0 # ---- table_rows must reflect inserted rows ---- OK: TABLE_ROWS >= 3 # ---- add secondary index and check index_length ---- -ALTER TABLE t1 ADD INDEX idx_val (val); +ALTER TABLE t_info_schema ADD INDEX idx_val (val); Warnings: Note 1071 Specified key was too long; max key length is 255 bytes -SELECT COUNT(*) FROM t1; +SELECT COUNT(*) FROM t_info_schema; COUNT(*) 3 OK: INDEX_LENGTH > 0 # ---- verify after bulk insert ---- -SELECT COUNT(*) FROM t1; +SELECT COUNT(*) FROM t_info_schema; COUNT(*) 200 OK: DATA_LENGTH > 0 after bulk insert +# ---- create_time must be non-null ---- +OK: CREATE_TIME is set +# ---- update_time must be non-null after DML ---- +OK: UPDATE_TIME is set +# ---- update_time advances after more DML ---- +INSERT INTO t_info_schema VALUES (9999, 'timestamp_test'); +OK: UPDATE_TIME advanced after INSERT # ---- cleanup ---- -DROP TABLE t1; +DROP TABLE t_info_schema; diff --git a/mysql-test/suite/tidesdb/t/tidesdb_info_schema.test b/mysql-test/suite/tidesdb/t/tidesdb_info_schema.test index d8af452..fab15bd 100644 --- a/mysql-test/suite/tidesdb/t/tidesdb_info_schema.test +++ b/mysql-test/suite/tidesdb/t/tidesdb_info_schema.test @@ -4,18 +4,18 @@ # --echo # ---- setup ---- -CREATE TABLE t1 ( +CREATE TABLE t_info_schema ( id INT PRIMARY KEY, val VARCHAR(200) ) ENGINE=TidesDB; -INSERT INTO t1 VALUES (1, REPEAT('a', 100)); -INSERT INTO t1 VALUES (2, REPEAT('b', 100)); -INSERT INTO t1 VALUES (3, REPEAT('c', 100)); +INSERT INTO t_info_schema VALUES (1, REPEAT('a', 100)); +INSERT INTO t_info_schema VALUES (2, REPEAT('b', 100)); +INSERT INTO t_info_schema VALUES (3, REPEAT('c', 100)); --echo # ---- data_length must be non-zero ---- let $data_len = `SELECT DATA_LENGTH FROM information_schema.TABLES - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't1'`; + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_info_schema'`; if (!$data_len) { --echo FAIL: DATA_LENGTH is 0 @@ -27,7 +27,7 @@ if ($data_len) --echo # ---- table_rows must reflect inserted rows ---- let $rows = `SELECT TABLE_ROWS FROM information_schema.TABLES - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't1'`; + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_info_schema'`; if ($rows < 3) { --echo FAIL: TABLE_ROWS < 3 @@ -38,16 +38,16 @@ if ($rows >= 3) } --echo # ---- add secondary index and check index_length ---- -ALTER TABLE t1 ADD INDEX idx_val (val); +ALTER TABLE t_info_schema ADD INDEX idx_val (val); # force stats refresh (2s cache) --sleep 3 # touch the table so info() is called fresh -SELECT COUNT(*) FROM t1; +SELECT COUNT(*) FROM t_info_schema; let $idx_len = `SELECT INDEX_LENGTH FROM information_schema.TABLES - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't1'`; + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_info_schema'`; if (!$idx_len) { --echo FAIL: INDEX_LENGTH is 0 @@ -62,16 +62,16 @@ if ($idx_len) let $i = 4; while ($i <= 200) { - eval INSERT INTO t1 VALUES ($i, REPEAT('x', 100)); + eval INSERT INTO t_info_schema VALUES ($i, REPEAT('x', 100)); inc $i; } --enable_query_log --sleep 3 -SELECT COUNT(*) FROM t1; +SELECT COUNT(*) FROM t_info_schema; let $data_len2 = `SELECT DATA_LENGTH FROM information_schema.TABLES - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't1'`; + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_info_schema'`; if (!$data_len2) { --echo FAIL: DATA_LENGTH is 0 after bulk insert @@ -81,5 +81,45 @@ if ($data_len2) --echo OK: DATA_LENGTH > 0 after bulk insert } +--echo # ---- create_time must be non-null ---- +let $ct = `SELECT CREATE_TIME FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_info_schema'`; +if (!$ct) +{ + --echo FAIL: CREATE_TIME is NULL +} +if ($ct) +{ + --echo OK: CREATE_TIME is set +} + +--echo # ---- update_time must be non-null after DML ---- +let $ut = `SELECT UPDATE_TIME FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_info_schema'`; +if (!$ut) +{ + --echo FAIL: UPDATE_TIME is NULL +} +if ($ut) +{ + --echo OK: UPDATE_TIME is set +} + +--echo # ---- update_time advances after more DML ---- +let $ut1 = `SELECT UNIX_TIMESTAMP(UPDATE_TIME) FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_info_schema'`; +--sleep 2 +INSERT INTO t_info_schema VALUES (9999, 'timestamp_test'); +let $ut2 = `SELECT UNIX_TIMESTAMP(UPDATE_TIME) FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_info_schema'`; +if ($ut2 >= $ut1) +{ + --echo OK: UPDATE_TIME advanced after INSERT +} +if ($ut2 < $ut1) +{ + --echo FAIL: UPDATE_TIME did not advance +} + --echo # ---- cleanup ---- -DROP TABLE t1; +DROP TABLE t_info_schema; diff --git a/tidesdb/ha_tidesdb.cc b/tidesdb/ha_tidesdb.cc index fe7be66..0c8ed1d 100644 --- a/tidesdb/ha_tidesdb.cc +++ b/tidesdb/ha_tidesdb.cc @@ -1429,6 +1429,14 @@ int ha_tidesdb::open(const char *name, int mode, uint test_if_locked) /* We recover hidden-PK counter (auto-inc is derived at runtime via index_last) */ recover_counters(); + + /* We seed create_time from the .frm file's mtime */ + { + char frm_path[FN_REFLEN]; + fn_format(frm_path, name, "", reg_ext, MY_UNPACK_FILENAME | MY_APPEND_EXT); + MY_STAT st_buf; + if (mysql_file_stat(0, frm_path, &st_buf, MYF(0))) share->create_time = st_buf.st_mtime; + } } unlock_shared_ha_data(); @@ -3091,6 +3099,13 @@ int ha_tidesdb::info(uint flag) stats.mrr_length_per_rec = ref_length + 8; } + /* HA_STATUS_TIME -- create_time from .frm stat, update_time from last DML */ + if ((flag & HA_STATUS_TIME) && share) + { + stats.create_time = share->create_time; + stats.update_time = share->update_time.load(std::memory_order_relaxed); + } + /* HA_STATUS_CONST -- set rec_per_key for index selectivity estimates. For PK (unique) -- rec_per_key = 1. For secondary indexes -- we estimate from total_keys / distinct count. @@ -3535,6 +3550,9 @@ int ha_tidesdb::external_lock(THD *thd, int lock_type) scan_iter_cf_ = NULL; scan_iter_txn_ = NULL; } + /* We bump update_time once per write-statement for information_schema */ + if (stmt_txn_dirty && share) share->update_time.store(time(0), std::memory_order_relaxed); + stmt_txn = NULL; stmt_txn_dirty = false; } diff --git a/tidesdb/ha_tidesdb.h b/tidesdb/ha_tidesdb.h index 48dde57..1a43219 100644 --- a/tidesdb/ha_tidesdb.h +++ b/tidesdb/ha_tidesdb.h @@ -117,6 +117,10 @@ class TidesDB_share : public Handler_share bool has_ttl; /* true when TTL is configured (default_ttl or ttl_field_idx) */ uint num_secondary_indexes; /* count of non-NULL secondary index CFs */ + /* Table timestamps for information_schema.TABLES */ + time_t create_time{0}; /* from .frm stat at first open */ + std::atomic update_time{0}; /* bumped on DML (write/update/delete) */ + /* Cached stats -- avoid expensive tidesdb_get_stats per statement. Refreshed at most every 2 seconds; read with relaxed atomics. */ std::atomic cached_records{0}; From 16ff887a787c3ee72b39d78c432906b8052bfdc1 Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Thu, 5 Mar 2026 16:30:14 -0500 Subject: [PATCH 3/8] #54 corrected; compilation error on mariadb 11.8. the handlerton callback signatures (commit, rollback, close_connection) dropped the handlerton* first parameter and st_typelib gained a hidden_values member. the version guard was >= 120000 which missed 11.8. lowered to >= 110800. #55 corrected; compilation error on mariadb 12.3. TABLE_SHARE::option_struct was removed. added TDB_TABLE_OPTIONS(tbl) compat macro that resolves to tbl->option_struct on 12.3+ and tbl->s->option_struct on earlier versions. all table option accesses now go through this macro. --- tidesdb/ha_tidesdb.cc | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/tidesdb/ha_tidesdb.cc b/tidesdb/ha_tidesdb.cc index 0c8ed1d..c7c61e9 100644 --- a/tidesdb/ha_tidesdb.cc +++ b/tidesdb/ha_tidesdb.cc @@ -43,6 +43,14 @@ static inline long long tdb_now_us() return (long long)microsecond_interval_timer(); } +/* MariaDB 12.3 moved option_struct from TABLE_SHARE to TABLE (MDEV-37815). + We provide a compat macro so the same code compiles on 11.x / 12.0-12.2 / 12.3+. */ +#if MYSQL_VERSION_ID >= 120300 +#define TDB_TABLE_OPTIONS(tbl) ((tbl)->option_struct) +#else +#define TDB_TABLE_OPTIONS(tbl) ((tbl)->s->option_struct) +#endif + /* Declared early so tdb_rc_to_ha() can reference it; default 0, toggled at runtime via SET GLOBAL tidesdb_debug_trace. */ static my_bool srv_debug_trace = 0; @@ -128,7 +136,7 @@ static ulong srv_max_open_sstables = 256; static ulonglong srv_max_memory_usage = 0; /* 0 = auto (library decides) */ static const char *log_level_names[] = {"DEBUG", "INFO", "WARN", "ERROR", "FATAL", "NONE", NullS}; -#if MYSQL_VERSION_ID >= 120000 +#if MYSQL_VERSION_ID >= 110800 static TYPELIB log_level_typelib = {array_elements(log_level_names) - 1, "log_level_typelib", log_level_names, NULL, NULL}; #else @@ -504,7 +512,7 @@ static int tidesdb_savepoint_release(THD *thd, void *sv) return tdb_rc_to_ha(rc, "savepoint_release"); } -#if MYSQL_VERSION_ID >= 120000 +#if MYSQL_VERSION_ID >= 110800 static int tidesdb_commit(THD *thd, bool all) #else static int tidesdb_commit(handlerton *, THD *thd, bool all) @@ -585,7 +593,7 @@ static int tidesdb_commit(handlerton *, THD *thd, bool all) return 0; } -#if MYSQL_VERSION_ID >= 120000 +#if MYSQL_VERSION_ID >= 110800 static int tidesdb_rollback(THD *thd, bool all) #else static int tidesdb_rollback(handlerton *, THD *thd, bool all) @@ -629,7 +637,7 @@ static int tidesdb_rollback(handlerton *, THD *thd, bool all) return 0; } -#if MYSQL_VERSION_ID >= 120000 +#if MYSQL_VERSION_ID >= 110800 static int tidesdb_close_connection(THD *thd) #else static int tidesdb_close_connection(handlerton *, THD *thd) @@ -1352,24 +1360,24 @@ int ha_tidesdb::open(const char *name, int mode, uint test_if_locked) } /* We read isolation level from table options */ - if (table->s->option_struct) + if (TDB_TABLE_OPTIONS(table)) { - uint iso_idx = table->s->option_struct->isolation_level; + uint iso_idx = TDB_TABLE_OPTIONS(table)->isolation_level; if (iso_idx < array_elements(tdb_isolation_map)) share->isolation_level = (tidesdb_isolation_level_t)tdb_isolation_map[iso_idx]; } /* We read TTL configuration from table + field options */ - if (table->s->option_struct) share->default_ttl = table->s->option_struct->ttl; + if (TDB_TABLE_OPTIONS(table)) share->default_ttl = TDB_TABLE_OPTIONS(table)->ttl; /* We read encryption configuration from table options */ share->encrypted = false; share->encryption_key_id = 1; share->encryption_key_version = 0; - if (table->s->option_struct && table->s->option_struct->encrypted) + if (TDB_TABLE_OPTIONS(table) && TDB_TABLE_OPTIONS(table)->encrypted) { share->encrypted = true; - share->encryption_key_id = (uint)table->s->option_struct->encryption_key_id; + share->encryption_key_id = (uint)TDB_TABLE_OPTIONS(table)->encryption_key_id; uint ver = encryption_key_get_latest_version(share->encryption_key_id); if (ver == ENCRYPTION_KEY_VERSION_INVALID) { @@ -1472,7 +1480,7 @@ int ha_tidesdb::create(const char *name, TABLE *table_arg, HA_CREATE_INFO *creat std::string cf_name = path_to_cf_name(name); - ha_table_option_struct *opts = table_arg->s->option_struct; + ha_table_option_struct *opts = TDB_TABLE_OPTIONS(table_arg); DBUG_ASSERT(opts); tidesdb_column_family_config_t cfg = build_cf_config(opts); @@ -2942,7 +2950,7 @@ int ha_tidesdb::delete_all_rows(void) stmt_txn_dirty = false; } - tidesdb_column_family_config_t cfg = build_cf_config(table->s->option_struct); + tidesdb_column_family_config_t cfg = build_cf_config(TDB_TABLE_OPTIONS(table)); /* We drop and recreate the main data CF (O(1) instead of iterating all keys) */ { @@ -3640,7 +3648,7 @@ bool ha_tidesdb::prepare_inplace_alter_table(TABLE *altered_table, } ha_alter_info->handler_ctx = ctx; - tidesdb_column_family_config_t cfg = build_cf_config(table->s->option_struct); + tidesdb_column_family_config_t cfg = build_cf_config(TDB_TABLE_OPTIONS(table)); std::string base_cf = share->cf_name; From 74609ef74c54beacf84fb4209dfd4119941a7d7c Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Thu, 5 Mar 2026 16:56:28 -0500 Subject: [PATCH 4/8] #58 corrected mtr extended; duplicate primary key and unique index inserts were silently accepted because tidesdb_txn_put overwrites existing keys without error. write_row now checks for existing pk via tidesdb_txn_get before inserting, and checks unique secondary indexes via iterator prefix scan. returns HA_ERR_FOUND_DUPP_KEY with lookup_errkey set so mariadb produces the standard ER_DUP_ENTRY error. the check is skipped for REPLACE INTO and INSERT ON DUPLICATE KEY UPDATE via the write_can_replace_ flag set from HA_EXTRA_WRITE_CAN_REPLACE and HA_EXTRA_INSERT_WITH_UPDATE in extra(). the tidesdb_stress test was updated to verify both duplicate rejection and REPLACE INTO behavior. #53 updated read me mariadb support --- README | 2 ++ tidesdb/ha_tidesdb.cc | 65 ++++++++++++++++++++++++++++++++++++++++++- tidesdb/ha_tidesdb.h | 1 + 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/README b/README index feb1a65..8de5ffe 100644 --- a/README +++ b/README @@ -3,6 +3,8 @@ TIDESQL A pluggable write and space optimized storage engine for MariaDB using TidesDB. +The pluggable engine is tested and supported on MariaDB 11.x, 12.x. + INSTALLATION ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ diff --git a/tidesdb/ha_tidesdb.cc b/tidesdb/ha_tidesdb.cc index c7c61e9..7d7f14e 100644 --- a/tidesdb/ha_tidesdb.cc +++ b/tidesdb/ha_tidesdb.cc @@ -790,7 +790,8 @@ ha_tidesdb::ha_tidesdb(handlerton *hton, TABLE_SHARE *table_arg) idx_search_comp_len_(0), in_bulk_insert_(false), bulk_insert_ops_(0), - keyread_only_(false) + keyread_only_(false), + write_can_replace_(false) { } @@ -1900,6 +1901,61 @@ int ha_tidesdb::write_row(const uchar *buf) t2 = tdb_now_us(); } + /* We check PK uniqueness before inserting (TidesDB put overwrites silently). + Skip when REPLACE INTO or INSERT ON DUPLICATE KEY UPDATE -- the server + handles the conflict by deleting the old row first (HA_EXTRA_WRITE_CAN_REPLACE). */ + if (share->has_user_pk && !write_can_replace_) + { + uint8_t *dup_val = NULL; + size_t dup_len = 0; + int grc = tidesdb_txn_get(txn, share->cf, dk, dk_len, &dup_val, &dup_len); + if (grc == TDB_SUCCESS) + { + tidesdb_free(dup_val); + errkey = lookup_errkey = share->pk_index; + tmp_restore_column_map(&table->read_set, old_map); + DBUG_RETURN(HA_ERR_FOUND_DUPP_KEY); + } + if (grc != TDB_ERR_NOT_FOUND) + { + tmp_restore_column_map(&table->read_set, old_map); + DBUG_RETURN(tdb_rc_to_ha(grc, "write_row pk_dup_check")); + } + } + + /* We check UNIQUE secondary index uniqueness via prefix scan */ + if (share->num_secondary_indexes > 0 && !write_can_replace_) + { + for (uint i = 0; i < table->s->keys; i++) + { + if (share->has_user_pk && i == share->pk_index) continue; + if (i >= share->idx_cfs.size() || !share->idx_cfs[i]) continue; + if (!(table->key_info[i].flags & HA_NOSAME)) continue; + + uchar idx_prefix[MAX_KEY_LENGTH]; + uint idx_prefix_len = make_comparable_key( + &table->key_info[i], buf, table->key_info[i].user_defined_key_parts, idx_prefix); + tidesdb_iter_t *dup_iter = NULL; + if (tidesdb_iter_new(txn, share->idx_cfs[i], &dup_iter) != TDB_SUCCESS || !dup_iter) + continue; + tidesdb_iter_seek(dup_iter, idx_prefix, idx_prefix_len); + if (tidesdb_iter_valid(dup_iter)) + { + uint8_t *fk = NULL; + size_t fks = 0; + if (tidesdb_iter_key(dup_iter, &fk, &fks) == TDB_SUCCESS && fks >= idx_prefix_len && + memcmp(fk, idx_prefix, idx_prefix_len) == 0) + { + tidesdb_iter_free(dup_iter); + errkey = lookup_errkey = i; + tmp_restore_column_map(&table->read_set, old_map); + DBUG_RETURN(HA_ERR_FOUND_DUPP_KEY); + } + } + tidesdb_iter_free(dup_iter); + } + } + /* We compute TTL only when the table has TTL configured */ time_t row_ttl = share->has_ttl ? compute_row_ttl(buf) : TIDESDB_TTL_NONE; @@ -3440,6 +3496,13 @@ int ha_tidesdb::extra(enum ha_extra_function operation) case HA_EXTRA_NO_KEYREAD: keyread_only_ = false; break; + case HA_EXTRA_WRITE_CAN_REPLACE: + case HA_EXTRA_INSERT_WITH_UPDATE: + write_can_replace_ = true; + break; + case HA_EXTRA_WRITE_CANNOT_REPLACE: + write_can_replace_ = false; + break; case HA_EXTRA_PREPARE_FOR_DROP: /* Table is about to be dropped -- skip fsync overhead */ break; diff --git a/tidesdb/ha_tidesdb.h b/tidesdb/ha_tidesdb.h index 1a43219..c514434 100644 --- a/tidesdb/ha_tidesdb.h +++ b/tidesdb/ha_tidesdb.h @@ -221,6 +221,7 @@ class ha_tidesdb : public handler /* Covering-index mode (HA_EXTRA_KEYREAD) */ bool keyread_only_; + bool write_can_replace_; /* true during REPLACE INTO / INSERT ON DUPLICATE KEY UPDATE */ /* ----- private helpers ---------------------------------------------------------------------- */ From 646943a6b12fcb1971d052579b1c392470048f32 Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Thu, 5 Mar 2026 16:57:23 -0500 Subject: [PATCH 5/8] update mtr --- .../suite/tidesdb/r/tidesdb_online_ddl.result | 92 +++++++++---------- .../suite/tidesdb/r/tidesdb_stress.result | 12 ++- .../suite/tidesdb/t/tidesdb_online_ddl.test | 68 +++++++------- .../suite/tidesdb/t/tidesdb_stress.test | 17 ++-- 4 files changed, 98 insertions(+), 91 deletions(-) diff --git a/mysql-test/suite/tidesdb/r/tidesdb_online_ddl.result b/mysql-test/suite/tidesdb/r/tidesdb_online_ddl.result index 27f6708..b359f7c 100644 --- a/mysql-test/suite/tidesdb/r/tidesdb_online_ddl.result +++ b/mysql-test/suite/tidesdb/r/tidesdb_online_ddl.result @@ -1,31 +1,31 @@ # ---- Setup ---- -CREATE TABLE t1 ( +CREATE TABLE t_ddl ( id INT PRIMARY KEY, a INT, b VARCHAR(100), c INT DEFAULT 0 ) ENGINE=TidesDB; -INSERT INTO t1 VALUES (1, 10, 'alpha', 100); -INSERT INTO t1 VALUES (2, 20, 'beta', 200); -INSERT INTO t1 VALUES (3, 30, 'gamma', 300); -INSERT INTO t1 VALUES (4, 10, 'delta', 400); -INSERT INTO t1 VALUES (5, 50, 'epsilon', 500); +INSERT INTO t_ddl VALUES (1, 10, 'alpha', 100); +INSERT INTO t_ddl VALUES (2, 20, 'beta', 200); +INSERT INTO t_ddl VALUES (3, 30, 'gamma', 300); +INSERT INTO t_ddl VALUES (4, 10, 'delta', 400); +INSERT INTO t_ddl VALUES (5, 50, 'epsilon', 500); # ---- INSTANT: change column default ---- -ALTER TABLE t1 ALTER COLUMN c SET DEFAULT 999, ALGORITHM=INSTANT; -INSERT INTO t1 (id, a, b) VALUES (6, 60, 'zeta'); -SELECT id, c FROM t1 WHERE id = 6; +ALTER TABLE t_ddl ALTER COLUMN c SET DEFAULT 999, ALGORITHM=INSTANT; +INSERT INTO t_ddl (id, a, b) VALUES (6, 60, 'zeta'); +SELECT id, c FROM t_ddl WHERE id = 6; id c 6 999 # ---- INSTANT: rename column ---- -ALTER TABLE t1 CHANGE b b_name VARCHAR(100), ALGORITHM=INSTANT; -SELECT id, b_name FROM t1 WHERE id = 1; +ALTER TABLE t_ddl CHANGE b b_name VARCHAR(100), ALGORITHM=INSTANT; +SELECT id, b_name FROM t_ddl WHERE id = 1; id b_name 1 alpha # ---- INSTANT: change table option (SYNC_MODE) ---- -ALTER TABLE t1 SYNC_MODE='NONE', ALGORITHM=INSTANT; -SHOW CREATE TABLE t1; +ALTER TABLE t_ddl SYNC_MODE='NONE', ALGORITHM=INSTANT; +SHOW CREATE TABLE t_ddl; Table Create Table -t1 CREATE TABLE `t1` ( +t_ddl CREATE TABLE `t_ddl` ( `id` int(11) NOT NULL, `a` int(11) DEFAULT NULL, `b_name` varchar(100) DEFAULT NULL, @@ -33,71 +33,71 @@ t1 CREATE TABLE `t1` ( PRIMARY KEY (`id`) ) ENGINE=TIDESDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci `SYNC_MODE`='NONE' # ---- INPLACE: add secondary index ---- -ALTER TABLE t1 ADD INDEX idx_a (a), ALGORITHM=INPLACE; -SHOW INDEX FROM t1; +ALTER TABLE t_ddl ADD INDEX idx_a (a), ALGORITHM=INPLACE; +SHOW INDEX FROM t_ddl; Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored -t1 0 PRIMARY 1 id A 6 NULL NULL BTREE NO -t1 1 idx_a 1 a A 6 NULL NULL YES BTREE NO +t_ddl 0 PRIMARY 1 id A 6 NULL NULL BTREE NO +t_ddl 1 idx_a 1 a A 6 NULL NULL YES BTREE NO # Verify index is usable -SELECT id, a FROM t1 WHERE a = 10 ORDER BY id; +SELECT id, a FROM t_ddl WHERE a = 10 ORDER BY id; id a 1 10 4 10 -SELECT id, a FROM t1 WHERE a >= 30 ORDER BY a; +SELECT id, a FROM t_ddl WHERE a >= 30 ORDER BY a; id a 3 30 5 50 6 60 # ---- INPLACE: add another index ---- -ALTER TABLE t1 ADD INDEX idx_c (c), ALGORITHM=INPLACE; -SHOW INDEX FROM t1; +ALTER TABLE t_ddl ADD INDEX idx_c (c), ALGORITHM=INPLACE; +SHOW INDEX FROM t_ddl; Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored -t1 0 PRIMARY 1 id A 6 NULL NULL BTREE NO -t1 1 idx_a 1 a A 6 NULL NULL YES BTREE NO -t1 1 idx_c 1 c A 6 NULL NULL YES BTREE NO -EXPLAIN SELECT id, c FROM t1 WHERE c = 200; +t_ddl 0 PRIMARY 1 id A 6 NULL NULL BTREE NO +t_ddl 1 idx_a 1 a A 6 NULL NULL YES BTREE NO +t_ddl 1 idx_c 1 c A 6 NULL NULL YES BTREE NO +EXPLAIN SELECT id, c FROM t_ddl WHERE c = 200; id select_type table type possible_keys key key_len ref rows Extra -1 SIMPLE t1 ref idx_c idx_c 5 const 1 Using index -SELECT id, c FROM t1 WHERE c = 200; +1 SIMPLE t_ddl ref idx_c idx_c 5 const 1 Using index +SELECT id, c FROM t_ddl WHERE c = 200; id c 2 200 # ---- INPLACE: drop index ---- -ALTER TABLE t1 DROP INDEX idx_a, ALGORITHM=INPLACE; -SHOW INDEX FROM t1; +ALTER TABLE t_ddl DROP INDEX idx_a, ALGORITHM=INPLACE; +SHOW INDEX FROM t_ddl; Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored -t1 0 PRIMARY 1 id A 6 NULL NULL BTREE NO -t1 1 idx_c 1 c A 6 NULL NULL YES BTREE NO +t_ddl 0 PRIMARY 1 id A 6 NULL NULL BTREE NO +t_ddl 1 idx_c 1 c A 6 NULL NULL YES BTREE NO # Verify remaining index still works -SELECT id, c FROM t1 WHERE c = 300; +SELECT id, c FROM t_ddl WHERE c = 300; id c 3 300 # ---- INPLACE: add + drop in one statement ---- -ALTER TABLE t1 ADD INDEX idx_a2 (a), DROP INDEX idx_c, ALGORITHM=INPLACE; -SHOW INDEX FROM t1; +ALTER TABLE t_ddl ADD INDEX idx_a2 (a), DROP INDEX idx_c, ALGORITHM=INPLACE; +SHOW INDEX FROM t_ddl; Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored -t1 0 PRIMARY 1 id A 6 NULL NULL BTREE NO -t1 1 idx_a2 1 a A 6 NULL NULL YES BTREE NO -EXPLAIN SELECT id, a FROM t1 WHERE a = 20; +t_ddl 0 PRIMARY 1 id A 6 NULL NULL BTREE NO +t_ddl 1 idx_a2 1 a A 6 NULL NULL YES BTREE NO +EXPLAIN SELECT id, a FROM t_ddl WHERE a = 20; id select_type table type possible_keys key key_len ref rows Extra -1 SIMPLE t1 ref idx_a2 idx_a2 5 const 1 Using index -SELECT id, a FROM t1 WHERE a = 20; +1 SIMPLE t_ddl ref idx_a2 idx_a2 5 const 1 Using index +SELECT id, a FROM t_ddl WHERE a = 20; id a 2 20 # ---- COPY fallback: add column ---- -ALTER TABLE t1 ADD COLUMN d INT DEFAULT 0; -SELECT id, d FROM t1 WHERE id = 1; +ALTER TABLE t_ddl ADD COLUMN d INT DEFAULT 0; +SELECT id, d FROM t_ddl WHERE id = 1; id d 1 0 # ---- COPY fallback: drop column ---- -ALTER TABLE t1 DROP COLUMN d; -SELECT * FROM t1 WHERE id = 1; +ALTER TABLE t_ddl DROP COLUMN d; +SELECT * FROM t_ddl WHERE id = 1; id a b_name c 1 10 alpha 100 # ---- Verify ALGORITHM=INPLACE rejected for column changes ---- -ALTER TABLE t1 ADD COLUMN e INT, ALGORITHM=INPLACE; +ALTER TABLE t_ddl ADD COLUMN e INT, ALGORITHM=INPLACE; ERROR 0A000: ALGORITHM=INPLACE is not supported for this operation. Try ALGORITHM=COPY # ---- Cleanup ---- -DROP TABLE t1; +DROP TABLE t_ddl; # ---- Test with data and hidden PK (no explicit PK) ---- CREATE TABLE t_nopk ( a INT, diff --git a/mysql-test/suite/tidesdb/r/tidesdb_stress.result b/mysql-test/suite/tidesdb/r/tidesdb_stress.result index b2adc02..72a9363 100644 --- a/mysql-test/suite/tidesdb/r/tidesdb_stress.result +++ b/mysql-test/suite/tidesdb/r/tidesdb_stress.result @@ -449,17 +449,19 @@ min_s max_s COMMIT; # # ============================================ -# TEST 19: Duplicate PK insert in transaction (upsert semantics) -# TidesDB uses KV put-overwrites, so a duplicate PK INSERT -# acts as an upsert. Verify the txn continues and the -# last write wins. +# TEST 19: PK uniqueness enforcement and REPLACE INTO +# Duplicate PK INSERT must return an error. +# REPLACE INTO overwrites the existing row. # ============================================ # CREATE TABLE stress_uniq (id INT PRIMARY KEY, val VARCHAR(50)) ENGINE=TIDESDB; INSERT INTO stress_uniq VALUES (1, 'first'); +INSERT INTO stress_uniq VALUES (1, 'should_fail'); +ERROR 23000: Duplicate entry '1' for key 'PRIMARY' +REPLACE INTO stress_uniq VALUES (1, 'replaced'); BEGIN; INSERT INTO stress_uniq VALUES (2, 'second'); -INSERT INTO stress_uniq VALUES (1, 'overwritten'); +REPLACE INTO stress_uniq VALUES (1, 'overwritten'); INSERT INTO stress_uniq VALUES (3, 'third'); COMMIT; SELECT * FROM stress_uniq ORDER BY id; diff --git a/mysql-test/suite/tidesdb/t/tidesdb_online_ddl.test b/mysql-test/suite/tidesdb/t/tidesdb_online_ddl.test index e136d19..849f58e 100644 --- a/mysql-test/suite/tidesdb/t/tidesdb_online_ddl.test +++ b/mysql-test/suite/tidesdb/t/tidesdb_online_ddl.test @@ -4,73 +4,73 @@ # --echo # ---- Setup ---- -CREATE TABLE t1 ( +CREATE TABLE t_ddl ( id INT PRIMARY KEY, a INT, b VARCHAR(100), c INT DEFAULT 0 ) ENGINE=TidesDB; -INSERT INTO t1 VALUES (1, 10, 'alpha', 100); -INSERT INTO t1 VALUES (2, 20, 'beta', 200); -INSERT INTO t1 VALUES (3, 30, 'gamma', 300); -INSERT INTO t1 VALUES (4, 10, 'delta', 400); -INSERT INTO t1 VALUES (5, 50, 'epsilon', 500); +INSERT INTO t_ddl VALUES (1, 10, 'alpha', 100); +INSERT INTO t_ddl VALUES (2, 20, 'beta', 200); +INSERT INTO t_ddl VALUES (3, 30, 'gamma', 300); +INSERT INTO t_ddl VALUES (4, 10, 'delta', 400); +INSERT INTO t_ddl VALUES (5, 50, 'epsilon', 500); --echo # ---- INSTANT: change column default ---- -ALTER TABLE t1 ALTER COLUMN c SET DEFAULT 999, ALGORITHM=INSTANT; -INSERT INTO t1 (id, a, b) VALUES (6, 60, 'zeta'); -SELECT id, c FROM t1 WHERE id = 6; +ALTER TABLE t_ddl ALTER COLUMN c SET DEFAULT 999, ALGORITHM=INSTANT; +INSERT INTO t_ddl (id, a, b) VALUES (6, 60, 'zeta'); +SELECT id, c FROM t_ddl WHERE id = 6; --echo # ---- INSTANT: rename column ---- -ALTER TABLE t1 CHANGE b b_name VARCHAR(100), ALGORITHM=INSTANT; -SELECT id, b_name FROM t1 WHERE id = 1; +ALTER TABLE t_ddl CHANGE b b_name VARCHAR(100), ALGORITHM=INSTANT; +SELECT id, b_name FROM t_ddl WHERE id = 1; --echo # ---- INSTANT: change table option (SYNC_MODE) ---- -ALTER TABLE t1 SYNC_MODE='NONE', ALGORITHM=INSTANT; -SHOW CREATE TABLE t1; +ALTER TABLE t_ddl SYNC_MODE='NONE', ALGORITHM=INSTANT; +SHOW CREATE TABLE t_ddl; --echo # ---- INPLACE: add secondary index ---- -ALTER TABLE t1 ADD INDEX idx_a (a), ALGORITHM=INPLACE; -SHOW INDEX FROM t1; +ALTER TABLE t_ddl ADD INDEX idx_a (a), ALGORITHM=INPLACE; +SHOW INDEX FROM t_ddl; --echo # Verify index is usable -SELECT id, a FROM t1 WHERE a = 10 ORDER BY id; -SELECT id, a FROM t1 WHERE a >= 30 ORDER BY a; +SELECT id, a FROM t_ddl WHERE a = 10 ORDER BY id; +SELECT id, a FROM t_ddl WHERE a >= 30 ORDER BY a; --echo # ---- INPLACE: add another index ---- -ALTER TABLE t1 ADD INDEX idx_c (c), ALGORITHM=INPLACE; -SHOW INDEX FROM t1; -EXPLAIN SELECT id, c FROM t1 WHERE c = 200; -SELECT id, c FROM t1 WHERE c = 200; +ALTER TABLE t_ddl ADD INDEX idx_c (c), ALGORITHM=INPLACE; +SHOW INDEX FROM t_ddl; +EXPLAIN SELECT id, c FROM t_ddl WHERE c = 200; +SELECT id, c FROM t_ddl WHERE c = 200; --echo # ---- INPLACE: drop index ---- -ALTER TABLE t1 DROP INDEX idx_a, ALGORITHM=INPLACE; -SHOW INDEX FROM t1; +ALTER TABLE t_ddl DROP INDEX idx_a, ALGORITHM=INPLACE; +SHOW INDEX FROM t_ddl; --echo # Verify remaining index still works -SELECT id, c FROM t1 WHERE c = 300; +SELECT id, c FROM t_ddl WHERE c = 300; --echo # ---- INPLACE: add + drop in one statement ---- -ALTER TABLE t1 ADD INDEX idx_a2 (a), DROP INDEX idx_c, ALGORITHM=INPLACE; -SHOW INDEX FROM t1; -EXPLAIN SELECT id, a FROM t1 WHERE a = 20; -SELECT id, a FROM t1 WHERE a = 20; +ALTER TABLE t_ddl ADD INDEX idx_a2 (a), DROP INDEX idx_c, ALGORITHM=INPLACE; +SHOW INDEX FROM t_ddl; +EXPLAIN SELECT id, a FROM t_ddl WHERE a = 20; +SELECT id, a FROM t_ddl WHERE a = 20; --echo # ---- COPY fallback: add column ---- -ALTER TABLE t1 ADD COLUMN d INT DEFAULT 0; -SELECT id, d FROM t1 WHERE id = 1; +ALTER TABLE t_ddl ADD COLUMN d INT DEFAULT 0; +SELECT id, d FROM t_ddl WHERE id = 1; --echo # ---- COPY fallback: drop column ---- -ALTER TABLE t1 DROP COLUMN d; -SELECT * FROM t1 WHERE id = 1; +ALTER TABLE t_ddl DROP COLUMN d; +SELECT * FROM t_ddl WHERE id = 1; --echo # ---- Verify ALGORITHM=INPLACE rejected for column changes ---- --error ER_ALTER_OPERATION_NOT_SUPPORTED -ALTER TABLE t1 ADD COLUMN e INT, ALGORITHM=INPLACE; +ALTER TABLE t_ddl ADD COLUMN e INT, ALGORITHM=INPLACE; --echo # ---- Cleanup ---- -DROP TABLE t1; +DROP TABLE t_ddl; --echo # ---- Test with data and hidden PK (no explicit PK) ---- CREATE TABLE t_nopk ( diff --git a/mysql-test/suite/tidesdb/t/tidesdb_stress.test b/mysql-test/suite/tidesdb/t/tidesdb_stress.test index 31296f5..42b3b86 100644 --- a/mysql-test/suite/tidesdb/t/tidesdb_stress.test +++ b/mysql-test/suite/tidesdb/t/tidesdb_stress.test @@ -501,20 +501,25 @@ COMMIT; --echo # --echo # ============================================ ---echo # TEST 19: Duplicate PK insert in transaction (upsert semantics) ---echo # TidesDB uses KV put-overwrites, so a duplicate PK INSERT ---echo # acts as an upsert. Verify the txn continues and the ---echo # last write wins. +--echo # TEST 19: PK uniqueness enforcement and REPLACE INTO +--echo # Duplicate PK INSERT must return an error. +--echo # REPLACE INTO overwrites the existing row. --echo # ============================================ --echo # CREATE TABLE stress_uniq (id INT PRIMARY KEY, val VARCHAR(50)) ENGINE=TIDESDB; INSERT INTO stress_uniq VALUES (1, 'first'); +# Duplicate PK INSERT must fail +--error ER_DUP_ENTRY +INSERT INTO stress_uniq VALUES (1, 'should_fail'); + +# REPLACE INTO should overwrite +REPLACE INTO stress_uniq VALUES (1, 'replaced'); + BEGIN; INSERT INTO stress_uniq VALUES (2, 'second'); -# Duplicate PK -- TidesDB overwrites (upsert), no error -INSERT INTO stress_uniq VALUES (1, 'overwritten'); +REPLACE INTO stress_uniq VALUES (1, 'overwritten'); INSERT INTO stress_uniq VALUES (3, 'third'); COMMIT; From 821810bcfab12c205049ec480be99c1dbe0e66d7 Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Thu, 5 Mar 2026 17:09:54 -0500 Subject: [PATCH 6/8] #59 also fixed ALTER TABLE ADD UNIQUE silently dropping rows when duplicates exist. inplace_alter_table now tracks seen index-column prefixes per unique index via std::set during the population scan. if a duplicate prefix is found the ALTER is aborted with ER_DUP_ENTRY and the partially built index CF is rolled back. the tidesdb_online_ddl test now verifies that ADD UNIQUE rejects duplicates and preserves all existing rows --- tidesdb/ha_tidesdb.cc | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tidesdb/ha_tidesdb.cc b/tidesdb/ha_tidesdb.cc index 7d7f14e..912445b 100644 --- a/tidesdb/ha_tidesdb.cc +++ b/tidesdb/ha_tidesdb.cc @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -3824,6 +3825,17 @@ bool ha_tidesdb::inplace_alter_table(TABLE *altered_table, Alter_inplace_info *h ha_rows rows_processed = 0; + /* For UNIQUE indexes, we track seen index-column prefixes to detect + duplicates. If a duplicate is found we must abort the ALTER. */ + std::vector idx_is_unique(ctx->add_cfs.size(), false); + std::vector> idx_seen(ctx->add_cfs.size()); + for (uint a = 0; a < ctx->add_cfs.size(); a++) + { + uint key_num = ctx->add_key_nums[a]; + KEY *ki = &altered_table->key_info[key_num]; + if (ki->flags & HA_NOSAME) idx_is_unique[a] = true; + } + /* We remember the last data key so we can seek directly to it after a batch commit, instead of walking from the beginning (O(n²)). */ uchar last_data_key[MAX_KEY_LENGTH + 2]; @@ -3902,6 +3914,22 @@ bool ha_tidesdb::inplace_alter_table(TABLE *altered_table, Alter_inplace_info *h pos += kp->length; if (field->real_maybe_null()) pos++; } + /* Check UNIQUE constraint before inserting */ + if (idx_is_unique[a]) + { + std::string prefix((const char *)ik, pos); + if (!idx_seen[a].insert(prefix).second) + { + /* Duplicate found -- abort the ALTER */ + tidesdb_iter_free(iter); + tidesdb_txn_rollback(txn); + tidesdb_txn_free(txn); + tmp_restore_column_map(&altered_table->read_set, old_map); + my_error(ER_DUP_ENTRY, MYF(0), "?", altered_table->key_info[key_num].name.str); + DBUG_RETURN(true); + } + } + /* We append PK to make the key unique */ memcpy(ik + pos, pk, pk_len); pos += pk_len; From 4a89be844c491bfb6e788d4dc7232aa9eb158b88 Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Fri, 6 Mar 2026 21:53:03 -0500 Subject: [PATCH 7/8] #55 corrected; compilation error on mariadb 12.3. TABLE_SHARE::option_struct was removed. added TDB_TABLE_OPTIONS(tbl) compat macro that resolves to tbl->option_struct on 12.3+ and tbl->s->option_struct on earlier versions. all table option accesses now go through this macro. #61 corrected; converting InnoDB tables with foreign keys to TidesDB failed with ER_ROW_IS_REFERENCED ('Cannot delete or update a parent row'). the MariaDB SQL layer called InnoDB's can_switch_engines() which unconditionally returns false when foreign keys exist, and the check did not respect SET FOREIGN_KEY_CHECKS=0 unlike other FK checks in ALTER TABLE. fixed by adding the OPTION_NO_FOREIGN_KEY_CHECKS guard to the can_switch_engines check in sql/sql_table.cc. users can now SET FOREIGN_KEY_CHECKS=0 before ALTER TABLE t ENGINE=TidesDB. the foreign keys are silently dropped since TidesDB does not support them. a new tidesdb_fk_convert mtr test verifies self-referencing and parent-child FK conversions with data preservation. #70 corrected; ALTER TABLE crashes MariaDB with large tables. the copy-based ALTER TABLE scan reads every row from the source table. with REPEATABLE_READ isolation (e.g. when autocommit=0), the TidesDB library added each key to the transaction's read-set for conflict detection, causing unbounded memory growth and heap corruption in tidesdb_txn_add_to_read_set during realloc. fixed by forcing READ_COMMITTED isolation for all DDL operations (ALTER TABLE, CREATE/DROP INDEX, TRUNCATE, OPTIMIZE, CREATE TABLE, DROP TABLE) in both external_lock and ensure_stmt_txn. at READ_COMMITTED the library skips read-set tracking entirely. DDL never needs OCC conflict checks so this is always safe. a new tidesdb_alter_crash mtr test verifies ALTER TABLE with ~100K rows under autocommit=0 completes without crash. #64 verified mtr extended; START TRANSACTION WITH CONSISTENT SNAPSHOT now works correctly. the start_consistent_snapshot handlerton callback was already implemented -- it eagerly creates a TidesDB transaction with REPEATABLE_READ isolation so the snapshot sequence number is captured immediately, not lazily at first data access. rows committed by other connections after the snapshot are correctly filtered by the library's sequence-based visibility check (seq <= snapshot_seq). a new tidesdb_consistent_snapshot mtr test verifies snapshot isolation across connections, multiple inserts after snapshot, and that plain BEGIN (without CONSISTENT SNAPSHOT) correctly sees new data. #57 corrected mtr extended; data survived DROP TABLE and CREATE OR REPLACE TABLE because the handlerton drop_table callback returned -1 ('not handled'). mariadb 12.x only calls hton->drop_table during DROP TABLE -- the handler::delete_table method is never reached. since the callback returned -1, the TidesDB column families and their on-disk directories were never removed. the next CREATE TABLE with the same name reused the existing CF with all its old SSTables. fixed by implementing tidesdb_hton_drop_table which calls tidesdb_drop_column_family for the main data CF and all secondary index CFs, then force-removes directories as a safety net. both hton->drop_table and handler::delete_table now share the same tidesdb_drop_table_impl. a new tidesdb_drop_create mtr test verifies DROP + CREATE, CREATE OR REPLACE, secondary index cleanup, and TRUNCATE TABLE. #56 added per-session TTL override via MYSQL_THDVAR_ULONGLONG. users can now set tidesdb_ttl at session scope to apply TTL to inserts/updates on any table, even tables without a table-level TTL option. priority: per-row TTL_COL > session tidesdb_ttl > table TTL option > no expiration. supports SET SESSION tidesdb_ttl=N as well as SET STATEMENT tidesdb_ttl=N FOR INSERT which scopes TTL to a single statement. the tidesdb_ttl mtr test now covers session override, SET STATEMENT syntax, and priority ordering vs per-row TTL_COL. #53 updated read me mariadb support --- README | 15 +-- .../tidesdb/r/tidesdb_alter_crash.result | 48 ++++++++ .../r/tidesdb_consistent_snapshot.result | 68 +++++++++++ .../tidesdb/r/tidesdb_drop_create.result | 79 +++++++++++++ .../suite/tidesdb/r/tidesdb_fk_convert.result | 82 ++++++++++++++ .../suite/tidesdb/r/tidesdb_online_ddl.result | 21 ++++ mysql-test/suite/tidesdb/r/tidesdb_ttl.result | 74 ++++++++++++ .../suite/tidesdb/t/tidesdb_alter_crash.opt | 2 + .../suite/tidesdb/t/tidesdb_alter_crash.test | 46 ++++++++ .../tidesdb/t/tidesdb_consistent_snapshot.opt | 2 + .../t/tidesdb_consistent_snapshot.test | 75 ++++++++++++ .../suite/tidesdb/t/tidesdb_drop_create.opt | 2 + .../suite/tidesdb/t/tidesdb_drop_create.test | 74 ++++++++++++ .../suite/tidesdb/t/tidesdb_fk_convert.opt | 2 + .../suite/tidesdb/t/tidesdb_fk_convert.test | 69 +++++++++++ .../suite/tidesdb/t/tidesdb_online_ddl.test | 19 ++++ mysql-test/suite/tidesdb/t/tidesdb_ttl.test | 92 +++++++++++++++ tidesdb/ha_tidesdb.cc | 107 ++++++++++++++++-- tidesdb/ha_tidesdb.h | 5 +- 19 files changed, 862 insertions(+), 20 deletions(-) create mode 100644 mysql-test/suite/tidesdb/r/tidesdb_alter_crash.result create mode 100644 mysql-test/suite/tidesdb/r/tidesdb_consistent_snapshot.result create mode 100644 mysql-test/suite/tidesdb/r/tidesdb_drop_create.result create mode 100644 mysql-test/suite/tidesdb/r/tidesdb_fk_convert.result create mode 100644 mysql-test/suite/tidesdb/t/tidesdb_alter_crash.opt create mode 100644 mysql-test/suite/tidesdb/t/tidesdb_alter_crash.test create mode 100644 mysql-test/suite/tidesdb/t/tidesdb_consistent_snapshot.opt create mode 100644 mysql-test/suite/tidesdb/t/tidesdb_consistent_snapshot.test create mode 100644 mysql-test/suite/tidesdb/t/tidesdb_drop_create.opt create mode 100644 mysql-test/suite/tidesdb/t/tidesdb_drop_create.test create mode 100644 mysql-test/suite/tidesdb/t/tidesdb_fk_convert.opt create mode 100644 mysql-test/suite/tidesdb/t/tidesdb_fk_convert.test diff --git a/README b/README index 8de5ffe..0672c88 100644 --- a/README +++ b/README @@ -24,7 +24,7 @@ themselves, though with the install script you can modify configuration files after the fact. LINUX (Ubuntu/Debian) ---------------------- +░░░░░░░░░░░░░░░░░░░░░░░░ 1. Install dependencies: @@ -63,7 +63,7 @@ LINUX (Ubuntu/Debian) MACOS ------ +░░░░░░░░░░░░░░░░░░░░░░░░ 1. Install dependencies: @@ -121,7 +121,7 @@ MACOS WINDOWS -------- +░░░░░░░░░░░░░░░░░░░░░░░░ 1. Install prerequisites: @@ -173,7 +173,7 @@ WINDOWS ENABLE PLUGIN -------------- +░░░░░░░░░░░░░░░░░░░░░░░░ After building, enable the plugin in MariaDB: @@ -215,7 +215,7 @@ TidesDB stores its data as a sibling of the MariaDB data directory: SYSTEM VARIABLES (SET GLOBAL tidesdb_...) ------------------------------------------ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ All are read-only (set at startup) unless noted otherwise. @@ -234,7 +234,7 @@ reduce log volume. TABLE OPTIONS (CREATE TABLE ... ENGINE=TidesDB