A MySQL 9.7 storage engine plugin that uses TidesDB — an LSM-tree key/value engine — as the row store. Builds as a loadable ha_tidesdb.so; install at runtime with INSTALL PLUGIN.
Status: experimental / proof-of-concept. Single-node CRUD works, type coverage is wide, and the MTR suite is fully green — 58/58 executed tests pass with 2 cleanly skipped (each marked with the specific feature gap that prevents it: native partitioning, MySQL vector API). See Status for the full breakdown.
This is a port of TideSQL (TidesDB + MariaDB) to MySQL 9.7. Despite the family resemblance, MariaDB and MySQL have diverged enough at the handler API and data-dictionary layers that this is a rewrite-by-replay rather than a drop-in.
If you already run mysql-server from your distro's package manager, you can install the plugin as a sibling package and enable the engine without rebuilding mysqld.
Debian / Ubuntu (mysql-server-9.7+):
sudo dpkg -i tidesdb-mysql-plugin_<version>_amd64.deb
# Enable on every restart:
sudo cp /usr/share/doc/tidesdb-mysql-plugin/tidesdb.cnf.example \
/etc/mysql/conf.d/tidesdb.cnf
sudo systemctl restart mysql
mysql -uroot -e "SHOW ENGINES;" | grep -i tidesdbRHEL / Oracle Linux / Rocky 9 (mysql-server-9.7+):
sudo dnf install ./tidesdb-mysql-plugin-<version>-1.x86_64.rpm
sudo cp /usr/share/doc/tidesdb-mysql-plugin/tidesdb.cnf.example \
/etc/my.cnf.d/tidesdb.cnf
sudo systemctl restart mysqld
mysql -uroot -e "SHOW ENGINES;" | grep -i tidesdbOr load the plugin once at runtime instead of at every restart:
INSTALL PLUGIN tidesdb SONAME 'ha_tidesdb.so';Build packages from source:
./scripts/package-deb.sh 0.2.0 # writes dist/tidesdb-mysql-plugin_0.2.0_amd64.deb
./scripts/package-rpm.sh 0.2.0 # writes dist/tidesdb-mysql-plugin-0.2.0-1.x86_64.rpmBoth produce a binary package containing ha_tidesdb.so plus docs and an example config snippet. The .deb is built on Ubuntu 24.04 and the .rpm on Oracle Linux 9 (matching the official mysql:9.7 image's libstdc++ ABI).
The fastest path: pull the published image — no build needed.
docker pull evgeniypatlan/test-images:mysql-9.7-tidesdb-v0.2.0
docker run -d --name tidesdb \
-e MYSQL_ROOT_PASSWORD=secret \
-p 3306:3306 \
evgeniypatlan/test-images:mysql-9.7-tidesdb-v0.2.0
# Verify the engine is available.
docker exec tidesdb mysql -uroot -psecret -e "SHOW ENGINES;" | grep -i tidesdb
# Expected: TIDESDB YES TidesDB ...
# Connect and try a few rows.
docker exec -it tidesdb mysql -uroot -psecret -e "
CREATE DATABASE demo;
CREATE TABLE demo.kv (k INT PRIMARY KEY, v VARCHAR(32)) ENGINE=TIDESDB;
INSERT INTO demo.kv VALUES (1, 'hello'), (2, 'world');
SELECT * FROM demo.kv;
"Tags published to Docker Hub:
evgeniypatlan/test-images:mysql-9.7-tidesdb-v0.2.0— pinned to the v0.2.0 release (recommended for reproducibility; see the release notes)evgeniypatlan/test-images:mysql-9.7-tidesdb-latest— moves forward with each release
docker compose -f docker/runtime/docker-compose.yml up -d works too if you'd rather use compose.
If you'd rather verify the build end-to-end, run a custom branch, or you can't reach Docker Hub:
git clone https://github.com/EvgeniyPatlan/tidesdb-mysql.git
cd tidesdb-mysql
# Cold: ~25-40 min (clones MySQL source, compiles the plugin on Oracle
# Linux 9 to match the official mysql:9.7 image's libstdc++ ABI).
docker build -f docker/Dockerfile.mysql -t tidesdb/mysql:9.7 .
# Run as above but with the local tag:
docker run -d --name tidesdb -e MYSQL_ROOT_PASSWORD=secret -p 3306:3306 tidesdb/mysql:9.7The repo ships a smoke test that spins up a container, asserts the engine loads, exercises CRUD + composite PK + composite UNIQUE, restarts the container and verifies data persisted. It is the same script CI runs on every push.
# Test the published image:
./docker/runtime/smoke-test.sh evgeniypatlan/test-images:mysql-9.7-tidesdb-v0.2.0
# Test a locally-built image:
./docker/runtime/smoke-test.sh tidesdb/mysql:9.7
# Default (no argument) uses tidesdb/mysql:9.7 — the local-build tag.
./docker/runtime/smoke-test.sh
# Override the temp container name / root password if you have a conflict:
CONTAINER=tdb-test PASSWORD=hunter2 \
./docker/runtime/smoke-test.sh evgeniypatlan/test-images:mysql-9.7-tidesdb-v0.2.0The script creates a throwaway container named tidesdb-smoke and docker rm -fs it on exit (including on failure), so it never pollutes your docker ps and is safe to re-run.
Expected output ends with:
[smoke] all checks passed for tidesdb/mysql:9.7
If a step fails, the script prints FAIL: <what> plus the last 30 lines of docker logs for the container so you can see what mysqld said.
If the smoke test passes and you want to run your own SQL against the same image (e.g. to reproduce a bug):
# Start a clean container in the background:
docker run -d --name tdb-play -e MYSQL_ROOT_PASSWORD=secret tidesdb/mysql:9.7
# Wait for it to be ready (mysql:9.7 entrypoint does a temp-mysqld bootstrap pass first):
until docker exec tdb-play mysql -uroot -psecret -e 'SELECT 1' >/dev/null 2>&1; do sleep 2; done
sleep 5 # let the entrypoint finish its restart
# Run a SQL file from your host:
docker exec -i tdb-play mysql -uroot -psecret < my_test.sql
# Or pipe inline SQL:
docker exec -i tdb-play mysql -uroot -psecret <<'SQL'
CREATE DATABASE t;
CREATE TABLE t.kv (k INT PRIMARY KEY, v VARCHAR(32)) ENGINE=TIDESDB;
INSERT INTO t.kv VALUES (1,'hello');
SELECT * FROM t.kv;
SQL
# Tear down:
docker rm -f tdb-playThe phase2 SQL fixtures under tests/phase2/ are valid input to mysql and can be replayed this way (skip the --source include/... lines — those are MTR-specific):
docker exec -i tdb-play mysql -uroot -psecret < tests/phase2/05_insert_one_row.sql./scripts/test-plugin.sh and the MTR suite (./mtr ...) need the full MySQL build tree (vendor/mysql-server/build/) — they invoke mysqld directly with custom data dirs and tooling that don't exist inside the runtime image. To run those, build from source (see below).
If you want to develop the plugin (run tests, edit code, rebuild fast):
./scripts/build-all.sh # ~30 min cold (clones MySQL, builds plugin)
./scripts/test-plugin.sh # 37/37 hand-rolled testsbuild-all.sh runs three steps:
setup-workspace.sh— clonesmysql-server@mysql-9.7.0andtidesdb@v9.3.2intovendor/, then installsplugin/andmysql-test-suite/into the MySQL tree.build-tidesdb.sh— buildslibtidesdb.a(Debug, with symbols) intovendor/tidesdb-prefix/.build-plugin.sh— configures MySQL withWITH_TIDESDB_STORAGE_ENGINE=DYNAMIC, buildsha_tidesdb.so.
Output: vendor/mysql-server/build/plugin_output_directory/ha_tidesdb.so.
-- One-time, after copying ha_tidesdb.so to mysqld's plugin_dir:
INSTALL PLUGIN tidesdb SONAME 'ha_tidesdb.so';
-- Use it:
CREATE TABLE t (id INT PRIMARY KEY, val VARCHAR(64)) ENGINE=TIDESDB;
INSERT INTO t VALUES (1, 'hello'), (2, 'world');
SELECT * FROM t WHERE id = 1;Per-table options are passed through ENGINE_ATTRIBUTE (JSON):
CREATE TABLE t (id INT PRIMARY KEY, v BLOB) ENGINE=TIDESDB
ENGINE_ATTRIBUTE='{"compression":"LZ4","bloom_filter":true}';Server-level system variables: tidesdb_flush_threads, tidesdb_compaction_threads, tidesdb_block_cache_size, tidesdb_max_open_sstables, tidesdb_unified_memtable_write_buffer_size, tidesdb_unified_memtable_sync_mode, tidesdb_default_write_buffer_size, tidesdb_default_sync_mode, tidesdb_default_compression, tidesdb_log_level. See docs/build-and-load.md for the full reference.
| Test layer | Result |
|---|---|
test-plugin.sh (hand-rolled CRUD, 37 cases) |
37/37 ✓ |
test-persistence.sh (cross-restart durability) |
PASS |
smoke-test.sh (end-to-end pipeline) |
all phases ✓ |
| MTR suite (lifted from TideSQL + post-review additions) | 58/58 executed pass, 2 skipped |
What works:
- Types:
INT,BIGINT,SMALLINT,TINYINT,VARCHAR,CHAR,TEXT,BLOB,DATE,DATETIME,TIMESTAMP,DECIMAL,FLOAT,DOUBLE,ENUM,JSON - Primary keys: single-column AND composite (mixed types, including
AUTO_INCREMENTas a non-leading column —PRIMARY KEY (tenant, id)withid AUTO_INCworks) INSERT,SELECT … WHERE pk = ?,UPDATE,DELETE, full table scans, range scans (PK + secondary)AUTO_INCREMENT,TRUNCATE TABLE,REPLACE INTO,INSERT … ON DUPLICATE KEY UPDATE- Secondary indexes (single-column AND composite) with ICP;
UNIQUE KEY (a, b)enforced even on tables withAUTO_INCREMENTPK - Online DDL:
ALTER TABLE … ADD/DROP INDEX, ALGORITHM=INPLACE(concurrent reads OK; writes blocked while index populates) ALGORITHM=INSTANT ADD COLUMNwith NULL or NOT NULL DEFAULT — existing rows back-fill fromdefault_valueson read, no row rewriteALGORITHM=INSTANT DROP COLUMNwhen the dropped column is at the end of the table — no row rewrite. Mid-column drops fall back toALGORITHM=COPY(still correct, just slower).- At-rest encryption (AES-256-CBC) via a master-key file pointed to by
--tidesdb-master-key-file=<path>(32 bytes raw key); per-table opt-in withENGINE_ATTRIBUTE='{"encrypted":true}' - Pessimistic row locking (opt-in via
--tidesdb-pessimistic-locking=ON) — InnoDB-style row-level locks for write workloads; engine-level OCC is automatically downgraded to TidesDBREAD_COMMITTEDso the SQL-layer lock manager handles serialization without false-positive commit conflicts. - 2-phase commit via
preparehook →ER_LOCK_DEADLOCKsurfaces cleanly to user code - Cross-restart persistence
ALTER TABLE x ENGINE=TIDESDB(engine conversion from InnoDB)- Per-table compression (
NONE | SNAPPY | LZ4 | ZSTD | LZ4_FAST) viaENGINE_ATTRIBUTE - Bloom filters via
ENGINE_ATTRIBUTE - Per-row TTL, server-level TidesDB tuning system variables, online backup / checkpoint
- Mixed-engine transactions (TidesDB + InnoDB in the same
BEGIN…COMMIT) - Full-text search, spatial / R-tree indexes, generated columns
Skipped MTR tests (specific feature gaps, each documented inline):
tidesdb_partition— native partitioning (HA_HAS_OWN_PARTITIONING + helper virtuals) not implemented; MySQL 8+ removed the legacyha_partitionshim.tidesdb_vector— uses MariaDB'sVECTOR(N)column type /VECTOR INDEXDDL; needs rewrite against MySQL 9 vector functions.
Not yet, but in-scope for follow-up work:
ALGORITHM=INSTANT DROP COLUMNfor non-trailing columns — currently falls back toALGORITHM=COPYfor middle-of-table drops. Lifting this requires per-row schema versioning (à la InnoDB's INSTANT DROP) so old rows can be remapped to the new layout. Trailing drops already work as INSTANT.- Atomic DDL via SDI — handlerton callbacks are registered but not exercised end-to-end; restart-safe DDL is the next big follow-up.
- Replication, FK constraints, savepoint nesting — not in scope yet.
plugin/ MySQL handler source (ha_tidesdb.{cc,h}, tidesdb_compat.h, CMakeLists.txt)
mysql-test-suite/ MTR suite — 61 tests, lifted from TideSQL and rebased onto ENGINE_ATTRIBUTE
tests/phase2/ Hand-rolled .sql / .expected pairs run by scripts/test-plugin.sh
scripts/ Build, run, and replay-port-edit scripts (Bash)
docker/ Builder image (Ubuntu 24.04 + MySQL/TidesDB build deps) and the runnable mysql:9.7 + TidesDB image
docs/ Reference docs (build & runtime config, test layers)
packaging/ .deb and .rpm templates + helpers
vendor/ Cloned upstreams (mysql-server, tidesdb, optional tidesql) — gitignored
vendor/ is populated by scripts/setup-workspace.sh. Nothing inside it is checked into this repo.
scripts/replay-port-edits.sh is a deterministic, idempotent script that takes the upstream TideSQL MariaDB source and applies every MariaDB→MySQL edit needed for MySQL 9.7. It's there both as documentation (each edit shows the diff in pattern→replacement form) and as a re-baseline tool when TideSQL upstream changes. Run with WITH_TIDESQL_REFERENCE=1 ./scripts/setup-workspace.sh to clone TideSQL alongside, then ./scripts/replay-port-edits.sh to regenerate plugin/ha_tidesdb.cc.
The big port hot-spots:
Field::sort_string()(MariaDB) →Field::make_sort_key()(MySQL) — load-bearing for primary-key encoding.- Handler virtuals
open(),create(),delete_table(),rename_table()all gain a trailingdd::Table*parameter in MySQL. - Per-table options grammar (MariaDB
ha_create_table_option[]) → JSONENGINE_ATTRIBUTEparsed with rapidjson. HTON_SUPPORTS_ENGINE_ATTRIBUTE,HA_GENERATED_COLUMNS, atomic-DDL flags must be set explicitly.INSTALL SONAME 'ha_tidesdb';(MariaDB) →INSTALL PLUGIN tidesdb SONAME 'ha_tidesdb.so';(MySQL).
scripts/replay-port-edits.sh encodes each of these MariaDB→MySQL edits as a deterministic pattern→replacement, so the port can be rebased against a future TideSQL upstream by re-running the script.
docs/build-and-load.md— toolchain and runtime config (system variables,ENGINE_ATTRIBUTE)docs/testing.md— test layers and how to run each
GPLv2, matching MySQL Server and the upstream TideSQL plugin. See LICENSE.
- TidesDB — the underlying LSM engine.
- TideSQL — the MariaDB integration this port is derived from. The handler scaffolding, system variables, and
ENGINE_ATTRIBUTEJSON design all trace back to TideSQL. - MySQL Server — the host server.