diff --git a/.clang-format b/.clang-format index 55db5bd2..d7dd7266 100644 --- a/.clang-format +++ b/.clang-format @@ -6,8 +6,16 @@ KeepEmptyLinesAtTheStartOfBlocks: false AlignConsecutiveMacros: true BreakBeforeBraces: Allman SortIncludes: true +# The plugin's own headers are not self-contained; they use server types (KEY, +# uchar, uint, MAX_KEY_LENGTH) that ha_tidesdb.h brings in, so ha_tidesdb.h must +# be included first in every translation unit. Disable clang-format's +# main-header-first rule (which would hoist ha_tidesdb_.h above ha_tidesdb.h) +# and pin my_global.h then ha_tidesdb.h ahead of every other include. +IncludeIsMainRegex: '$^' IncludeCategories: - Regex: '^"my_global\.h"' + Priority: -2 + - Regex: '^"ha_tidesdb\.h"' Priority: -1 - Regex: '^' Priority: 2 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index af1082cf..00000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,138 +0,0 @@ -name: Docker - -on: - workflow_dispatch: - push: - branches: [main, master] - paths: - - 'docker/**' - - 'tidesdb/**' - - 'mysql-test/**' - - '.github/workflows/docker.yml' - pull_request: - branches: [main, master] - paths: - - 'docker/**' - - 'tidesdb/**' - - 'mysql-test/**' - - '.github/workflows/docker.yml' - -env: - IMAGE_NAME: tidesql-ci - -jobs: - docker-build-and-run: - name: "Build & verify Docker image" - runs-on: ubuntu-latest - timeout-minutes: 90 - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Resolve TidesDB version - id: versions - run: | - VERSION=$(curl -fsSL https://api.github.com/repos/tidesdb/tidesdb/releases/latest \ - | grep '"tag_name":' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/') - if [ -z "$VERSION" ]; then - echo "Failed to fetch latest TidesDB version" >&2 - exit 1 - fi - echo "tidesdb=${VERSION}" >> "$GITHUB_OUTPUT" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build Docker image - uses: docker/build-push-action@v6 - with: - context: . - file: docker/ubuntu/Dockerfile - push: false - load: true - tags: "${{ env.IMAGE_NAME }}:ci" - build-args: | - MARIADB_VERSION=11.8 - TIDESDB_VERSION=${{ steps.versions.outputs.tidesdb }} - DISABLED_ENGINES=MROONGA,ROCKSDB,CONNECT,SPIDER,OQGRAPH,COLUMNSTORE - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Start container - run: | - docker run -d \ - --name tidesql-ci \ - --tmpfs /tmp:exec \ - "${{ env.IMAGE_NAME }}:ci" - - echo "Waiting for MariaDB to start..." - for i in $(seq 1 60); do - if docker exec tidesql-ci mariadb -u root -e "SELECT 1" &>/dev/null; then - echo "MariaDB ready (${i}s)" - break - fi - if [ "$i" = "60" ]; then - echo "MariaDB failed to start within 60s" - docker logs tidesql-ci - exit 1 - fi - sleep 1 - done - - - name: Verify TidesDB plugin is loaded - run: | - docker exec tidesql-ci mariadb -u root -e " - SELECT PLUGIN_NAME, PLUGIN_STATUS, PLUGIN_TYPE - FROM INFORMATION_SCHEMA.PLUGINS - WHERE PLUGIN_NAME = 'TidesDB'; - " - docker exec tidesql-ci mariadb -u root -BN -e " - SELECT PLUGIN_STATUS - FROM INFORMATION_SCHEMA.PLUGINS - WHERE PLUGIN_NAME = 'TidesDB'; - " | grep -q ACTIVE - - - name: Smoke test - run: | - docker exec tidesql-ci mariadb -u root -e " - CREATE DATABASE IF NOT EXISTS ci_test; - USE ci_test; - - CREATE TABLE t ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) - ) ENGINE=TidesDB; - - INSERT INTO t (name) VALUES ('alice'), ('bob'), ('charlie'); - SELECT * FROM t ORDER BY id; - - START TRANSACTION; - INSERT INTO t (name) VALUES ('should_not_exist'); - ROLLBACK; - - SELECT COUNT(*) AS expect_3 FROM t; - - DROP TABLE t; - DROP DATABASE ci_test; - " - - - name: SHOW ENGINE TIDESDB STATUS - run: | - docker exec tidesql-ci mariadb -u root -e "SHOW ENGINE TIDESDB STATUS\G" - - - name: Collect logs on failure - if: failure() - run: | - echo "=== Container logs ===" - docker logs tidesql-ci 2>&1 | tail -200 - - echo "" - echo "=== MariaDB error log ===" - docker exec tidesql-ci cat /usr/local/mariadb/log/error.log 2>/dev/null \ - | tail -200 || true - - - name: Cleanup - if: always() - run: | - docker rm -f tidesql-ci 2>/dev/null || true diff --git a/.github/workflows/ha_replication.yml b/.github/workflows/ha_replication.yml deleted file mode 100644 index fd95c704..00000000 --- a/.github/workflows/ha_replication.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: HA Replication (SQL failover, FS backend) - -# SQL-level single-writer failover for TideSQL, mirroring the tidesdb library's -# failover suite one layer up: real mariadbd nodes sharing an FS object-store -# bucket, driven over SQL. Uses the FS backend (no S3/MinIO), so it is fast and -# dependency-free -- the analogue of the library's failover_local tier. -# -# Building MariaDB + the plugin is heavy, so this runs nightly + on demand + on -# PRs that touch the plugin or the harness. A real-MinIO S3 tier (the analogue of -# the library's failover_k8s) runs separately in ha_replication_k8s.yml. -on: - workflow_dispatch: - schedule: - - cron: "0 5 * * *" - pull_request: - branches: [ "master" ] - paths: - - "tidesdb/**" - - "test/ha_replication/**" - - ".github/workflows/ha_replication.yml" - -jobs: - ha-replication: - runs-on: ubuntu-latest - name: SQL failover (FS object store) - steps: - - name: Checkout TideSQL - uses: actions/checkout@v4 - with: - path: tidesql - - - name: Install build dependencies - run: | - sudo apt-get update - sudo apt-get install -y build-essential cmake ninja-build bison libssl-dev \ - libjemalloc2 libjemalloc-dev libncurses-dev pkg-config - - - name: Build TidesDB library + MariaDB + plugin - working-directory: tidesql - run: | - # default build: FS object-store backend is always compiled in (no S3 needed). - ./install.sh --mariadb-prefix "$GITHUB_WORKSPACE/mariadb" \ - --build-dir "$GITHUB_WORKSPACE/build" --jobs "$(nproc)" - - - name: Run SQL failover scenarios (FS backend) - working-directory: tidesql - env: - TIDESQL_PREFIX: ${{ github.workspace }}/mariadb - TDB_HA_JEMALLOC: /usr/lib/x86_64-linux-gnu/libjemalloc.so.2 - run: | - chmod +x test/ha_replication/run_local.sh - ./test/ha_replication/run_local.sh - - - name: Node logs on failure - if: failure() - working-directory: tidesql - run: | - for f in test/ha_replication/.scratch/n*/err.log; do - echo "===== $f ====="; tail -60 "$f" || true - done diff --git a/.github/workflows/ha_replication_k8s.yml b/.github/workflows/ha_replication_k8s.yml deleted file mode 100644 index bb455fc4..00000000 --- a/.github/workflows/ha_replication_k8s.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: HA Replication (k8s SQL failover, kind + MinIO) - -# Cloud-simulation SQL failover: real mariadbd pods on a kind cluster fencing -# against a real MinIO bucket over the TidesDB S3 connector's conditional writes -- -# the analogue of the tidesdb library's failover_k8s, one layer up. Heavier than -# the FS-backend ha_replication run (it builds a MariaDB+plugin node image), so it -# is nightly + manual + on PRs that touch the plugin or the harness. -on: - workflow_dispatch: - schedule: - - cron: "0 6 * * *" - pull_request: - branches: [ "master" ] - paths: - - "tidesdb/**" - - "test/ha_replication/**" - - ".github/workflows/ha_replication_k8s.yml" - -jobs: - ha-replication-k8s: - runs-on: ubuntu-latest - name: SQL failover (kind + MinIO) - steps: - - uses: actions/checkout@v4 - - - name: Free up disk for the MariaDB build - run: | - sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /opt/hostedtoolcache/CodeQL || true - docker system prune -af || true - - - name: Create kind cluster - uses: helm/kind-action@v1.10.0 - with: - cluster_name: tidesql-failover - - - name: Build node image (MariaDB + plugin + S3 libtidesdb) - run: docker build -f test/ha_replication/Dockerfile -t tidesql-node:ci . - - - name: Load image into kind - run: kind load docker-image tidesql-node:ci --name tidesql-failover - - - name: Run k8s failover scenarios - run: | - chmod +x test/ha_replication/run_k8s.sh - ./test/ha_replication/run_k8s.sh - - - name: Diagnostics on failure - if: failure() - run: | - kubectl get pods -o wide || true - kubectl describe pods -l app=tidesql-node || true - for p in tidesql-node-0 tidesql-node-1 tidesql-node-2; do - echo "=== logs $p ==="; kubectl logs "$p" --tail=80 || true - done - kubectl logs deploy/minio --tail=40 || true diff --git a/.github/workflows/k8s_integration_test.yml b/.github/workflows/k8s_integration_test.yml deleted file mode 100644 index ec7f1f4a..00000000 --- a/.github/workflows/k8s_integration_test.yml +++ /dev/null @@ -1,1021 +0,0 @@ -name: Kubernetes Integration Tests (kind) - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - workflow_dispatch: - -jobs: - k8s-test: - runs-on: ubuntu-latest - name: K8s Primary/Replica/Failover Integration - timeout-minutes: 30 - - steps: - - name: Checkout TideSQL - uses: actions/checkout@v4 - - - name: Install kind - run: | - curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.25.0/kind-linux-amd64 - chmod +x ./kind - sudo mv ./kind /usr/local/bin/kind - kind version - - - name: Create kind cluster - run: | - kind create cluster --name tidesql-test --wait 60s - kubectl cluster-info --context kind-tidesql-test - kubectl get nodes - - - name: Resolve TidesDB version - id: tidesdb-version - run: | - # Resolve latest TidesDB release tag from GitHub - TAG=$(curl -sf https://api.github.com/repos/tidesdb/tidesdb/releases/latest | grep '"tag_name"' | head -1 | cut -d'"' -f4) - if [ -z "$TAG" ]; then - echo "Failed to resolve latest TidesDB release" - exit 1 - fi - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "TidesDB version: $TAG" - - - name: Build TideSQL Docker image - run: | - cd docker - docker build -f ubuntu/Dockerfile \ - --build-arg MARIADB_VERSION=mariadb-11.8.6 \ - --build-arg TIDESDB_VERSION=${{ steps.tidesdb-version.outputs.tag }} \ - --build-arg WITH_TESTS=0 \ - --build-arg WITH_S3=1 \ - --build-arg DISABLED_ENGINES="ROCKSDB,MROONGA,SPIDER,CONNECT,OQGRAPH,COLUMNSTORE,SPHINX" \ - -t tidesdb/tidesql:test \ - .. - timeout-minutes: 30 - - - name: Load image into kind - run: kind load docker-image tidesdb/tidesql:test --name tidesql-test - - - name: Deploy MinIO - run: | - kubectl create namespace tidesql - - # Deploy MinIO as a simple pod + service - kubectl apply -n tidesql -f - <<'EOF' - apiVersion: v1 - kind: Pod - metadata: - name: minio - labels: - app: minio - spec: - containers: - - name: minio - image: minio/minio:latest - command: ["minio", "server", "/data"] - env: - - name: MINIO_ROOT_USER - value: minioadmin - - name: MINIO_ROOT_PASSWORD - value: minioadmin - ports: - - containerPort: 9000 - --- - apiVersion: v1 - kind: Service - metadata: - name: minio - spec: - selector: - app: minio - ports: - - port: 9000 - targetPort: 9000 - EOF - - echo "Waiting for MinIO..." - kubectl wait -n tidesql --for=condition=Ready pod/minio --timeout=60s - - - name: Create S3 bucket - run: | - kubectl exec -n tidesql minio -- \ - sh -c 'mkdir -p /data/tidesql-test' - echo "Bucket created" - - - name: Deploy TideSQL primary - run: | - # Create secret and configmap for MinIO - kubectl apply -n tidesql -f - <<'EOF' - apiVersion: v1 - kind: Secret - metadata: - name: tidesdb-s3-credentials - type: Opaque - stringData: - access-key: "minioadmin" - secret-key: "minioadmin" - --- - apiVersion: v1 - kind: ConfigMap - metadata: - name: tidesdb-primary-config - data: - tidesdb.cnf: | - [mariadb] - plugin-load-add=ha_tidesdb.so - - tidesdb_object_store_backend=S3 - tidesdb_s3_endpoint=minio.tidesql.svc.cluster.local:9000 - tidesdb_s3_bucket=tidesql-test - tidesdb_s3_access_key=minioadmin - tidesdb_s3_secret_key=minioadmin - tidesdb_s3_use_ssl=OFF - tidesdb_s3_path_style=ON - tidesdb_objstore_wal_sync_on_commit=ON - - tidesdb_unified_memtable=ON - tidesdb_unified_memtable_sync_mode=FULL - tidesdb_unified_memtable_write_buffer_size=32000000 - tidesdb_block_cache_size=64M - tidesdb_flush_threads=2 - tidesdb_compaction_threads=2 - tidesdb_log_level=DEBUG - --- - apiVersion: v1 - kind: Pod - metadata: - name: tidesql-primary - labels: - app: tidesql - role: primary - spec: - containers: - - name: mariadb - image: tidesdb/tidesql:test - command: ["/bin/bash", "-c"] - args: - - | - mkdir -p /etc/mysql/custom && cp /opt/tidesdb-config/tidesdb.cnf /etc/mysql/custom/tidesdb-k8s.cnf && echo "[k8s] TidesDB S3 config injected" || echo "[k8s] WARNING: config copy failed" - exec /usr/local/bin/tidesql-entrypoint.sh - ports: - - containerPort: 3306 - volumeMounts: - - name: config - mountPath: /opt/tidesdb-config/tidesdb.cnf - subPath: tidesdb.cnf - readinessProbe: - exec: - command: ["mariadb-admin", "ping", "-h", "localhost"] - initialDelaySeconds: 10 - periodSeconds: 5 - volumes: - - name: config - configMap: - name: tidesdb-primary-config - --- - apiVersion: v1 - kind: Service - metadata: - name: tidesql-primary - spec: - selector: - app: tidesql - role: primary - ports: - - port: 3306 - targetPort: 3306 - EOF - - echo "Waiting for primary..." - kubectl wait -n tidesql --for=condition=Ready pod/tidesql-primary --timeout=120s - - - name: Create monitor user for remote access - run: | - # MariaDB root is unix_socket only (localhost). Create a user that - # the failover controller (a separate pod) can use over TCP. - kubectl exec -n tidesql tidesql-primary -- \ - mariadb -e " - CREATE USER IF NOT EXISTS 'monitor'@'%' IDENTIFIED BY 'M0nitor!Pass9'; - GRANT ALL PRIVILEGES ON *.* TO 'monitor'@'%' WITH GRANT OPTION; - FLUSH PRIVILEGES; - " - echo "Monitor user created on primary" - - - name: Verify S3 config is loaded - run: | - echo "=== Config file injected into primary ===" - kubectl exec -n tidesql tidesql-primary -- \ - cat /etc/mysql/custom/tidesdb-k8s.cnf 2>/dev/null || echo "(file missing!)" - - echo "" - echo "=== S3 system variables ===" - kubectl exec -n tidesql tidesql-primary -- \ - mariadb -e "SHOW VARIABLES LIKE 'tidesdb_object_store%'; SHOW VARIABLES LIKE 'tidesdb_s3%'; SHOW VARIABLES LIKE 'tidesdb_objstore%'; SHOW VARIABLES LIKE 'tidesdb_replica%';" - - echo "" - echo "=== TidesDB engine status ===" - kubectl exec -n tidesql tidesql-primary -- \ - mariadb -e "SHOW ENGINE TIDESDB STATUS\G" | head -30 - - echo "" - echo "=== MariaDB error log (last 20 lines) ===" - kubectl exec -n tidesql tidesql-primary -- \ - sh -c 'tail -20 /usr/local/mariadb/log/error.log 2>/dev/null || tail -20 /usr/local/mariadb/data/default/*.err 2>/dev/null' || echo "(no error log found)" - - - name: Write test data to primary - run: | - # Write enough data to exceed the 32MB unified memtable write buffer - # and trigger a flush to SSTables which then get uploaded to S3 - kubectl exec -n tidesql tidesql-primary -- \ - mariadb -e " - CREATE DATABASE k8s_test; - USE k8s_test; - CREATE TABLE items ( - id INT NOT NULL PRIMARY KEY, - name VARCHAR(100), - data TEXT - ) ENGINE=TidesDB; - INSERT INTO items VALUES (1, 'alpha', REPEAT('A', 10000)); - INSERT INTO items VALUES (2, 'beta', REPEAT('B', 10000)); - INSERT INTO items VALUES (3, 'gamma', REPEAT('C', 10000)); - " - # Insert bulk data to exceed the 32MB flush threshold - for i in $(seq 4 200); do - kubectl exec -n tidesql tidesql-primary -- \ - mariadb -e "INSERT INTO k8s_test.items VALUES ($i, 'bulk_$i', REPEAT('X', 10000))" 2>/dev/null - done - kubectl exec -n tidesql tidesql-primary -- \ - mariadb -e "SELECT COUNT(*) AS total_rows, SUM(LENGTH(data)) AS total_bytes FROM k8s_test.items" - - - name: Verify data on primary - run: | - RESULT=$(kubectl exec -n tidesql tidesql-primary -- \ - mariadb -N -e "SELECT COUNT(*) FROM k8s_test.items") - echo "Primary row count: $RESULT" - [ "$RESULT" -ge 3 ] || (echo "FAIL: expected at least 3 rows, got $RESULT"; exit 1) - - - name: Force flush to S3 - run: | - # OPTIMIZE triggers flush + compaction which should push SSTables to S3 - kubectl exec -n tidesql tidesql-primary -- \ - mariadb -e "OPTIMIZE TABLE k8s_test.items" || true - - # Give time for S3 uploads to complete - sleep 10 - - # Verify data actually reached MinIO - echo "=== MinIO bucket contents ===" - kubectl exec -n tidesql minio -- \ - ls -lR /data/tidesql-test/ 2>/dev/null | head -30 || true - - echo "=== Primary TidesDB data directory ===" - kubectl exec -n tidesql tidesql-primary -- \ - ls -lR /usr/local/mariadb/data/tidesdb_data/ 2>/dev/null | head -20 || true - - echo "=== Primary TidesDB LOG (last 30 lines) ===" - kubectl exec -n tidesql tidesql-primary -- \ - sh -c 'cat /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null || cat /usr/local/mariadb/tidesdb_data/LOG 2>/dev/null' | tail -30 || true - - - name: Deploy TideSQL replica - run: | - kubectl apply -n tidesql -f - <<'EOF' - apiVersion: v1 - kind: ConfigMap - metadata: - name: tidesdb-replica-config - data: - tidesdb.cnf: | - [mariadb] - plugin-load-add=ha_tidesdb.so - - tidesdb_object_store_backend=S3 - tidesdb_s3_endpoint=minio.tidesql.svc.cluster.local:9000 - tidesdb_s3_bucket=tidesql-test - tidesdb_s3_access_key=minioadmin - tidesdb_s3_secret_key=minioadmin - tidesdb_s3_use_ssl=OFF - tidesdb_s3_path_style=ON - - tidesdb_replica_mode=ON - tidesdb_replica_sync_interval=1000000 - - tidesdb_unified_memtable=ON - tidesdb_block_cache_size=64M - tidesdb_flush_threads=2 - tidesdb_compaction_threads=2 - tidesdb_log_level=INFO - --- - apiVersion: v1 - kind: Pod - metadata: - name: tidesql-replica - labels: - app: tidesql - role: replica - spec: - containers: - - name: mariadb - image: tidesdb/tidesql:test - command: ["/bin/bash", "-c"] - args: - - | - mkdir -p /etc/mysql/custom && cp /opt/tidesdb-config/tidesdb.cnf /etc/mysql/custom/tidesdb-k8s.cnf && echo "[k8s] TidesDB S3 config injected" || echo "[k8s] WARNING: config copy failed" - exec /usr/local/bin/tidesql-entrypoint.sh - ports: - - containerPort: 3306 - volumeMounts: - - name: config - mountPath: /opt/tidesdb-config/tidesdb.cnf - subPath: tidesdb.cnf - readinessProbe: - exec: - command: ["mariadb-admin", "ping", "-h", "localhost"] - initialDelaySeconds: 10 - periodSeconds: 5 - volumes: - - name: config - configMap: - name: tidesdb-replica-config - --- - apiVersion: v1 - kind: Service - metadata: - name: tidesql-replica - spec: - selector: - app: tidesql - role: replica - ports: - - port: 3306 - targetPort: 3306 - EOF - - echo "Waiting for replica..." - kubectl wait -n tidesql --for=condition=Ready pod/tidesql-replica --timeout=120s - - # Create monitor user on replica for failover controller access - kubectl exec -n tidesql tidesql-replica -- \ - mariadb -e " - CREATE USER IF NOT EXISTS 'monitor'@'%' IDENTIFIED BY 'M0nitor!Pass9'; - GRANT ALL PRIVILEGES ON *.* TO 'monitor'@'%' WITH GRANT OPTION; - FLUSH PRIVILEGES; - " - echo "Monitor user created on replica" - - - name: Wait for replica sync - run: | - echo "Waiting for replica to sync from S3..." - sleep 20 - - # Check if replica can even see databases - echo "=== Replica databases ===" - kubectl exec -n tidesql tidesql-replica -- \ - mariadb -e "SHOW DATABASES" 2>/dev/null || true - - echo "=== Replica TidesDB engine status ===" - kubectl exec -n tidesql tidesql-replica -- \ - mariadb -e "SHOW ENGINE TIDESDB STATUS\G" 2>/dev/null | head -20 || true - - # Retry a few times for sync - for i in $(seq 1 15); do - RESULT=$(kubectl exec -n tidesql tidesql-replica -- \ - mariadb -N -e "SELECT COUNT(*) FROM k8s_test.items" 2>/dev/null || echo "0") - echo "Replica sync check $i: $RESULT rows" - [ "$RESULT" -ge 3 ] && break - sleep 5 - done - - if [ "$RESULT" -lt 3 ]; then - echo "=== Replica TidesDB LOG ===" - kubectl exec -n tidesql tidesql-replica -- \ - sh -c 'cat /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null | tail -30' || true - echo "FAIL: replica did not sync (got $RESULT rows)" - exit 1 - fi - echo "Replica synced successfully" - - - name: Verify replica data matches primary - run: | - PRIMARY_DATA=$(kubectl exec -n tidesql tidesql-primary -- \ - mariadb -N -e "SELECT id, name FROM k8s_test.items ORDER BY id") - REPLICA_DATA=$(kubectl exec -n tidesql tidesql-replica -- \ - mariadb -N -e "SELECT id, name FROM k8s_test.items ORDER BY id") - - echo "Primary: $PRIMARY_DATA" - echo "Replica: $REPLICA_DATA" - - [ "$PRIMARY_DATA" = "$REPLICA_DATA" ] || \ - (echo "FAIL: data mismatch"; exit 1) - echo "Data matches" - - - name: Verify replica rejects writes - run: | - kubectl exec -n tidesql tidesql-replica -- \ - mariadb -e "INSERT INTO k8s_test.items VALUES (99999, 'should_fail', 'x')" 2>&1 \ - | grep -q 'ERROR\|error\|read.only' && echo "PASS: replica correctly rejects writes" \ - || echo "WARNING: replica did not reject write (may not have read-only enforcement)" - - - name: Test manual failover - kill primary and promote - run: | - echo "=== Manual failover test ===" - echo "Killing primary..." - kubectl delete pod -n tidesql tidesql-primary --grace-period=0 --force - sleep 5 - - echo "Promoting replica manually..." - kubectl exec -n tidesql tidesql-replica -- \ - mariadb -e "SET GLOBAL tidesdb_promote_primary = ON" 2>&1 || true - sleep 5 - - # Verify promoted replica accepts writes (use id that doesn't conflict) - kubectl exec -n tidesql tidesql-replica -- \ - mariadb -e "INSERT INTO k8s_test.items VALUES (10001, 'manual_failover', REPEAT('D', 500))" \ - 2>&1 || true - - RESULT=$(kubectl exec -n tidesql tidesql-replica -- \ - mariadb -N -e "SELECT COUNT(*) FROM k8s_test.items" 2>/dev/null || echo "0") - echo "Post-manual-failover row count: $RESULT" - [ "$RESULT" -ge 3 ] || (echo "FAIL: data lost after manual failover"; exit 1) - echo "Manual failover successful" - - - name: Restore primary for automated failover test - run: | - # The promoted replica is currently acting as primary against the - # shared MinIO bucket. We must fully drain and remove it before - # creating the new tidesql-primary -- otherwise both nodes upload - # concurrently to the same bucket and race on the schema CF MANIFEST. - # That race is what caused k8s_test.items to vanish on the new - # primary in earlier runs (split-brain on the object store). - echo "=== Draining promoted-replica before restore ===" - - # Flush so any post-promotion writes (the manual_failover INSERT) are - # in an SSTable, then graceful shutdown so the close path drains the - # async upload queue. 60s timeout because the unified-WAL drain over - # the object store can outlast a shorter window; if mariadb-admin - # exits via the timeout's SIGKILL the kubectl exec layer surfaces - # an "exit code 137" line which is benign but reads as a fault when - # scanning logs. Grep it out so the next reader is not misled. - kubectl exec -n tidesql tidesql-replica -- \ - mariadb -e "FLUSH LOGS; OPTIMIZE TABLE k8s_test.items;" 2>&1 || true - kubectl exec -n tidesql tidesql-replica -- \ - timeout 60 mariadb-admin shutdown 2>&1 \ - | grep -v "command terminated with exit code 137" || true - - # Prefer a graceful pod removal so any in-flight shutdown work has a - # chance to finish; fall back to --force only if the pod is still - # there after the grace period. - kubectl delete pod -n tidesql tidesql-replica --grace-period=30 2>&1 || true - if kubectl get pod -n tidesql tidesql-replica >/dev/null 2>&1; then - kubectl delete pod -n tidesql tidesql-replica --grace-period=0 --force 2>&1 || true - fi - # Block on the pod being fully removed before bringing up the new - # primary; condition=Ready alone doesn't give us this guarantee. - kubectl wait -n tidesql --for=delete pod/tidesql-replica --timeout=60s || true - echo "Promoted-replica drained and removed" - - # Now safe to attach a new primary to the quiet bucket. - kubectl apply -n tidesql -f - <<'EOF' - apiVersion: v1 - kind: Pod - metadata: - name: tidesql-primary - labels: - app: tidesql - role: primary - spec: - containers: - - name: mariadb - image: tidesdb/tidesql:test - command: ["/bin/bash", "-c"] - args: - - | - mkdir -p /etc/mysql/custom && cp /opt/tidesdb-config/tidesdb.cnf /etc/mysql/custom/tidesdb-k8s.cnf && echo "[k8s] TidesDB S3 config injected" || echo "[k8s] WARNING: config copy failed" - exec /usr/local/bin/tidesql-entrypoint.sh - ports: - - containerPort: 3306 - volumeMounts: - - name: config - mountPath: /opt/tidesdb-config/tidesdb.cnf - subPath: tidesdb.cnf - readinessProbe: - exec: - command: ["mariadb-admin", "ping", "-h", "localhost"] - initialDelaySeconds: 10 - periodSeconds: 5 - volumes: - - name: config - configMap: - name: tidesdb-primary-config - EOF - - kubectl wait -n tidesql --for=condition=Ready pod/tidesql-primary --timeout=120s - - # mariadb-admin ping returning OK does NOT mean TidesDB cold-start - # discovery has finished pulling k8s_test.items from S3. Poll for the - # table to be visible before treating the new primary as ready. - echo "Waiting for k8s_test.items to be visible on new primary..." - VISIBLE=0 - for i in $(seq 1 30); do - if kubectl exec -n tidesql tidesql-primary -- \ - mariadb -e "SELECT 1 FROM k8s_test.items LIMIT 1" >/dev/null 2>&1; then - echo " primary sees k8s_test.items after ${i}s" - VISIBLE=1 - break - fi - sleep 1 - done - if [ "$VISIBLE" -ne 1 ]; then - echo "FAIL: new primary never discovered k8s_test.items from S3" - kubectl exec -n tidesql tidesql-primary -- \ - mariadb -e "SHOW DATABASES" 2>&1 || true - kubectl exec -n tidesql tidesql-primary -- \ - sh -c 'cat /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null | tail -80' || true - exit 1 - fi - - # Create monitor user on new primary - kubectl exec -n tidesql tidesql-primary -- \ - mariadb -e " - CREATE USER IF NOT EXISTS 'monitor'@'%' IDENTIFIED BY 'M0nitor!Pass9'; - GRANT ALL PRIVILEGES ON *.* TO 'monitor'@'%' WITH GRANT OPTION; - FLUSH PRIVILEGES; - " - - # Bring up a fresh replica against the (now single-writer) bucket. - kubectl apply -n tidesql -f - <<'EOF' - apiVersion: v1 - kind: Pod - metadata: - name: tidesql-replica - labels: - app: tidesql - role: replica - spec: - containers: - - name: mariadb - image: tidesdb/tidesql:test - command: ["/bin/bash", "-c"] - args: - - | - mkdir -p /etc/mysql/custom && cp /opt/tidesdb-config/tidesdb.cnf /etc/mysql/custom/tidesdb-k8s.cnf && echo "[k8s] TidesDB S3 config injected" || echo "[k8s] WARNING: config copy failed" - exec /usr/local/bin/tidesql-entrypoint.sh - ports: - - containerPort: 3306 - volumeMounts: - - name: config - mountPath: /opt/tidesdb-config/tidesdb.cnf - subPath: tidesdb.cnf - readinessProbe: - exec: - command: ["mariadb-admin", "ping", "-h", "localhost"] - initialDelaySeconds: 10 - periodSeconds: 5 - volumes: - - name: config - configMap: - name: tidesdb-replica-config - EOF - - kubectl wait -n tidesql --for=condition=Ready pod/tidesql-replica --timeout=120s - - # Create monitor user on fresh replica - kubectl exec -n tidesql tidesql-replica -- \ - mariadb -e " - CREATE USER IF NOT EXISTS 'monitor'@'%' IDENTIFIED BY 'M0nitor!Pass9'; - GRANT ALL PRIVILEGES ON *.* TO 'monitor'@'%' WITH GRANT OPTION; - FLUSH PRIVILEGES; - " - - # Shared diagnostic block. Snapshots the world at a labelled - # checkpoint so failures can be localised to a specific moment - # rather than a "post-mortem after everything tore down" capture. - snapshot_state() { - local label="$1" - echo "" - echo "=== SNAPSHOT [$label] @ $(date -u '+%H:%M:%S') ===" - echo "=+=+= MinIO top-level =+=+=" - kubectl exec -n tidesql minio -- \ - ls -la /data/tidesql-test/ 2>&1 | head -20 || true - echo "=+=+= MinIO k8s_test__items =+=+=" - kubectl exec -n tidesql minio -- \ - ls -la /data/tidesql-test/k8s_test__items/ 2>&1 | head -40 || true - echo "=+=+= MinIO uwal_*.log presence =+=+=" - kubectl exec -n tidesql minio -- \ - ls -la /data/tidesql-test/ 2>&1 | grep -E 'uwal_' || echo "(no uwal segments)" - echo "=+=+= Primary TidesDB LOG (last 12 lines) =+=+=" - kubectl exec -n tidesql tidesql-primary -- \ - sh -c 'tail -12 /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null' \ - 2>&1 || echo "(primary unreachable)" - echo "=+=+= Replica TidesDB LOG (last 12 lines) =+=+=" - kubectl exec -n tidesql tidesql-replica -- \ - sh -c 'tail -12 /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null' \ - 2>&1 || echo "(replica unreachable)" - echo "=+=+= Counts =+=+=" - local pc=$(timeout 5 kubectl exec -n tidesql tidesql-primary -- \ - mariadb --connect-timeout=3 -N -e "SELECT COUNT(*) FROM k8s_test.items" 2>/dev/null || echo "?") - local rc=$(timeout 5 kubectl exec -n tidesql tidesql-replica -- \ - mariadb --connect-timeout=3 -N -e "SELECT COUNT(*) FROM k8s_test.items" 2>/dev/null || echo "?") - echo "primary COUNT(*) = $pc" - echo "replica COUNT(*) = $rc" - echo "=== END SNAPSHOT [$label] ===" - echo "" - } - # Expose helper to subsequent steps via /tmp - declare -f snapshot_state > /tmp/snapshot_state.sh - - snapshot_state "before-new-write" - - # Write fresh data to existing items table. No OPTIMIZE here -- - # OPTIMIZE forces a synchronous compaction whose new SSTables - # race with replica MANIFEST sync. Real failover never has an - # OPTIMIZE wedged between INSERT and failover; the natural flush - # path (or a FLUSH LOGS) is enough for the replica to pick up. - kubectl exec -n tidesql tidesql-primary -- \ - mariadb -e " - INSERT INTO k8s_test.items VALUES (10002, 'auto_failover_marker', REPEAT('F', 500)); - FLUSH LOGS; - " 2>&1 || true - - snapshot_state "after-insert-flush-logs" - - # Polled converge-or-fail. Drops the blind sleep 15 in favour of - # explicit visibility checks so the log shows the replica's - # trajectory rather than just the end-state. Each iteration - # snapshots a fingerprint (replica count + bucket file count) - # so we can see whether the system is making progress or stuck. - echo "=== Waiting for replica to converge on row 10002 ===" - REPLICA_OK=0 - for i in $(seq 1 60); do - RC=$(timeout 5 kubectl exec -n tidesql tidesql-replica -- \ - mariadb --connect-timeout=3 -N -e "SELECT COUNT(*) FROM k8s_test.items" \ - 2>/dev/null || echo "0") - BUCKET_FILES=$(kubectl exec -n tidesql minio -- \ - sh -c 'ls /data/tidesql-test/k8s_test__items/ 2>/dev/null | wc -l' \ - 2>/dev/null || echo "?") - UWAL_COUNT=$(kubectl exec -n tidesql minio -- \ - sh -c 'ls /data/tidesql-test/ 2>/dev/null | grep -c "^uwal_"' \ - 2>/dev/null || echo "?") - REPLICA_LASTLOG=$(kubectl exec -n tidesql tidesql-replica -- \ - sh -c 'tail -1 /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null' \ - 2>&1 | tr -d '\n' | cut -c1-160 || true) - echo " iter=${i} replica_count=$RC bucket_items=$BUCKET_FILES uwal_segments=$UWAL_COUNT replica_lastlog=\"$REPLICA_LASTLOG\"" - if [ "$RC" -ge 202 ] 2>/dev/null; then - echo " converged at iter=${i} (count=$RC)" - REPLICA_OK=1 - break - fi - sleep 1 - done - - snapshot_state "pre-failover-check" - - if [ "$REPLICA_OK" -ne 1 ]; then - echo "FAIL: replica never converged to row 10002 within 60s" - - # Cold-start lines live in the first dozen lines of LOG; the - # WAL-replay / UNIMAP-load loop pushes them off any plain tail. - # Dump head + tail so we can see both the open-time discovery - # and the steady-state poll behavior at once. - echo "=== Replica TidesDB LOG (head 120 lines) ===" - kubectl exec -n tidesql tidesql-replica -- \ - sh -c 'head -120 /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null' || true - echo "=== Replica TidesDB LOG (tail 80 lines) ===" - kubectl exec -n tidesql tidesql-replica -- \ - sh -c 'tail -80 /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null' || true - echo "=== Primary TidesDB LOG (head 120 lines) ===" - kubectl exec -n tidesql tidesql-primary -- \ - sh -c 'head -120 /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null' || true - echo "=== Primary TidesDB LOG (tail 80 lines) ===" - kubectl exec -n tidesql tidesql-primary -- \ - sh -c 'tail -80 /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null' || true - - # Engine status tells us how many SSTables the replica actually - # registered locally. If this shows 0 we know cold-start did - # not populate the per-CF MANIFEST. If it shows N>0 the data - # is registered and the iterator path is the next suspect. - echo "=== Replica SHOW ENGINE TIDESDB STATUS ===" - kubectl exec -n tidesql tidesql-replica -- \ - mariadb --connect-timeout=3 -e "SHOW ENGINE TIDESDB STATUS\G" 2>&1 || true - echo "=== Primary SHOW ENGINE TIDESDB STATUS ===" - kubectl exec -n tidesql tidesql-primary -- \ - mariadb --connect-timeout=3 -e "SHOW ENGINE TIDESDB STATUS\G" 2>&1 || true - - # Replica COUNT(*) re-run without 2>/dev/null suppression so we - # see the actual error if the query is failing rather than - # silently returning empty. - echo "=== Replica COUNT(*) with stderr ===" - kubectl exec -n tidesql tidesql-replica -- \ - mariadb --connect-timeout=3 -e "SELECT COUNT(*) FROM k8s_test.items" 2>&1 || true - echo "=== Replica SHOW DATABASES / SHOW TABLES ===" - kubectl exec -n tidesql tidesql-replica -- \ - mariadb --connect-timeout=3 -e "SHOW DATABASES; SHOW TABLES FROM k8s_test" 2>&1 || true - - # MariaDB error log catches [ERROR] [TIDESDB] lines from plugin - # paths (e.g. drop_table on replica returning TDB_ERR_READONLY). - echo "=== Replica MariaDB error log (full) ===" - kubectl exec -n tidesql tidesql-replica -- \ - sh -c 'cat /usr/local/mariadb/log/error.log 2>/dev/null || cat /usr/local/mariadb/data/default/*.err 2>/dev/null' 2>&1 || true - - echo "=== Full MinIO listing ===" - kubectl exec -n tidesql minio -- ls -lR /data/tidesql-test/ 2>&1 || true - exit 1 - fi - - echo "Pre-failover replica row count: $RC" - echo "Primary restored with test data" - - - name: Deploy automated failover controller - run: | - # Deploy the failover controller using the monitor user for TCP access - kubectl apply -n tidesql -f - <<'EOF' - apiVersion: v1 - kind: ConfigMap - metadata: - name: tidesdb-failover-script - data: - failover.sh: | - #!/bin/bash - set -uo pipefail - PRIMARY_HOST="${PRIMARY_HOST:-tidesql-primary}" - PRIMARY_PORT="${PRIMARY_PORT:-3306}" - REPLICA_HOST="${REPLICA_HOST:-tidesql-replica}" - REPLICA_PORT="${REPLICA_PORT:-3306}" - CHECK_INTERVAL="${CHECK_INTERVAL:-3}" - FAILURE_THRESHOLD="${FAILURE_THRESHOLD:-2}" - MYSQL_USER="${MYSQL_USER:-monitor}" - MYSQL_PASSWORD="${MYSQL_PASSWORD:-M0nitor!Pass9}" - FAILURES=0 - log() { echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*"; } - check_primary() { - mariadb -h "$PRIMARY_HOST" -P "$PRIMARY_PORT" \ - -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" \ - -e "SELECT 1" > /dev/null 2>&1 - } - promote_replica() { - log "PROMOTING replica $REPLICA_HOST to primary" - mariadb -h "$REPLICA_HOST" -P "$REPLICA_PORT" \ - -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" \ - -e "SET GLOBAL tidesdb_promote_primary = ON" 2>&1 - log "Promotion command sent" - } - log "TidesQL Failover Controller started" - log "Primary: $PRIMARY_HOST:$PRIMARY_PORT" - log "Replica: $REPLICA_HOST:$REPLICA_PORT" - log "Check interval: ${CHECK_INTERVAL}s, threshold: $FAILURE_THRESHOLD" - while true; do - if check_primary; then - [ "$FAILURES" -gt 0 ] && log "Primary recovered after $FAILURES failures" - FAILURES=0 - else - FAILURES=$((FAILURES + 1)) - log "Primary check failed ($FAILURES/$FAILURE_THRESHOLD)" - if [ "$FAILURES" -ge "$FAILURE_THRESHOLD" ]; then - promote_replica - log "Failover complete. Exiting." - exit 0 - fi - fi - sleep "$CHECK_INTERVAL" - done - --- - apiVersion: v1 - kind: Pod - metadata: - name: tidesql-failover - labels: - app: tidesql-failover - spec: - restartPolicy: Never - containers: - - name: controller - image: mariadb:latest - command: ["bash", "/scripts/failover.sh"] - env: - - name: PRIMARY_HOST - value: "tidesql-primary" - - name: REPLICA_HOST - value: "tidesql-replica" - - name: CHECK_INTERVAL - value: "3" - - name: FAILURE_THRESHOLD - value: "2" - - name: MYSQL_USER - value: "monitor" - - name: MYSQL_PASSWORD - value: "M0nitor!Pass9" - volumeMounts: - - name: scripts - mountPath: /scripts - volumes: - - name: scripts - configMap: - name: tidesdb-failover-script - defaultMode: 0755 - EOF - - kubectl wait -n tidesql --for=condition=Ready pod/tidesql-failover --timeout=60s - echo "Failover controller deployed" - - # Verify it's running and can reach the primary - sleep 5 - kubectl logs -n tidesql tidesql-failover --tail=5 - - - name: Snapshot primary logs before auto-failover kill - run: | - # The auto-failover step force-deletes tidesql-primary, after which - # `kubectl logs/exec` on that pod returns NotFound and the post- - # mortem dump cannot tell us what the primary was doing. Capture - # everything we'd want from it before the kill. - mkdir -p /tmp/k8s-logs/pre-autofail - kubectl logs -n tidesql tidesql-primary \ - > /tmp/k8s-logs/pre-autofail/primary-stdout.log 2>&1 || true - kubectl exec -n tidesql tidesql-primary -- \ - sh -c 'cat /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null' \ - > /tmp/k8s-logs/pre-autofail/primary-tidesdb.log 2>&1 || true - kubectl exec -n tidesql tidesql-primary -- \ - sh -c 'tail -200 /usr/local/mariadb/log/error.log 2>/dev/null \ - || tail -200 /usr/local/mariadb/data/default/*.err 2>/dev/null' \ - > /tmp/k8s-logs/pre-autofail/primary-mariadb.log 2>&1 || true - kubectl exec -n tidesql tidesql-primary -- \ - mariadb -e "SHOW DATABASES; SHOW TABLES IN k8s_test; SELECT COUNT(*) FROM k8s_test.items;" \ - > /tmp/k8s-logs/pre-autofail/primary-state.txt 2>&1 || true - echo "Captured pre-autofail primary logs to /tmp/k8s-logs/pre-autofail/" - ls -la /tmp/k8s-logs/pre-autofail/ - - - name: Test automated failover - kill primary - run: | - # Pull in the snapshot helper from the prior step (saved by the - # restore step). Defines snapshot_state again in this shell. - if [ -r /tmp/snapshot_state.sh ]; then - source /tmp/snapshot_state.sh 2>/dev/null || true - fi - snapshot_state() { - local label="$1" - echo "" - echo "=== SNAPSHOT [$label] @ $(date -u '+%H:%M:%S') ===" - kubectl exec -n tidesql minio -- \ - ls -la /data/tidesql-test/ 2>&1 | head -20 || true - kubectl exec -n tidesql minio -- \ - sh -c 'ls -la /data/tidesql-test/k8s_test__items/ 2>/dev/null' | head -40 || true - local rc=$(timeout 5 kubectl exec -n tidesql tidesql-replica -- \ - mariadb --connect-timeout=3 -N -e "SELECT COUNT(*) FROM k8s_test.items" 2>/dev/null || echo "?") - echo "replica COUNT(*) = $rc" - kubectl exec -n tidesql tidesql-replica -- \ - sh -c 'tail -12 /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null' \ - 2>&1 || true - echo "=== END SNAPSHOT [$label] ===" - } - - echo "=== Automated failover test ===" - snapshot_state "before-kill" - - echo "Killing primary to trigger automatic failover..." - kubectl delete pod -n tidesql tidesql-primary --grace-period=0 --force - - # The failover controller checks every 3s with threshold=2 - # So it should detect and promote within ~10s - echo "Waiting for failover controller to detect and promote..." - - # Wait for the controller to finish (it exits after promotion). - # While waiting, snapshot the replica + MinIO every few iterations - # so we can see whether the replica is making catch-up progress - # during the dead-primary window. - for i in $(seq 1 20); do - STATUS=$(kubectl get pod -n tidesql tidesql-failover -o jsonpath='{.status.phase}' 2>/dev/null || echo "unknown") - echo " check $i: controller status=$STATUS" - if [ "$STATUS" = "Succeeded" ]; then - echo "Failover controller completed promotion" - break - fi - # Sample replica state on a few iterations so we can correlate - # what the replica saw against when the controller decided. - if [ $((i % 3)) -eq 1 ]; then - RC=$(timeout 3 kubectl exec -n tidesql tidesql-replica -- \ - mariadb --connect-timeout=3 -N -e "SELECT COUNT(*) FROM k8s_test.items" 2>/dev/null || echo "?") - REPL_LAST=$(kubectl exec -n tidesql tidesql-replica -- \ - sh -c 'tail -1 /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null' 2>&1 \ - | tr -d '\n' | cut -c1-160 || true) - echo " [wait-iter=${i}] replica_count=$RC replica_lastlog=\"$REPL_LAST\"" - fi - sleep 3 - done - - snapshot_state "after-controller-completes" - - # Show controller logs - echo "=== Failover controller logs ===" - kubectl logs -n tidesql tidesql-failover 2>/dev/null || true - - - name: Verify automated failover succeeded - timeout-minutes: 2 - run: | - # The replica should now be promoted and accept writes. Instead - # of a blind sleep 5, poll for the count to land for up to 30s so - # the log shows whether the data was visible immediately after - # promotion or only after the engine finished its post-promotion - # MANIFEST replay + WAL replay pass. - echo "=== Post-promotion convergence trajectory ===" - POST_OK=0 - for i in $(seq 1 30); do - RESULT=$(timeout 5 kubectl exec -n tidesql tidesql-replica -- \ - mariadb --connect-timeout=3 -N -e "SELECT COUNT(*) FROM k8s_test.items" 2>/dev/null || echo "0") - PROMOTED_LASTLOG=$(kubectl exec -n tidesql tidesql-replica -- \ - sh -c 'tail -1 /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null' 2>&1 \ - | tr -d '\n' | cut -c1-160 || true) - echo " iter=${i} count=$RESULT lastlog=\"$PROMOTED_LASTLOG\"" - if [ "$RESULT" -ge 202 ] 2>/dev/null; then - POST_OK=1 - break - fi - sleep 1 - done - - echo "" - echo "=== Final MinIO state after auto-failover ===" - kubectl exec -n tidesql minio -- \ - ls -la /data/tidesql-test/ 2>&1 || true - kubectl exec -n tidesql minio -- \ - sh -c 'ls -la /data/tidesql-test/k8s_test__items/ 2>/dev/null' || true - - echo "" - echo "=== Promoted-replica TidesDB LOG (last 100 lines) ===" - kubectl exec -n tidesql tidesql-replica -- \ - sh -c 'tail -100 /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null' || true - - echo "Post-auto-failover row count: $RESULT" - if [ "$POST_OK" -ne 1 ]; then - echo "FAIL: data lost after automated failover (got $RESULT)" - exit 1 - fi - - # Verify promoted replica accepts writes - timeout 15 kubectl exec -n tidesql tidesql-replica -- \ - mariadb --connect-timeout=5 -e "INSERT INTO k8s_test.items VALUES (10003, 'after_auto_failover', REPEAT('G', 500))" \ - 2>&1 || echo "INSERT failed or timed out (non-fatal)" - - NEW_RESULT=$(timeout 10 kubectl exec -n tidesql tidesql-replica -- \ - mariadb --connect-timeout=5 -N -e "SELECT COUNT(*) FROM k8s_test.items" 2>/dev/null || echo "0") - echo "After write: $NEW_RESULT rows" - - echo "Automated failover successful - data intact, writes accepted" - - - name: Collect logs on failure - if: failure() - run: | - # Logs we captured before auto-failover deleted tidesql-primary. - # Without these the post-mortem is blind for the auto-failover scenario. - if [ -d /tmp/k8s-logs/pre-autofail ]; then - echo "=== Pre-auto-failover primary state (captured before pod delete) ===" - for f in /tmp/k8s-logs/pre-autofail/*; do - echo "=+=+= $f =+=+=" - tail -120 "$f" 2>/dev/null || echo "(empty)" - echo "" - done - fi - echo "=== Primary container logs ===" - kubectl logs -n tidesql tidesql-primary 2>/dev/null | tail -50 || echo "(none)" - echo "" - echo "=== Primary MariaDB error log ===" - kubectl exec -n tidesql tidesql-primary -- \ - sh -c 'tail -40 /usr/local/mariadb/log/error.log 2>/dev/null || tail -40 /usr/local/mariadb/data/default/*.err 2>/dev/null' || echo "(none)" - echo "" - echo "=== Primary TidesDB LOG ===" - kubectl exec -n tidesql tidesql-primary -- \ - sh -c 'cat /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null || cat /usr/local/mariadb/tidesdb_data/LOG 2>/dev/null' | tail -40 || echo "(none)" - echo "" - echo "=== Primary config ===" - kubectl exec -n tidesql tidesql-primary -- \ - cat /etc/mysql/custom/tidesdb-k8s.cnf 2>/dev/null || echo "(missing)" - echo "" - echo "=== Replica container logs ===" - kubectl logs -n tidesql tidesql-replica 2>/dev/null | tail -50 || echo "(none)" - echo "" - echo "=== Replica MariaDB error log ===" - kubectl exec -n tidesql tidesql-replica -- \ - sh -c 'tail -40 /usr/local/mariadb/log/error.log 2>/dev/null || tail -40 /usr/local/mariadb/data/default/*.err 2>/dev/null' || echo "(none)" - echo "" - echo "=== Replica TidesDB LOG ===" - kubectl exec -n tidesql tidesql-replica -- \ - sh -c 'cat /usr/local/mariadb/data/tidesdb_data/LOG 2>/dev/null || cat /usr/local/mariadb/tidesdb_data/LOG 2>/dev/null' | tail -40 || echo "(none)" - echo "" - echo "=== MinIO bucket ===" - kubectl exec -n tidesql minio -- ls -lR /data/tidesql-test/ 2>/dev/null | head -30 || echo "(empty or error)" - echo "" - echo "=== Failover controller logs ===" - kubectl logs -n tidesql tidesql-failover 2>/dev/null | tail -20 || echo "(none)" - echo "" - echo "=== MinIO logs ===" - kubectl logs -n tidesql minio 2>/dev/null | tail -20 || echo "(none)" - echo "" - echo "=== Pod status ===" - kubectl get pods -n tidesql -o wide 2>/dev/null || true - echo "" - echo "=== Events ===" - kubectl get events -n tidesql --sort-by='.lastTimestamp' 2>/dev/null | tail -20 || true - - - name: Cleanup - if: always() - run: | - kind delete cluster --name tidesql-test || true diff --git a/.github/workflows/mariadb-test.yml b/.github/workflows/mariadb-test.yml index 366a04eb..80cb6731 100644 --- a/.github/workflows/mariadb-test.yml +++ b/.github/workflows/mariadb-test.yml @@ -7,6 +7,14 @@ on: branches: [ main, master ] workflow_dispatch: +# Pinned build versions, the single place to move from one release to the next. +# TideSQL plugin 5.0.0 targets TidesDB library 10.0.0 on MariaDB 13. Bump these +# two constants together to retarget the whole matrix. TIDESDB_VERSION is a tag +# on tidesdb/tidesdb, MARIADB_VERSION a tag on MariaDB/server. +env: + TIDESDB_VERSION: "10.0.0" + MARIADB_VERSION: "mariadb-13.0.1" + jobs: test: runs-on: ${{ matrix.os }} @@ -94,14 +102,6 @@ jobs: restore-keys: | vcpkg-windows-x64- - # Resolve the TidesDB library version we are about to build so the cache key - # tracks the LIBRARY, not the plugin. Without this the key hashed the plugin - # dir (tidesql/tidesdb/**), so a library bump never invalidated the cache. - - name: Resolve TidesDB library revision - id: tidesdb_rev - shell: bash - run: echo "sha=$(git ls-remote https://github.com/tidesdb/tidesdb.git HEAD | cut -f1)" >> "$GITHUB_OUTPUT" - - name: Cache TidesDB library uses: actions/cache@v4 with: @@ -109,10 +109,11 @@ jobs: /usr/local/lib/libtidesdb* /usr/local/include/tidesdb* C:/tidesdb - # Exact upstream-commit match only. No restore-keys fallback: a loose - # "tidesdb-${{ runner.os }}-" prefix would restore the newest cache for - # the OS regardless of contents, i.e. a stale library. - key: tidesdb-${{ runner.os }}-${{ steps.tidesdb_rev.outputs.sha }} + # Keyed on the pinned TidesDB version, so bumping TIDESDB_VERSION + # invalidates the cache and a stale library is never restored. No + # restore-keys fallback: a loose "tidesdb-${{ runner.os }}-" prefix would + # restore the newest cache for the OS regardless of the pinned version. + key: tidesdb-${{ runner.os }}-${{ env.TIDESDB_VERSION }} - name: Cache ccache if: runner.os != 'Windows' @@ -156,20 +157,13 @@ jobs: echo "CMAKE_TOOLCHAIN_FILE=$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" >> $env:GITHUB_ENV echo "TIDESDB_ROOT=C:/tidesdb" >> $env:GITHUB_ENV - - name: Set MariaDB version - id: mariadb-release - shell: bash - run: | - echo "tag=mariadb-12.2.2" >> $GITHUB_OUTPUT - echo "MariaDB version: mariadb-12.2.2" - - name: Clone MariaDB server shell: bash run: | MAX_RETRIES=3 for i in $(seq 1 $MAX_RETRIES); do - echo "Attempt $i of $MAX_RETRIES: Cloning MariaDB server..." - if git clone --depth 1 --branch ${{ steps.mariadb-release.outputs.tag }} https://github.com/MariaDB/server.git mariadb-server; then + echo "Attempt $i of $MAX_RETRIES: Cloning MariaDB server $MARIADB_VERSION..." + if git clone --depth 1 --branch "$MARIADB_VERSION" https://github.com/MariaDB/server.git mariadb-server; then cd mariadb-server git submodule update --init --recursive --depth 1 exit 0 @@ -186,7 +180,7 @@ jobs: run: | rm -rf tidesdb-lib for i in 1 2 3; do - if git clone --depth 1 https://github.com/tidesdb/tidesdb.git tidesdb-lib; then + if git clone --depth 1 --branch "$TIDESDB_VERSION" https://github.com/tidesdb/tidesdb.git tidesdb-lib; then break fi echo "Clone failed (attempt $i), retrying in 10s..." @@ -209,7 +203,7 @@ jobs: rm -rf tidesdb-lib for i in 1 2 3; do - if git clone --depth 1 https://github.com/tidesdb/tidesdb.git tidesdb-lib; then + if git clone --depth 1 --branch "$TIDESDB_VERSION" https://github.com/tidesdb/tidesdb.git tidesdb-lib; then break fi echo "Clone failed (attempt $i), retrying in 10s..." @@ -230,7 +224,7 @@ jobs: run: | rm -rf tidesdb-lib for i in 1 2 3; do - if git clone --depth 1 https://github.com/tidesdb/tidesdb.git tidesdb-lib; then + if git clone --depth 1 --branch "$TIDESDB_VERSION" https://github.com/tidesdb/tidesdb.git tidesdb-lib; then break fi echo "Clone failed (attempt $i), retrying in 10s..." @@ -252,14 +246,12 @@ jobs: if: runner.os != 'Windows' run: | cp -r tidesql/tidesdb mariadb-server/storage/ - cp -r tidesql/mysql-test/suite/tidesdb mariadb-server/mysql-test/suite/ - name: Copy TidesDB storage engine to MariaDB (Windows) if: runner.os == 'Windows' shell: cmd run: | xcopy /E /I tidesql\tidesdb mariadb-server\storage\tidesdb - xcopy /E /I tidesql\mysql-test\suite\tidesdb mariadb-server\mysql-test\suite\tidesdb - name: Build MariaDB with TidesDB plugin (Ubuntu) if: runner.os == 'Linux' diff --git a/.github/workflows/object_store_test.yml b/.github/workflows/object_store_test.yml deleted file mode 100644 index 2737c1d7..00000000 --- a/.github/workflows/object_store_test.yml +++ /dev/null @@ -1,142 +0,0 @@ -name: Object Store Tests (MinIO) - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - workflow_dispatch: - -jobs: - objstore-test: - runs-on: ubuntu-latest - name: TidesDB Object Store Integration (MinIO) - - steps: - - name: Checkout TideSQL - uses: actions/checkout@v4 - with: - path: tidesql - - - name: Start MinIO - run: | - docker run -d --name minio \ - -p 9000:9000 \ - -e MINIO_ROOT_USER=minioadmin \ - -e MINIO_ROOT_PASSWORD=minioadmin \ - minio/minio:latest server /data - for i in $(seq 1 30); do - curl -sf http://localhost:9000/minio/health/live && break - echo "waiting for minio ($i)..." - sleep 2 - done - - - name: Create test bucket - run: | - curl -sL https://dl.min.io/client/mc/release/linux-amd64/mc -o /tmp/mc - chmod +x /tmp/mc - /tmp/mc alias set local http://localhost:9000 minioadmin minioadmin - /tmp/mc mb local/tidesql-test || true - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - build-essential cmake ninja-build git \ - libncurses5-dev libssl-dev libxml2-dev zlib1g-dev \ - libevent-dev libcurl4-openssl-dev liblz4-dev \ - libzstd-dev libsnappy-dev bison gnutls-dev \ - libpcre2-dev ccache - - - name: Cache ccache - uses: actions/cache@v4 - with: - path: ~/.ccache - key: ccache-objstore-${{ github.sha }} - restore-keys: ccache-objstore- - - - name: Configure ccache - run: | - ccache --set-config=max_size=500M - ccache --set-config=compression=true - ccache -z - - - name: Install TidesDB library with S3 support - run: | - git clone --depth 1 https://github.com/tidesdb/tidesdb.git tidesdb-lib - cd tidesdb-lib - echo "TidesDB commit: $(git rev-parse HEAD)" - mkdir build && cd build - cmake .. -DTIDESDB_WITH_S3=ON - make -j$(nproc) - sudo make install - sudo ldconfig - - - name: Clone MariaDB server - run: | - git clone --depth 1 --branch mariadb-11.8.6 \ - https://github.com/MariaDB/server.git mariadb-server - cd mariadb-server - git submodule update --init --recursive --depth 1 - - - name: Copy TidesDB storage engine to MariaDB - run: | - cp -r tidesql/tidesdb mariadb-server/storage/ - cp -r tidesql/mysql-test/suite/tidesdb mariadb-server/mysql-test/suite/ - - - name: Build MariaDB with TidesDB plugin - working-directory: mariadb-server - run: | - mkdir -p build && cd build - cmake .. \ - -G Ninja \ - -DCMAKE_C_COMPILER_LAUNCHER=ccache \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ - -DPLUGIN_TIDESDB=DYNAMIC \ - -DTIDESDB_WITH_S3=ON \ - -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DPLUGIN_ROCKSDB=NO \ - -DPLUGIN_MROONGA=NO \ - -DPLUGIN_SPIDER=NO \ - -DPLUGIN_COLUMNSTORE=NO \ - -DPLUGIN_CONNECT=NO \ - -DPLUGIN_OQGRAPH=NO \ - -DPLUGIN_ARCHIVE=NO \ - -DWITH_UNIT_TESTS=OFF \ - -DCONC_WITH_UNITTEST=OFF \ - -DWITH_MARIABACKUP=OFF \ - -DWITH_EMBEDDED_SERVER=OFF - ninja -j$(nproc) - ccache -s - - - name: Run object store MTR test - working-directory: mariadb-server/build - run: | - ./mysql-test/mtr \ - --suite=tidesdb \ - tidesdb.tidesdb_object_store \ - --mysqld=--tidesdb_object_store_backend=S3 \ - --mysqld=--tidesdb_s3_endpoint=localhost:9000 \ - --mysqld=--tidesdb_s3_bucket=tidesql-test \ - --mysqld=--tidesdb_s3_access_key=minioadmin \ - --mysqld=--tidesdb_s3_secret_key=minioadmin \ - --mysqld=--tidesdb_s3_use_ssl=OFF \ - --mysqld=--tidesdb_s3_path_style=ON \ - --force --retry=1 - - - name: Run standard test suite (local mode) - working-directory: mariadb-server/build - run: | - ./mysql-test/mtr --suite=tidesdb --parallel=4 --force --retry=1 - - - name: Verify bucket has data - if: always() - run: | - echo "Objects in tidesql-test bucket:" - /tmp/mc ls --recursive local/tidesql-test/ | head -20 || true - echo "Total objects:" - /tmp/mc ls --recursive local/tidesql-test/ | wc -l || true - - - name: Stop MinIO - if: always() - run: docker rm -f minio || true diff --git a/.gitignore b/.gitignore index 0ba47a79..8fc5c23d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ cmake-build-release build .vscode vcpkg_installed -.vs \ No newline at end of file +.vs +doc/*.pdf \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..e68f65cd --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,5 @@ +# For MariaDB Packaging + +cmake_minimum_required(VERSION 3.12) +find_package(mariadb-plugin REQUIRED) +add_subdirectory(tidesdb) \ No newline at end of file diff --git a/LICENSE b/LICENSE index 515f5335..57f2ac83 100644 --- a/LICENSE +++ b/LICENSE @@ -1,12 +1,339 @@ - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; version 2 of the License. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA \ No newline at end of file + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/README b/README index 245c56b9..e42cd6ed 100644 --- a/README +++ b/README @@ -1,751 +1,13 @@ TIDESQL -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +======= -A pluggable write and space optimized storage engine for MariaDB using TidesDB. - -To find compatibility table with MariaDB go to https://tidesdb.com/reference/tidesql/#mariadb-compatibility - - -INSTALLATION -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ - -You can choose to run install.sh, for instructions run ./install.sh --help - -To install in a specific location(e.g /data being NVMe disk) -./install.sh --mariadb-prefix /data/mariadb --tidesdb-prefix /data/tidesdb --build-dir /data/tidesql-build - - -The install script will build TidesDB and MariaDB from source. -You can specify the versions if you want. The script will install everything -and make TidesDB available as an engine to utilize. - -To skip storage engines you don't need (saves build time and reduces warnings): - -./install.sh --list-engines # see what can be skipped -./install.sh --skip-engines mroonga,rocksdb,connect,spider,oqgraph,columnstore - -To use specific MariaDB version you use: ---mariadb-version mariadb-12.3.1 - -mariadb-12.3.1 aligning to https://github.com/MariaDB/server/releases/tag/mariadb-12.3.1 - -When using install script you are installing the latest version of TideSQL at the time of clone or download. - -To link libtidesdb against a non-default allocator (jemalloc is a common -pick for write-heavy workloads): - -./install.sh --allocator jemalloc -./install.sh --allocator mimalloc -./install.sh --allocator tcmalloc - -The flag only affects libtidesdb.so's internal allocations; mariadbd's -allocator is unchanged. For a process-wide swap also LD_PRELOAD the -allocator at mariadbd startup: - - LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 \ - /usr/local/mariadb/bin/mariadbd-safe \ - --defaults-file=/usr/local/mariadb/my.cnf & - -Note that --rebuild-plugin does not rebuild libtidesdb, so changing the -allocator requires a full install run (omit --rebuild-plugin) to take -effect. Verify the linkage with: - - ldd /usr/local/lib/libtidesdb.so | grep -E 'jemalloc|mimalloc|tcmalloc' - -Skippable engines: - archive Archive storage engine (read-only row-format tables) - blackhole Blackhole engine (accepts writes, stores nothing) - columnstore MariaDB ColumnStore (columnar analytics engine) - connect CONNECT engine (access external data sources) - example Example storage engine (test/demo only) - federated Legacy Federated engine (MODULE_ONLY) - federatedx FederatedX engine (query remote MySQL/MariaDB tables) - mroonga Mroonga full-text search engine (requires Groonga) - oqgraph Open Query GRAPH engine (graph computation) - rocksdb MyRocks / RocksDB LSM-tree engine - sequence Sequence engine (virtual auto-increment sequences) - sphinx SphinxSE engine (Sphinx full-text search integration) - spider Spider engine (sharding / federation) - -InnoDB, Aria, MyISAM, and CSV cannot be skipped (server depends on them). - -Below are manual install instructions for those who want to configure everything -themselves, though with the install script you can modify configuration files -after the fact. - -LINUX (Ubuntu/Debian) -░░░░░░░░░░░░░░░░░░░░░░░░ - -1. Install dependencies: - - sudo apt update - sudo apt install -y build-essential cmake ninja-build bison flex \ - libzstd-dev liblz4-dev libsnappy-dev libncurses-dev libssl-dev \ - libxml2-dev libevent-dev libcurl4-openssl-dev pkg-config - -2. Install TidesDB library: - - git clone --depth 1 https://github.com/tidesdb/tidesdb.git tidesdb-lib - cd tidesdb-lib && mkdir build && cd build - cmake .. - make -j$(nproc) - sudo make install - sudo ldconfig - -3. Clone MariaDB and copy TidesDB storage engine: - - git clone --depth 1 --branch mariadb-11.8.6 https://github.com/MariaDB/server.git mariadb-server - cd mariadb-server - git submodule update --init --recursive - cp -r /path/to/tidesql/tidesdb storage/ - mkdir -p storage/tidesdb/mysql-test - cp -r /path/to/tidesql/mysql-test/suite/tidesdb storage/tidesdb/mysql-test/ - -4. Build MariaDB: - - mkdir build && cd build - cmake .. - make -j$(nproc) - -5. Run tests (from the build directory): - - cd mysql-test - perl mtr --suite=tidesdb --parallel=4 --force - - -MACOS -░░░░░░░░░░░░░░░░░░░░░░░░ - -1. Install dependencies: - - brew install cmake ninja bison snappy lz4 zstd - -2. Install TidesDB library: - - # Ensure Xcode SDK is used (not CommandLineTools) - sudo xcode-select -s /Applications/Xcode.app/Contents/Developer - export SDKROOT=$(xcrun --show-sdk-path) - - git clone --depth 1 https://github.com/tidesdb/tidesdb.git tidesdb-lib - cd tidesdb-lib && mkdir build && cd build - cmake .. -DCMAKE_OSX_SYSROOT=${SDKROOT} - make -j$(sysctl -n hw.ncpu) - sudo make install - -3. Clone MariaDB and copy TidesDB storage engine: - - git clone --depth 1 --branch mariadb-11.8.6 https://github.com/MariaDB/server.git mariadb-server - cd mariadb-server - git submodule update --init --recursive - cp -r /path/to/tidesql/tidesdb storage/ - mkdir -p storage/tidesdb/mysql-test - cp -r /path/to/tidesql/mysql-test/suite/tidesdb storage/tidesdb/mysql-test/ - -4. Build MariaDB: - - # Remove CommandLineTools SDK to prevent header conflicts - sudo rm -rf /Library/Developer/CommandLineTools/SDKs/MacOSX*.sdk - - export SDKROOT=$(xcrun --show-sdk-path) - export CC=$(xcrun -find clang) - export CXX=$(xcrun -find clang++) - - mkdir build && cd build - cmake .. \ - -DCMAKE_C_COMPILER=${CC} \ - -DCMAKE_CXX_COMPILER=${CXX} \ - -DCMAKE_OSX_SYSROOT=${SDKROOT} \ - -DCMAKE_C_FLAGS="-Wno-nullability-completeness" \ - -DCMAKE_CXX_FLAGS="-Wno-nullability-completeness" \ - -DWITH_SSL=bundled \ - -DWITH_PCRE=bundled \ - -G Ninja - cmake --build . --parallel $(sysctl -n hw.ncpu) - - Note -- The CommandLineTools SDK removal is needed because CMake may find - headers in /Library/Developer/CommandLineTools/SDKs/MacOSX*.sdk which - causes C/C++ header path conflicts with libc++. - -5. Run tests (from the build directory): - - cd mysql-test - perl mtr --suite=tidesdb --parallel=4 --force - - -WINDOWS -░░░░░░░░░░░░░░░░░░░░░░░░ - -1. Install prerequisites: - - - Visual Studio 2022 with C++ workload - - CMake (choco install cmake) - - Strawberry Perl (choco install strawberryperl) - - WinFlexBison (download from GitHub releases) - -2. Install vcpkg dependencies: - - cd C:\vcpkg - git pull - .\vcpkg.exe install zstd:x64-windows lz4:x64-windows snappy:x64-windows pthreads:x64-windows - -3. Install TidesDB library: - - git clone --depth 1 https://github.com/tidesdb/tidesdb.git tidesdb-lib - cd tidesdb-lib - mkdir build && cd build - cmake .. -G "Visual Studio 17 2022" -A x64 ^ - -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake ^ - -DCMAKE_INSTALL_PREFIX=C:/tidesdb - cmake --build . --config Release - cmake --install . --config Release - -4. Clone MariaDB and copy TidesDB storage engine: - - git clone --depth 1 --branch mariadb-11.8.6 https://github.com/MariaDB/server.git mariadb-server - cd mariadb-server - git submodule update --init --recursive - xcopy /E /I path\to\tidesql\tidesdb storage\tidesdb - xcopy /E /I path\to\tidesql\mysql-test\suite\tidesdb storage\tidesdb\mysql-test\tidesdb - -5. Build MariaDB: - - mkdir build && cd build - cmake .. -G "Visual Studio 17 2022" -A x64 ^ - -DCMAKE_PREFIX_PATH="C:/tidesdb;C:/vcpkg/installed/x64-windows" ^ - -DCONNECT_DYNAMIC=NO - cmake --build . --config RelWithDebInfo --parallel - - Note -- CONNECT plugin is disabled (-DCONNECT_DYNAMIC=NO) because it - requires libxml2 headers that may conflict with vcpkg installations. - -6. Run tests: - - cd mysql-test - perl mtr --suite=tidesdb --parallel=4 --force - - -ENABLE PLUGIN -░░░░░░░░░░░░░░░░░░░░░░░░ - -After building, enable the plugin in MariaDB: - - INSTALL SONAME 'ha_tidesdb'; -- works on all platforms - - - -FEATURES -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ - -Core: -- MVCC transactions with per-table isolation (autocommit uses READ_COMMITTED; - multi-statement transactions use the table's configured level) -- SQL savepoints (SAVEPOINT / ROLLBACK TO / RELEASE SAVEPOINT) -- START TRANSACTION WITH CONSISTENT SNAPSHOT -- Lock-free concurrency (no THR_LOCK, TidesDB handles isolation internally) -- Pessimistic row locking (tidesdb_pessimistic_locking, ON by default). - Two-mode lock manager with shared (S) and exclusive (X) locks over a - cache-line aligned hash-partitioned table sized at init from CPU - count (8 * hardware_concurrency, clamped to 128..65536 partitions). - Multiple S holders coexist on the same row, X conflicts with any - other holder, and new S requests block while an X is queued so - writers are never starved. Writes acquire X on the actual mutation - target inside write_row, update_row and delete_row; range scans - under UPDATE/DELETE do not lock rows that ICP filters away. SELECT - FOR UPDATE keeps the per-row lock on every cursor-visible row per - the SQL contract. Plain reads acquire S under REPEATABLE-READ or - SERIALIZABLE, no lock under READ-COMMITTED or SNAPSHOT (MVCC - snapshot suffices). DFS wait-for-graph deadlock detection bounded - by DEADLOCK_MAX_DEPTH walks every conflicting holder per hop; - lock-wait timeout configurable via tidesdb_lock_wait_timeout_ms - (default 50 s, mirrors innodb_lock_wait_timeout). Lock entries - recycle in place once empty so per-partition memory is bounded by - peak concurrent locks. Status counters tidesdb_lock_waits, - tidesdb_lock_wait_us, tidesdb_lock_deadlocks, tidesdb_lock_timeouts, - tidesdb_lock_held, tidesdb_lock_entries, tidesdb_lock_entry_recycles - surface contention for observability -- LSM-tree storage with optional B+tree SSTable format -- Compression (NONE, LZ4, LZ4_FAST, ZSTD, Snappy) -- Bloom filters for fast key lookups -- Block cache for frequently accessed data -- Primary key (single and composite) and secondary index support -- Index Condition Pushdown (ICP) for secondary index scans -- Multi-Range Read (MRR) with sorted point-lookup batching for queries of - the shape WHERE col IN (v1, v2, ..., vN). Accepted only when every - range is a full-key point equality and there are 2+ ranges; keys are - sorted by comparable bytes so the LSM sees a monotone stream of seeks. -- Bulk UPDATE / DELETE batching -- multi-row UPDATE and DELETE statements - share the same mid-txn commit plumbing as bulk INSERT, keeping long - statements' txn memory bounded regardless of statement size -- Single-delete with pair cancellation at compaction -- every secondary - index delete (regular, FTS, spatial) uses tidesdb_txn_single_delete - unconditionally, safe by construction because each (col_values, pk) - composite is put once and deleted once per row lifetime. Primary CF - single-delete is opt-in via the tidesdb_single_delete_primary session - variable (default OFF) for sessions that only INSERT and DELETE -- Tombstone-density compaction trigger -- per-table TOMBSTONE_DENSITY_TRIGGER - and TOMBSTONE_DENSITY_MIN_ENTRIES options arm a post-flush check that - escalates compaction for any single SSTable whose tombstone density - crosses the configured ratio. Aggregates surface as - tidesdb_total_tombstones, tidesdb_tombstone_ratio, and - tidesdb_max_sst_tombstone_density status vars -- Auto compact-after-range-delete -- session variable - tidesdb_compact_after_range_delete_min_rows (default 0) lets a multi-row - DELETE that touches at least N rows synchronously call - tidesdb_compact_range over the touched primary-key range at end of - statement, physically reclaiming tombstoned space without waiting for - a structural trigger -- REPLACE INTO and INSERT ... ON DUPLICATE KEY UPDATE -- AUTO_INCREMENT with O(1) atomic counter (no iterator per INSERT). - TRUNCATE TABLE and ALTER TABLE ... AUTO_INCREMENT=N both correctly - reset the counter via the reset_auto_increment handler hook. -- TTL (time-to-live) per-row and per-table expiration -- Virtual/generated columns -- Online backup (SET GLOBAL tidesdb_backup_dir = '/path') -- Hard-link checkpoint (SET GLOBAL tidesdb_checkpoint_dir = '/path') -- OPTIMIZE TABLE (synchronous purge + compact via tidesdb_purge_cf) -- CHECK TABLE (verifies SSTable metadata and block integrity) -- REPAIR TABLE (full purge + compaction, rebuilds LSM tree) -- FLUSH TABLES FOR EXPORT (HA_CAN_EXPORT, consistent file copy) -- SHOW ENGINE TIDESDB STATUS (DB stats, memory, cache, conflict info, - unified-memtable block with active bytes / immutable count / WAL - generation / next-CF index, and a write-amplification block with - cumulative user/WAL/flush/compaction bytes plus a derived total WA - ratio. Object-store builds also report the last successfully - uploaded unified-WAL generation.) -- SHOW GLOBAL STATUS LIKE 'tidesdb%' monitoring variables for tools like - Prometheus, PMM, Datadog -- column-family and memtable bytes, cache hit - rate, tombstone density aggregates, lock-manager observability - (tidesdb_lock_waits, tidesdb_lock_wait_us, tidesdb_lock_deadlocks, - tidesdb_lock_timeouts, tidesdb_lock_held, tidesdb_lock_entries, - tidesdb_lock_entry_recycles), back-pressure wait counters - (tidesdb_backpressure_waits, tidesdb_backpressure_wait_us), unified - memtable state (tidesdb_unified_memtable_enabled, - tidesdb_unified_memtable_bytes, tidesdb_unified_immutable_count, - tidesdb_unified_is_flushing, tidesdb_unified_wal_generation), - object-store progress (tidesdb_object_store_enabled, - tidesdb_replica_mode_active, tidesdb_primary_epoch, tidesdb_seen_epoch, - tidesdb_local_cache_bytes, tidesdb_local_cache_files, - tidesdb_upload_queue_depth, tidesdb_total_uploads, tidesdb_upload_failures, - tidesdb_last_uploaded_generation), and write-amplification counters - (tidesdb_uwal_bytes_written, tidesdb_wal_bytes_written, - tidesdb_flush_bytes_written, tidesdb_compaction_bytes_written, - tidesdb_compaction_bytes_read, tidesdb_user_bytes_written, - tidesdb_flush_count, tidesdb_compaction_count) -- Handlerton lifecycle + administrative hooks wired: - drop_database DROP DATABASE removes all TidesDB CFs for the db - flush_logs FLUSH LOGS syncs the TidesDB WAL (safe before backup) - panic orderly close on signal-driven shutdown - pre_shutdown background thread quiesce before deinit - kill_query KILL QUERY wakes waiters in row_lock_acquire and is - checked inside rnd_next / index_next / spatial / - ft_read so long scans return HA_ERR_ABORTED_BY_USER -- Partitioning (RANGE, LIST, HASH, KEY) -- Data-at-rest encryption (MariaDB encryption plugin integration; fail-closed - on missing key so a rotated-out version cannot silently mis-encrypt or - return zeroed plaintext) -- Online DDL (instant metadata, inplace add/drop secondary index, force-copy - for FULLTEXT/SPATIAL adds so row back-fill goes through write_row, copy - for column-type changes and PK rewrites) -- TRUNCATE TABLE as O(1) drop+recreate (instant regardless of table size) -- ANALYZE TABLE with detailed CF statistics (levels, keys, sizes, cache - hit rate, plus a per-CF write-amplification note that reports user - bytes ingested versus WAL / flush / compaction bytes and the derived - ratio, so a single ANALYZE makes the WA of an individual table - legible without having to compute it from global counters) -- Cached optimizer statistics (refreshed every 2 seconds from TidesDB) -- Full-text search (FULLTEXT indexes with BM25 ranking, natural language - and boolean mode, charset-aware tokenizer, configurable BM25 parameters, - stop words matching InnoDB defaults, blend_chars for Romance language - elision support, extended FT API via HA_CAN_FULLTEXT_EXT) -- Vector search (VECTOR indexes via MariaDB's MHNSW approximate nearest - neighbor, Euclidean and cosine distance, INSERT/UPDATE/DELETE support) -- Spatial indexes (SPATIAL KEY with Hilbert curve encoding on LSM, - MBRIntersects/MBRContains/MBRWithin/MBREquals/MBRDisjoint predicates) - -Cloud / Object Store: -- S3-compatible object store backend (AWS S3, MinIO, GCS) -- Four-tier hot/warm/cold/frozen storage hierarchy -- Block-level range_get for point lookups on frozen SSTables (single HTTP request) -- Parallel SSTable prefetch for iterators -- Read-only replica mode with MANIFEST sync and WAL replay -- Primary promotion via SET GLOBAL tidesdb_promote_primary = ON (claims a new - lease epoch; can fail under fencing if another node already holds a newer one) -- Single-writer fencing via object-store lease epochs -- promotion claims the - next epoch with a conditional write, a superseded primary fails its next - publish and self-demotes to replica, so split-brain writes never become - authoritative (observable via tidesdb_primary_epoch / tidesdb_seen_epoch) -- Filesystem object-store backend (object_store_backend=FS) for failover and - replication testing across processes without S3 -- Configurable WAL sync (per-commit for RPO=0, or bytes-threshold) -- LRU local file cache with paired klog/vlog eviction -- Upload retry with verification, download retry with backoff -- Kubernetes HA deployment -- one identical StatefulSet pool with a - lease-elected primary and object-store-fenced single-writer safety - (see k8s/production/) - - -CONFIGURATION -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ - -TidesDB stores its data as a sibling of the MariaDB data directory: - /tidesdb_data - - -SYSTEM VARIABLES (SET GLOBAL tidesdb_...) -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ - -Read-only (set at startup): - - flush_threads Background flush threads (default: 4; 0 lets the - library auto-size to min(CPU count, 4)) - max_concurrent_flushes Global cap on in-flight memtable flushes; 0 aligns - the cap with flush_threads (default: 0) - compaction_threads Background compaction threads (default: 4) - log_level DEBUG/INFO/WARN/ERROR/FATAL/NONE (default: DEBUG) - block_cache_size Global block cache in bytes (default: 256MB) - max_open_sstables Max cached SSTable files (default: 256; 0 = unlimited, - bounded only by the process open-file limit) - max_memory_usage Global memory limit in bytes; 0 = auto (default: 0) - data_home_dir Override TidesDB data directory (default: auto) - log_to_file Write logs to file vs stderr (default: ON) - log_truncation_at Log file truncation size (default: 24MB; 0 = off) - unified_memtable Single shared WAL+memtable across all CFs (default: ON) - unified_memtable_write_buffer_size Write buffer for unified memtable (default: 256MB) - unified_memtable_sync_mode NONE/INTERVAL/FULL for unified WAL (default: FULL) - unified_memtable_sync_interval Sync interval in µs for INTERVAL mode (default: 128000) - unified_memtable_skip_list_max_level Skip-list max level for the unified - memtable; 0 keeps the library default - unified_memtable_skip_list_probability Skip-list level promotion probability - for the unified memtable; 0.0 keeps - the library default - object_store_backend LOCAL, S3, or FS (default: LOCAL). FS uses a - shared local filesystem directory as the bucket - stand-in (flock-based single-writer safety) so - failover and replication can be exercised without S3 - objstore_fs_path Shared directory used as the bucket when - object_store_backend=FS; every node points at the - same path (required for FS, ignored otherwise) - s3_endpoint S3 endpoint (e.g. s3.amazonaws.com) - s3_bucket S3 bucket name - s3_prefix S3 key prefix (e.g. production/db1/) - s3_access_key S3 access key ID - s3_secret_key S3 secret access key - s3_region S3 region (NULL for MinIO) - s3_use_ssl Use HTTPS (default: ON) - s3_path_style Path-style URLs for MinIO (default: OFF) - s3_tls_ca_path Custom CA bundle path for the S3 TLS handshake; - NULL uses the system bundle - s3_tls_insecure_skip_verify Disable TLS peer/host verification (INSECURE, - test endpoints only; default: OFF) - s3_multipart_threshold Object size in bytes that triggers S3 multipart - upload; 0 keeps the library default - s3_multipart_part_size S3 multipart chunk size in bytes; 0 keeps the - library default - objstore_local_cache_max Local cache size limit (default: 0 = unlimited) - objstore_wal_sync_threshold WAL sync byte threshold (default: 1MB) - objstore_wal_sync_on_commit Upload WAL on every commit (default: OFF) - objstore_cache_on_read Cache downloaded objects locally (default: ON) - objstore_cache_on_write Cache uploaded objects locally (default: ON) - objstore_max_concurrent_uploads - Concurrent upload threads; 0 keeps the library - default - objstore_max_concurrent_downloads - Concurrent download threads; 0 keeps the library - default - objstore_multipart_threshold - Object size in bytes that triggers multipart - upload at the objstore layer; 0 keeps the library - default - objstore_multipart_part_size - Multipart chunk size in bytes at the objstore - layer; 0 keeps the library default - objstore_sync_manifest_to_object - Upload MANIFEST after each compaction (default: ON) - objstore_wal_upload_sync Block memtable flush on WAL upload - (default: OFF for background WAL upload) - objstore_replicate_wal Upload WAL segments for replica recovery - (default: ON) - objstore_replica_replay_wal Replay WAL on replicas for near-real-time - visibility (default: ON) - replica_mode Read-only replica (default: OFF) - replica_sync_interval MANIFEST poll interval in us (default: 5000000) - finish_compactions_on_close - Maps to tidesdb_config_t.finish_compactions_on_close, - applied at open (default: OFF). Default OFF makes - tidesdb_close cancel in-flight compactions at their - next checkpoint for a fast shutdown -- the cancelled - merge discards uncommitted output and leaves inputs - intact, so no data is lost either way and recovery - handles a mid-merge state the same. Set ON to let - in-flight compactions run to completion before close - returns, which is safer for object-store and replica - setups where a mid-compaction cancel can leave S3 - with referenced-but-not-yet-uploaded SSTables that - confuse a syncing replica. - -Dynamic (SET GLOBAL at runtime): - - backup_dir Set to a path to trigger online backup - checkpoint_dir Set to a path to trigger hard-link checkpoint - print_all_conflicts Log all TDB_ERR_CONFLICT events (default: OFF) - pessimistic_locking Enable plugin-level row locks for SELECT...FOR UPDATE, - UPDATE, DELETE, and INSERT (default: ON). Two-mode - S/X lock manager over a cache-line aligned - hash-partitioned table sized at init from CPU - count (8 * hardware_concurrency, clamped to - 128..65536 partitions). Writes acquire X on the - actual mutation target inside write_row, - update_row and delete_row; range scans under - UPDATE/DELETE do not lock rows that ICP filters - away. Plain reads under REPEATABLE-READ or - SERIALIZABLE acquire S (multiple S holders - coexist), while READ-COMMITTED and SNAPSHOT - reads take no lock and rely on MVCC. A new S - blocks while an X is queued so writers cannot - be starved. DFS wait-for-graph deadlock - detection with bounded depth, lock-wait timeout - controlled by tidesdb_lock_wait_timeout_ms. Locks - can cover non-existing keys (SFU on a missing row - blocks INSERT of that key). OFF reverts to pure - optimistic MVCC, where conflicts surface as - HA_ERR_LOCK_DEADLOCK at COMMIT and the application - must retry. - promote_primary Set to ON to promote a replica to primary; claims a - new lease epoch with a conditional write and can fail - (returns a SQL error, node stays a replica) if - another node already holds a newer epoch, so a - failover controller must check the result rather than - assume success (trigger variable, resets to OFF) - fts_min_word_len Minimum word length for FTS indexing (default: 3) - fts_max_word_len Maximum word length for FTS indexing (default: 84) - fts_bm25_k1 BM25 k1 term-frequency saturation (default: 1.2) - fts_bm25_b BM25 b document-length normalization (default: 0.75) - ft_stopword_table Custom stop word table 'db/table' (default: NULL = InnoDB defaults) - fts_blend_chars Blend characters for Romance elision (default: empty) - -Session (SET SESSION tidesdb_...): - - ttl Per-session TTL override in seconds (default: 0) - skip_unique_check Skip PK/unique checks on INSERT (default: OFF) - lock_wait_timeout_ms Milliseconds a pessimistic row-lock acquire - waits before returning HA_ERR_LOCK_WAIT_TIMEOUT - (default: 50000, mirrors innodb_lock_wait_timeout). - 0 disables the bound and the wait is interrupted - only by KILL QUERY. - backpressure_wait_timeout_ms - Milliseconds the plugin blocks a writer on - TidesDB back-pressure (memtable, flush queue, - or L0 backlog at soft cap) before surfacing - the condition to the SQL layer as - HA_ERR_LOCK_WAIT_TIMEOUT (default: 60000). - The budget is anchored once per statement at - external_lock(F_WRLCK) and shared across every - backpressure call inside that statement, so a - multi-row INSERT respects the configured ms - end-to-end instead of multiplying it by row - count. 0 disables blocking and the - back-pressure signal returns immediately. - single_delete_primary Route primary-CF DELETEs through - tidesdb_txn_single_delete (default: OFF). - Caller promises the session does no UPDATE - on non-PK columns, no REPLACE INTO, and no - INSERT ... ON DUPLICATE KEY UPDATE on tables - without secondary indexes. Violating the - contract may re-expose older row versions - after compaction. Safe for INSERT-only + - DELETE-only workloads (e.g. log rotation, - iibench l.i1 shape). - compact_after_range_delete_min_rows - If non-zero, after a multi-row DELETE that - touches at least this many rows, the engine - calls tidesdb_compact_range over the - touched primary-key range to physically - reclaim tombstoned space synchronously - instead of waiting for a structural - compaction trigger (default: 0 = disabled). - Useful for sliding-window expiry, tenant - eviction, or any DELETE-by-PK-range op. - default_compression Default compression for new tables - default_write_buffer_size Default write buffer for new tables (64MB) - default_sync_mode Default sync mode for new tables (FULL) - default_tombstone_density_trigger - Default tombstone-density compaction - trigger ratio for new tables, in parts - per 10000 (5000 = 0.50 ratio). When set - non-zero, after each flush the engine - inspects level-1 SSTables and escalates - compaction for any single SST whose - tombstone count divided by entry count - exceeds the ratio (default: 0 = disabled). - default_tombstone_density_min_entries - Minimum entry count for an SSTable to be - considered by the tombstone-density - trigger; smaller SSTables are ignored - (default: 1024). - (and other default_* variables for all table options) - -Logging -- TidesDB writes to /LOG by default with automatic -truncation at 24 MB. Set log_level to WARN or higher in production to -reduce log volume. - - -TABLE OPTIONS (CREATE TABLE ... ENGINE=TidesDB