From dabb32cfe7841341e0192103f905d1d1b34466b0 Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Thu, 6 Aug 2026 10:01:16 -0500 Subject: [PATCH 1/3] chore: remove obsolete egg-info files and add bulk load API and SQL parser ADRs --- bindings/python/decentdb.egg-info/PKG-INFO | 189 ------------------ bindings/python/decentdb.egg-info/SOURCES.txt | 45 ----- .../decentdb.egg-info/dependency_links.txt | 1 - .../python/decentdb.egg-info/entry_points.txt | 7 - .../python/decentdb.egg-info/requires.txt | 2 - .../python/decentdb.egg-info/top_level.txt | 2 - ...md => 0212-bulk-load-api-specification.md} | 0 ...uery.md => 0213-sql-parser-libpg-query.md} | 0 ...ary-key.md => 0214-integer-primary-key.md} | 0 9 files changed, 246 deletions(-) delete mode 100644 bindings/python/decentdb.egg-info/PKG-INFO delete mode 100644 bindings/python/decentdb.egg-info/SOURCES.txt delete mode 100644 bindings/python/decentdb.egg-info/dependency_links.txt delete mode 100644 bindings/python/decentdb.egg-info/entry_points.txt delete mode 100644 bindings/python/decentdb.egg-info/requires.txt delete mode 100644 bindings/python/decentdb.egg-info/top_level.txt rename design/adr/{0027-bulk-load-api-specification.md => 0212-bulk-load-api-specification.md} (100%) rename design/adr/{0035-sql-parser-libpg-query.md => 0213-sql-parser-libpg-query.md} (100%) rename design/adr/{0036-integer-primary-key.md => 0214-integer-primary-key.md} (100%) diff --git a/bindings/python/decentdb.egg-info/PKG-INFO b/bindings/python/decentdb.egg-info/PKG-INFO deleted file mode 100644 index 14311805..00000000 --- a/bindings/python/decentdb.egg-info/PKG-INFO +++ /dev/null @@ -1,189 +0,0 @@ -Metadata-Version: 2.4 -Name: decentdb -Version: 2.15.0 -Summary: Python DB-API 2.0 driver and SQLAlchemy dialect for DecentDB -Author: DecentDB Contributors -Classifier: Development Status :: 4 - Beta -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: Apache Software License -Classifier: Programming Language :: Python :: 3 -Requires-Python: >=3.8 -Description-Content-Type: text/markdown -Requires-Dist: SQLAlchemy>=2.0.0 -Requires-Dist: rich>=13.0.0 - -# DecentDB Python Bindings - -This package provides: -1. `decentdb`: A DB-API 2.0 compliant driver for DecentDB. Like `sqlite3`, - `Connection.execute(...)` returns a new cursor for each call. -2. `decentdb_sqlalchemy`: A SQLAlchemy 2.x dialect. - -## Usage - -```python -import sqlalchemy -from sqlalchemy import create_engine - -# Use the decentdb dialect -engine = create_engine("decentdb+pysql:////path/to/database.ddb") - -with engine.connect() as conn: - conn.execute(sqlalchemy.text("CREATE TABLE IF NOT EXISTS users (id INT, name TEXT)")) - conn.execute(sqlalchemy.text("INSERT INTO users VALUES (1, 'Alice')")) - conn.commit() - - result = conn.execute(sqlalchemy.text("SELECT * FROM users")) - for row in result: - print(row) -``` - -## Semantic result values - -The DB-API driver decodes semantic native types directly: - -- `ENUM` -> `decentdb.EnumValue(type_id, label_id)` -- `IPADDR` / `INET` -> `ipaddress` address objects -- `CIDR` -> `ipaddress` network objects -- `DATE`, `TIME`, `TIMESTAMPTZ` -> `datetime.date`, `datetime.time`, and - timezone-aware UTC `datetime.datetime` -- `INTERVAL` -> `decentdb.IntervalValue(months, days, micros)` -- `MACADDR` / `MACADDR8` -> canonical lowercase `str` - -SQLAlchemy `Date`, `Time`, and `DateTime(timezone=True)` now compile to the -native `DATE`, `TIME`, and `TIMESTAMPTZ` column types. - -## Concurrency Model - -DecentDB operates as an embedded database with the following concurrency model: -- **Single Writer**: Only one connection can write to the database at a time. -- **Multiple Readers**: Multiple connections can read simultaneously (Snapshot Isolation). -- **Process Model**: Local on-disk databases coordinate native OS processes through the WAL coordination sidecar when the VFS supports file locks. - -Use `process_coordination="required"` when multi-process safety is required: - -```python -with connect( - "app.ddb", - process_coordination="required", - process_coordination_timeout_ms=30_000, -) as con: - print(con.execute("SELECT * FROM sys.process_coordination").fetchone()) -``` - -## Bounded Write Queue (DDB v3) - -Python now exposes write-queue execution through both low-level C bindings and the DB-API path. - -```python -from decentdb import connect - -with connect( - "queue_demo.ddb", - write_queue_enabled=True, - write_queue_capacity=128, - write_queue_default_timeout_ms=500, - write_queue_group_commit=True, -) as con: - con.execute("CREATE TABLE IF NOT EXISTS events(id INTEGER PRIMARY KEY, payload TEXT)") - con.execute_queued( - "INSERT INTO events(id, payload) VALUES (?, ?)", - (1, "queued"), - timeout_ms=250, - ) - metrics = con.write_queue_metrics() - print(metrics["admitted"], metrics["committed"]) -``` - -`write_queue_default_timeout_ms` can be omitted to use the engine default; pass -`DDB_WRITE_QUEUE_TIMEOUT_DEFAULT` to leave a single `execute_queued` call at the native -default. - -- `write_queue_enabled` - Enables queued writer mode for the connection. -- `write_queue_capacity` - Maximum in-flight queued write entries. -- `write_queue_group_commit` - Enables queue grouping for durable batching behavior. -- `write_queue_max_batch` - Maximum statements per commit group. -- `write_queue_max_group_delay_us` - Maximum delay before a partial batch is forced to commit. -- `write_queue_default_timeout_ms` - Default timeout applied by direct queued API calls when no explicit timeout is passed. - -## Reactive Subscriptions - -Python exposes watch handles for committed-state reactive updates: - -```python -with connect("reactive_demo.ddb") as con: - con.execute("CREATE TABLE IF NOT EXISTS events(id INT64 PRIMARY KEY, payload TEXT)") - watch = con.watch_query("SELECT id, payload FROM events ORDER BY id") - print(watch.next(timeout_ms=1000)) # initial event - - con.execute("INSERT INTO events VALUES (?, ?)", (1, "created")) - print(watch.next(timeout_ms=1000)) # invalidation event - watch.close() -``` - -Use `watch_table`, `watch_range`, `watch_query`, and `change_stream` for table, -range, query, and ordered change-stream events. `Watch.next` returns `None` on -timeout. - -## Benchmarks - -To run the fetch benchmark: -```bash -python benchmarks/bench_fetch.py -``` - -## SQLite Import - -Convert an existing SQLite database file into a DecentDB database file: - -```bash -decentdb-sqlite-import /path/to/input.sqlite /path/to/output.decentdb -``` - -By default, identifiers are normalized to lowercase so you can query without quoting (Postgres-style). - -To preserve original SQLite casing (requires quoting identifiers in SQL): - -```bash -decentdb-sqlite-import --preserve-case /path/to/input.sqlite /path/to/output.decentdb -``` - -To overwrite an existing destination: - -```bash -decentdb-sqlite-import --overwrite /path/to/input.sqlite /path/to/output.decentdb -``` - -Write a machine-readable conversion report: - -```bash -decentdb-sqlite-import /path/to/input.sqlite /path/to/output.decentdb --report-json report.json -``` - -Or to stdout: - -```bash -decentdb-sqlite-import /path/to/input.sqlite /path/to/output.decentdb --report-json - -``` - -## Statement Cache Statistics - -Connections expose `stmt_cache_stats` as a read-only dictionary with: - -- `hits` -- `misses` -- `size` -- `capacity` - -Low hit rates usually mean SQL strings are being built with embedded literals -instead of parameters. Prefer parameterized queries so prepared statements can -be reused. - -On `Connection.close()`, DecentDB emits `decentdb.PerformanceWarning` when the -statement cache sees at least 100 lookups and the hit rate stays below 50%. diff --git a/bindings/python/decentdb.egg-info/SOURCES.txt b/bindings/python/decentdb.egg-info/SOURCES.txt deleted file mode 100644 index 6ed2a69c..00000000 --- a/bindings/python/decentdb.egg-info/SOURCES.txt +++ /dev/null @@ -1,45 +0,0 @@ -README.md -pyproject.toml -decentdb/__init__.py -decentdb/native.py -decentdb.egg-info/PKG-INFO -decentdb.egg-info/SOURCES.txt -decentdb.egg-info/dependency_links.txt -decentdb.egg-info/entry_points.txt -decentdb.egg-info/requires.txt -decentdb.egg-info/top_level.txt -decentdb/tools/__init__.py -decentdb/tools/__main__.py -decentdb/tools/pgbak_import.py -decentdb/tools/sqlite_import.py -decentdb_sqlalchemy/__init__.py -decentdb_sqlalchemy/dialect.py -tests/test_api_coverage.py -tests/test_basic.py -tests/test_cache.py -tests/test_comprehensive.py -tests/test_concurrency_stress.py -tests/test_coverage_decimal.py -tests/test_coverage_gaps.py -tests/test_cross_connection_visibility.py -tests/test_cursor_cache_bounded.py -tests/test_datatypes.py -tests/test_decimal.py -tests/test_edge_cases.py -tests/test_explain_analyze.py -tests/test_fulltext_showcase.py -tests/test_lifecycle_leak_smoke.py -tests/test_memory_leak.py -tests/test_open_close_leak.py -tests/test_pgbak_import.py -tests/test_process_coordination.py -tests/test_relationships.py -tests/test_resource_management.py -tests/test_save_as.py -tests/test_schema_introspection.py -tests/test_sqlalchemy.py -tests/test_sqlite_import.py -tests/test_threading.py -tests/test_types_sqlalchemy.py -tests/test_v2_features.py -tests/test_valgrind_memcheck.py \ No newline at end of file diff --git a/bindings/python/decentdb.egg-info/dependency_links.txt b/bindings/python/decentdb.egg-info/dependency_links.txt deleted file mode 100644 index 8b137891..00000000 --- a/bindings/python/decentdb.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/bindings/python/decentdb.egg-info/entry_points.txt b/bindings/python/decentdb.egg-info/entry_points.txt deleted file mode 100644 index fcb92699..00000000 --- a/bindings/python/decentdb.egg-info/entry_points.txt +++ /dev/null @@ -1,7 +0,0 @@ -[console_scripts] -decentdb-pgbak-import = decentdb.tools.pgbak_import:main -decentdb-sqlite-import = decentdb.tools.sqlite_import:main - -[sqlalchemy.dialects] -decentdb = decentdb_sqlalchemy.dialect:DecentDBDialect -decentdb.pysql = decentdb_sqlalchemy.dialect:DecentDBDialect diff --git a/bindings/python/decentdb.egg-info/requires.txt b/bindings/python/decentdb.egg-info/requires.txt deleted file mode 100644 index b7b1efa5..00000000 --- a/bindings/python/decentdb.egg-info/requires.txt +++ /dev/null @@ -1,2 +0,0 @@ -SQLAlchemy>=2.0.0 -rich>=13.0.0 diff --git a/bindings/python/decentdb.egg-info/top_level.txt b/bindings/python/decentdb.egg-info/top_level.txt deleted file mode 100644 index fe0ae067..00000000 --- a/bindings/python/decentdb.egg-info/top_level.txt +++ /dev/null @@ -1,2 +0,0 @@ -decentdb -decentdb_sqlalchemy diff --git a/design/adr/0027-bulk-load-api-specification.md b/design/adr/0212-bulk-load-api-specification.md similarity index 100% rename from design/adr/0027-bulk-load-api-specification.md rename to design/adr/0212-bulk-load-api-specification.md diff --git a/design/adr/0035-sql-parser-libpg-query.md b/design/adr/0213-sql-parser-libpg-query.md similarity index 100% rename from design/adr/0035-sql-parser-libpg-query.md rename to design/adr/0213-sql-parser-libpg-query.md diff --git a/design/adr/0036-integer-primary-key.md b/design/adr/0214-integer-primary-key.md similarity index 100% rename from design/adr/0036-integer-primary-key.md rename to design/adr/0214-integer-primary-key.md From 1fd3b0f66ba15094bda0b390ac60e2fd2e649252 Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Thu, 6 Aug 2026 10:02:10 -0500 Subject: [PATCH 2/3] Add fuzz testing for row decoding and WAL recovery - Introduced a new Cargo.toml for fuzz testing with dependencies on libfuzzer-sys and decentdb. - Implemented fuzz target for record/row decoding to ensure it does not panic on malformed input. - Created fuzz target for WAL recovery that validates recovery from fuzz-generated WALs without panicking. - Added multiple JSON scenarios for grouped commit operations, including cases for successful commits, crashes after sync, and failures during commit and flush operations. --- .github/workflows/ci.yml | 36 + .github/workflows/coverage-nightly.yml | 47 + .github/workflows/memory-safety-nightly.yml | 35 + .github/workflows/release.yml | 25 + .gitignore | 4 + AGENTS.md | 6 +- Cargo.lock | 38 +- Cargo.toml | 3 +- VERSION | 2 +- benchmarks/rust-baseline/Cargo.lock | 4 +- bindings/README.md | 31 + bindings/dart/dart/pubspec.yaml | 2 +- bindings/dart/examples/console/pubspec.lock | 2 +- .../examples/console_complex/pubspec.lock | 2 +- .../examples/flutter_desktop/pubspec.lock | 2 +- bindings/dart/flutter/android/build.gradle | 2 +- bindings/dart/flutter/example/pubspec.lock | 4 +- bindings/dart/flutter/example/pubspec.yaml | 2 +- .../dart/flutter/ios/decentdb_flutter.podspec | 2 +- bindings/dart/flutter/pubspec.lock | 2 +- bindings/dart/flutter/pubspec.yaml | 2 +- .../dbeaver-extension/META-INF/MANIFEST.MF | 4 +- bindings/java/dbeaver-extension/build.gradle | 2 +- bindings/java/driver/build.gradle | 2 +- .../com/decentdb/jdbc/DecentDBDriver.java | 2 +- bindings/node/decentdb/package-lock.json | 4 +- bindings/node/decentdb/package.json | 2 +- bindings/node/knex-decentdb/package-lock.json | 6 +- bindings/node/knex-decentdb/package.json | 2 +- .../python/.tmp/bench_complex_results.json | 324 +- bindings/python/pyproject.toml | 2 +- bindings/web/package-lock.json | 4 +- bindings/web/package.json | 2 +- crates/decentdb-benchmark/Cargo.toml | 1 + crates/decentdb-cli/Cargo.toml | 1 + crates/decentdb-migrate/Cargo.toml | 1 + crates/decentdb/Cargo.toml | 2 + crates/decentdb/src/db.rs | 12565 +--- crates/decentdb/src/db/branch_ops.rs | 220 + crates/decentdb/src/db/pragmas.rs | 426 + crates/decentdb/src/db/prepared_fast_paths.rs | 2162 + crates/decentdb/src/db/reactive_ops.rs | 151 + crates/decentdb/src/db/sync_ops.rs | 3779 + crates/decentdb/src/exec/bench_queries.rs | 6323 ++ crates/decentdb/src/exec/codec.rs | 1920 + crates/decentdb/src/exec/deferred.rs | 1218 + crates/decentdb/src/exec/evaluate.rs | 1125 + crates/decentdb/src/exec/grouped.rs | 572 + crates/decentdb/src/exec/indexes.rs | 833 + crates/decentdb/src/exec/joins.rs | 2665 + crates/decentdb/src/exec/manifest.rs | 861 + crates/decentdb/src/exec/mod.rs | 58305 +++------------- crates/decentdb/src/exec/paged_tables.rs | 2158 + crates/decentdb/src/exec/runtime_eval.rs | 2901 + crates/decentdb/src/exec/runtime_keys.rs | 1600 + crates/decentdb/src/exec/simple_queries.rs | 12738 ++++ crates/decentdb/src/exec/table_data.rs | 887 + crates/decentdb/src/lib.rs | 27 + crates/decentdb/src/planner/mod.rs | 6 +- crates/decentdb/src/record/row.rs | 21 + crates/libpg_query_sys/Cargo.toml | 1 + deny.toml | 41 + design/FUTURE_WINS.md | 6 +- design/SPEC.md | 2 +- design/VERSIONING_GUIDE.md | 13 +- .../0049-constraint-index-deduplication.md | 2 +- .../adr/0071-sql-null-three-valued-logic.md | 2 +- design/adr/0074-exists-subquery-surface-v0.md | 2 +- design/adr/0078-cte-non-recursive-scope-v0.md | 2 +- design/adr/0085-after-triggers-v0.md | 2 +- design/adr/0107-recursive-cte-execution.md | 2 +- .../adr/0123-phase1-table-btree-foundation.md | 2 +- .../0203-compact-dense-runtime-int64-index.md | 2 +- .../adr/0205-compact-paged-row-directory.md | 2 +- .../0207-inline-runtime-encoded-index-keys.md | 2 +- design/adr/0214-integer-primary-key.md | 2 +- docs/about/changelog.md | 68 + docs/user-guide/benchmarks.md | 2 +- fuzz/.gitignore | 3 + fuzz/Cargo.lock | 1390 + fuzz/Cargo.toml | 32 + fuzz/fuzz_targets/record_decode.rs | 19 + fuzz/fuzz_targets/wal_recovery.rs | 102 + scripts/bump_version.sh | 17 + scripts/do-pre-commit-checks.py | 18 + tests/bindings/dart/pubspec.lock | 2 +- .../grouped_commit_fault_injection_plan.md | 18 +- .../scenarios/grouped_commit_all_commit.json | 19 + .../grouped_commit_crash_after_sync.json | 19 + ...uped_commit_fail_during_second_commit.json | 27 + ...grouped_commit_fail_sync_before_flush.json | 21 + 91 files changed, 58873 insertions(+), 57046 deletions(-) create mode 100644 .github/workflows/coverage-nightly.yml create mode 100644 crates/decentdb/src/db/branch_ops.rs create mode 100644 crates/decentdb/src/db/pragmas.rs create mode 100644 crates/decentdb/src/db/prepared_fast_paths.rs create mode 100644 crates/decentdb/src/db/reactive_ops.rs create mode 100644 crates/decentdb/src/db/sync_ops.rs create mode 100644 crates/decentdb/src/exec/bench_queries.rs create mode 100644 crates/decentdb/src/exec/codec.rs create mode 100644 crates/decentdb/src/exec/deferred.rs create mode 100644 crates/decentdb/src/exec/evaluate.rs create mode 100644 crates/decentdb/src/exec/grouped.rs create mode 100644 crates/decentdb/src/exec/indexes.rs create mode 100644 crates/decentdb/src/exec/joins.rs create mode 100644 crates/decentdb/src/exec/manifest.rs create mode 100644 crates/decentdb/src/exec/paged_tables.rs create mode 100644 crates/decentdb/src/exec/runtime_eval.rs create mode 100644 crates/decentdb/src/exec/runtime_keys.rs create mode 100644 crates/decentdb/src/exec/simple_queries.rs create mode 100644 crates/decentdb/src/exec/table_data.rs create mode 100644 deny.toml create mode 100644 fuzz/.gitignore create mode 100644 fuzz/Cargo.lock create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/fuzz_targets/record_decode.rs create mode 100644 fuzz/fuzz_targets/wal_recovery.rs create mode 100644 tests/harness/scenarios/grouped_commit_all_commit.json create mode 100644 tests/harness/scenarios/grouped_commit_crash_after_sync.json create mode 100644 tests/harness/scenarios/grouped_commit_fail_during_second_commit.json create mode 100644 tests/harness/scenarios/grouped_commit_fail_sync_before_flush.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b814016..82ec086b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,42 @@ jobs: - name: Vendored Header Drift Check run: python3 scripts/check_vendored_headers.py + msrv: + if: github.event_name != 'push' || !endsWith(github.actor, '[bot]') + runs-on: ubuntu-latest + env: + CI: "true" + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install Rust (MSRV) + # Keep in sync with rust-version in the workspace Cargo.toml. + uses: dtolnay/rust-toolchain@1.88.0 + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + + - name: MSRV Check + run: cargo check --workspace --all-targets + + supply-chain: + if: github.event_name != 'push' || !endsWith(github.actor, '[bot]') + runs-on: ubuntu-latest + env: + CI: "true" + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install cargo-deny + uses: taiki-e/install-action@v2 + with: + tool: cargo-deny + + - name: cargo deny check + run: cargo deny check + test: if: github.event_name != 'push' || !endsWith(github.actor, '[bot]') runs-on: ubuntu-latest diff --git a/.github/workflows/coverage-nightly.yml b/.github/workflows/coverage-nightly.yml new file mode 100644 index 00000000..3381b66c --- /dev/null +++ b/.github/workflows/coverage-nightly.yml @@ -0,0 +1,47 @@ +name: Coverage Nightly + +on: + schedule: + # Run after the memory-safety nightly. + - cron: "0 6 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: coverage-${{ github.ref }} + cancel-in-progress: true + +jobs: + llvm-cov: + name: Rust Coverage (llvm-cov) + runs-on: ubuntu-latest + timeout-minutes: 120 + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: llvm-tools-preview + + - name: Install cargo-llvm-cov and cargo-nextest + uses: taiki-e/install-action@v2 + with: + tool: cargo-llvm-cov,cargo-nextest + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + + - name: Generate lcov coverage report + run: cargo cov-ci + + - name: Upload lcov report + uses: actions/upload-artifact@v4 + with: + name: lcov-info + path: lcov.info + if-no-files-found: error diff --git a/.github/workflows/memory-safety-nightly.yml b/.github/workflows/memory-safety-nightly.yml index 19dc3289..79eadba2 100644 --- a/.github/workflows/memory-safety-nightly.yml +++ b/.github/workflows/memory-safety-nightly.yml @@ -95,6 +95,41 @@ jobs: - name: Run WAL corruption fuzz run: ./target/release/wal_fuzz + coverage-guided-fuzz: + name: Coverage-Guided Fuzz (${{ matrix.target }}) + runs-on: ubuntu-latest + # libFuzzer runs are time-boxed per target; the budget covers a cold + # instrumented engine build plus the fuzz windows below. + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + target: [wal_recovery, record_decode] + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install Rust nightly + uses: dtolnay/rust-toolchain@nightly + + - name: Install cargo-fuzz + run: cargo install cargo-fuzz --locked + + - name: Run libFuzzer target + working-directory: fuzz + run: >- + cargo +nightly fuzz run ${{ matrix.target }} + -- -max_total_time=600 -rss_limit_mb=4096 + + - name: Upload crash artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: fuzz-artifacts-${{ matrix.target }} + path: fuzz/artifacts/${{ matrix.target }}/ + if-no-files-found: ignore + python-leak-regressions: name: Python Leak Regressions runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 232c0b00..51f6cd43 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -114,6 +114,7 @@ jobs: uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt + targets: wasm32-unknown-unknown - name: Set up Python uses: actions/setup-python@v5 @@ -174,6 +175,13 @@ jobs: - name: Short Crash Harness run: python tests/harness/runner.py tests/harness/scenarios/short_crash.json + - name: Grouped Commit Fault-Injection Harnesses + run: | + python tests/harness/runner.py tests/harness/scenarios/grouped_commit_all_commit.json + python tests/harness/runner.py tests/harness/scenarios/grouped_commit_fail_during_second_commit.json + python tests/harness/runner.py tests/harness/scenarios/grouped_commit_fail_sync_before_flush.json + python tests/harness/runner.py tests/harness/scenarios/grouped_commit_crash_after_sync.json + - name: Build cdylib run: cargo build -p decentdb @@ -205,6 +213,23 @@ jobs: dart run smoke.dart cd ../../.. + - name: Web Binding Smoke (WASM + browser OPFS) + run: | + cargo install wasm-bindgen-cli --version 0.2.114 --locked + cd bindings/web + npm ci + npm run build + cd ../.. + cargo build -p decentdb --target wasm32-unknown-unknown --release + wasm-bindgen target/wasm32-unknown-unknown/release/decentdb.wasm \ + --target web \ + --out-dir bindings/web/dist \ + --out-name decentdb_wasm + cd bindings/web + npm run browser:install:ci + npm run browser:smoke:ci + cd ../.. + - name: Storage Soak Harness run: python tests/harness/runner.py tests/harness/scenarios/soak_storage.json diff --git a/.gitignore b/.gitignore index 6f51e550..9cf30920 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,7 @@ bindings/dotnet/tests/DecentDB.Tests/TestResults/* bindings/dotnet/tests/DecentDB.EntityFrameworkCore.Tests/TestResults/* benchmarks/rust-baseline/.tmp/* + +# Python packaging artifacts +bindings/python/decentdb.egg-info/ +*.egg-info/ diff --git a/AGENTS.md b/AGENTS.md index 5a2cc347..6c21ce26 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,8 +28,10 @@ DecentDB is a Rust-native embedded relational database. The goal is a world-clas | `crates/decentdb-migrate/` | On-disk format migration parser | | `crates/decentdb-benchmark/` | Rust-native benchmark runner | | `crates/libpg_query_sys/` | C SQL parser wrapper (`pg_query` dep) | -| `bindings/{dotnet,python,go,java,node,dart}/` | Language bindings | -| `tests/bindings/` | Smoke tests for all language bindings | +| `fuzz/` | cargo-fuzz crate (independent workspace) with libFuzzer targets for WAL recovery and record decode; engine shims behind the `fuzz-internals` feature | +| `bindings/{dotnet,python,go,java,node,dart}/` | Language bindings (thin wrappers over the C ABI) | +| `bindings/web/` | Web binding (TypeScript + WASM/OPFS; separate wasm protocol stack, not the C ABI) | +| `tests/bindings/` | Smoke tests for all language bindings, including `c/` (C ABI smoke) and `web/` (Playwright browser suites) | | `tests/harness/` | Python test harness (runner + scenarios + datasets) | | `include/decentdb.h` | Stable C ABI header | | `scripts/` | Repo automation (benchmark charts, pre-commit checks, etc.) | diff --git a/Cargo.lock b/Cargo.lock index bd50a363..03dc8c4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,9 +109,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arbitrary" @@ -773,9 +773,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] @@ -831,7 +831,7 @@ dependencies = [ [[package]] name = "decentdb" -version = "2.17.0" +version = "2.17.1" dependencies = [ "base64 0.22.1", "chacha20", @@ -850,7 +850,7 @@ dependencies = [ "miniz_oxide", "mlua", "proptest", - "rand 0.8.5", + "rand 0.8.7", "regex", "rusqlite", "serde", @@ -867,7 +867,7 @@ dependencies = [ [[package]] name = "decentdb-benchmark" -version = "2.17.0" +version = "2.17.1" dependencies = [ "anyhow", "clap", @@ -881,7 +881,7 @@ dependencies = [ [[package]] name = "decentdb-cli" -version = "2.17.0" +version = "2.17.1" dependencies = [ "anyhow", "clap", @@ -895,7 +895,7 @@ dependencies = [ [[package]] name = "decentdb-migrate" -version = "2.17.0" +version = "2.17.1" dependencies = [ "anyhow", "clap", @@ -1859,7 +1859,7 @@ dependencies = [ [[package]] name = "libpg_query_sys" -version = "2.17.0" +version = "2.17.1" dependencies = [ "pg_query", ] @@ -2318,7 +2318,7 @@ dependencies = [ "bit-vec", "bitflags", "num-traits", - "rand 0.9.2", + "rand 0.9.5", "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", @@ -2434,7 +2434,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.5", "ring", "rustc-hash 2.1.1", "rustls", @@ -2499,9 +2499,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2510,9 +2510,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -2748,7 +2748,7 @@ dependencies = [ "borsh", "bytes", "num-traits", - "rand 0.8.5", + "rand 0.8.7", "rkyv", "serde", "serde_json", @@ -2828,9 +2828,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", diff --git a/Cargo.toml b/Cargo.toml index 4dec7415..d0c29596 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,9 @@ exclude = [ resolver = "2" [workspace.package] -version = "2.17.0" +version = "2.17.1" edition = "2021" +rust-version = "1.88" authors = ["Steven Hildreth"] license = "Apache-2.0" repository = "https://github.com/sphildreth/decentdb" diff --git a/VERSION b/VERSION index d76bd2ba..3f8eb714 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.17.0 +2.17.1 diff --git a/benchmarks/rust-baseline/Cargo.lock b/benchmarks/rust-baseline/Cargo.lock index 2be5fe3d..2c0743b5 100644 --- a/benchmarks/rust-baseline/Cargo.lock +++ b/benchmarks/rust-baseline/Cargo.lock @@ -642,7 +642,7 @@ dependencies = [ [[package]] name = "decentdb" -version = "2.17.0" +version = "2.17.1" dependencies = [ "base64", "chacha20", @@ -1474,7 +1474,7 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libpg_query_sys" -version = "2.17.0" +version = "2.17.1" dependencies = [ "pg_query", ] diff --git a/bindings/README.md b/bindings/README.md index 61d297e0..3c17d2d0 100644 --- a/bindings/README.md +++ b/bindings/README.md @@ -28,10 +28,12 @@ engine: - `tests/bindings/python/` - `tests/bindings/dotnet/` +- `tests/bindings/c/` - `tests/bindings/go/` - `tests/bindings/java/` - `tests/bindings/node/` - `tests/bindings/dart/` +- `tests/bindings/web/` (Playwright browser suites; WASM/OPFS, not the C ABI) The following package trees live under `bindings/`: @@ -46,11 +48,39 @@ The following package trees live under `bindings/`: extension - `bindings/node/`: low-level `decentdb/` package and `knex-decentdb/` - `bindings/dart/`: Dart package, native glue, scripts, and examples +- `bindings/web/`: TypeScript + WASM/OPFS browser binding (`@decentdb/web`); a + separate wasm protocol stack rather than a C ABI wrapper All in-tree bindings consume the C ABI's semantic value tags for `ENUM`, `IPADDR`, `CIDR`, `DATE`, `TIME`, `TIMESTAMPTZ`, `INTERVAL`, and `MACADDR`; language-specific result shapes are documented in `docs/api/bindings-matrix.md`. +## Feature coverage matrix + +Higher-level engine capabilities are not yet uniform across bindings. This +matrix is the authoritative inventory (verified against the binding sources, +not just package docs); keep it updated when a binding gains a surface. + +| Capability | Python | .NET | Go | Java | Node | Dart | Web (wasm) | +|---|---|---|---|---|---|---|---| +| Core C ABI: exec/query, prepared statements, transactions | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (separate wasm protocol, not the C ABI) | +| Write queue (queued writes, metrics) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | +| Watch / change streams | ✓ | — | ✓ | — | — | — | — | +| Branch, named snapshots, diff/restore | — | — | ✓ | — | — | ✓ | — (disabled by design; capability metadata reports `branchSnapshots=false`) | +| Extension lifecycle JSON APIs | — | — | — | — | — | — | — | + +Notes: + +- Java's JNI shim maps all C ABI value tags (including `UUID`, `GEOMETRY`, + `GEOGRAPHY`, `ENUM`, `IPADDR`, `CIDR`, `DATE`, `TIME`, `TIMESTAMPTZ`, + `INTERVAL`, `MACADDR`) to JDBC type codes; its gaps are the higher-level + feature surfaces above, not value decoding. +- The web binding is a separate TypeScript + WASM/OPFS protocol stack; C ABI + feature rows do not apply to it directly. +- Extension lifecycle APIs (`ddb_extension_*_json`) are currently only + declared in the C ABI header and exercised from Rust/CLI; no binding wraps + them yet. + Benchmark entry points: - Go: `go run ./benchmarks/bench_fetch/main.go` (from `bindings/go/decentdb-go`) @@ -76,3 +106,4 @@ The following language integration trees now live in-repo under `bindings/`: - `bindings/java/` - `bindings/node/` - `bindings/dart/` +- `bindings/web/` diff --git a/bindings/dart/dart/pubspec.yaml b/bindings/dart/dart/pubspec.yaml index e88734b6..89340525 100644 --- a/bindings/dart/dart/pubspec.yaml +++ b/bindings/dart/dart/pubspec.yaml @@ -1,6 +1,6 @@ name: decentdb description: Dart FFI bindings for the Rust DecentDB C ABI. -version: 2.17.0 +version: 2.17.1 repository: https://github.com/sphildreth/decentdb homepage: https://github.com/sphildreth/decentdb/tree/main/bindings/dart diff --git a/bindings/dart/examples/console/pubspec.lock b/bindings/dart/examples/console/pubspec.lock index 032b2834..fa01c451 100644 --- a/bindings/dart/examples/console/pubspec.lock +++ b/bindings/dart/examples/console/pubspec.lock @@ -7,7 +7,7 @@ packages: path: "../../dart" relative: true source: path - version: "2.17.0" + version: "2.17.1" ffi: dependency: transitive description: diff --git a/bindings/dart/examples/console_complex/pubspec.lock b/bindings/dart/examples/console_complex/pubspec.lock index 032b2834..fa01c451 100644 --- a/bindings/dart/examples/console_complex/pubspec.lock +++ b/bindings/dart/examples/console_complex/pubspec.lock @@ -7,7 +7,7 @@ packages: path: "../../dart" relative: true source: path - version: "2.17.0" + version: "2.17.1" ffi: dependency: transitive description: diff --git a/bindings/dart/examples/flutter_desktop/pubspec.lock b/bindings/dart/examples/flutter_desktop/pubspec.lock index 032b2834..fa01c451 100644 --- a/bindings/dart/examples/flutter_desktop/pubspec.lock +++ b/bindings/dart/examples/flutter_desktop/pubspec.lock @@ -7,7 +7,7 @@ packages: path: "../../dart" relative: true source: path - version: "2.17.0" + version: "2.17.1" ffi: dependency: transitive description: diff --git a/bindings/dart/flutter/android/build.gradle b/bindings/dart/flutter/android/build.gradle index e41f03a7..2452d7d2 100644 --- a/bindings/dart/flutter/android/build.gradle +++ b/bindings/dart/flutter/android/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'dev.decentdb.decentdb_flutter' -version = '2.17.0' +version = '2.17.1' android { namespace 'dev.decentdb.decentdb_flutter' diff --git a/bindings/dart/flutter/example/pubspec.lock b/bindings/dart/flutter/example/pubspec.lock index d2e24df7..9bfccab4 100644 --- a/bindings/dart/flutter/example/pubspec.lock +++ b/bindings/dart/flutter/example/pubspec.lock @@ -71,14 +71,14 @@ packages: path: "../../dart" relative: true source: path - version: "2.17.0" + version: "2.17.1" decentdb_flutter: dependency: "direct main" description: path: ".." relative: true source: path - version: "2.17.0" + version: "2.17.1" fake_async: dependency: transitive description: diff --git a/bindings/dart/flutter/example/pubspec.yaml b/bindings/dart/flutter/example/pubspec.yaml index 5deb3aa9..eb939d1c 100644 --- a/bindings/dart/flutter/example/pubspec.yaml +++ b/bindings/dart/flutter/example/pubspec.yaml @@ -1,7 +1,7 @@ name: decentdb_flutter_example description: Reference Flutter mobile app for DecentDB. publish_to: none -version: 2.17.0 +version: 2.17.1 environment: sdk: ^3.0.0 diff --git a/bindings/dart/flutter/ios/decentdb_flutter.podspec b/bindings/dart/flutter/ios/decentdb_flutter.podspec index 0b7f58fe..e44cce6c 100644 --- a/bindings/dart/flutter/ios/decentdb_flutter.podspec +++ b/bindings/dart/flutter/ios/decentdb_flutter.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'decentdb_flutter' - s.version = '2.17.0' + s.version = '2.17.1' s.summary = 'Flutter mobile integration helpers for DecentDB.' s.description = 'Provides Flutter registration and native artifact wiring for the DecentDB Dart FFI package.' s.homepage = 'https://github.com/sphildreth/decentdb' diff --git a/bindings/dart/flutter/pubspec.lock b/bindings/dart/flutter/pubspec.lock index cda1b9ec..0b9a88b4 100644 --- a/bindings/dart/flutter/pubspec.lock +++ b/bindings/dart/flutter/pubspec.lock @@ -71,7 +71,7 @@ packages: path: "../dart" relative: true source: path - version: "2.17.0" + version: "2.17.1" fake_async: dependency: transitive description: diff --git a/bindings/dart/flutter/pubspec.yaml b/bindings/dart/flutter/pubspec.yaml index 132cf703..02113939 100644 --- a/bindings/dart/flutter/pubspec.yaml +++ b/bindings/dart/flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: decentdb_flutter description: Flutter mobile integration helpers for the DecentDB Dart FFI package. -version: 2.17.0 +version: 2.17.1 publish_to: none repository: https://github.com/sphildreth/decentdb homepage: https://github.com/sphildreth/decentdb/tree/main/bindings/dart/flutter diff --git a/bindings/java/dbeaver-extension/META-INF/MANIFEST.MF b/bindings/java/dbeaver-extension/META-INF/MANIFEST.MF index d687bc1a..55667bde 100644 --- a/bindings/java/dbeaver-extension/META-INF/MANIFEST.MF +++ b/bindings/java/dbeaver-extension/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: DecentDB DBeaver Extension Bundle-SymbolicName: org.jkiss.dbeaver.ext.decentdb;singleton:=true -Bundle-Version: 2.17.0 +Bundle-Version: 2.17.1 Bundle-Activator: org.jkiss.dbeaver.ext.decentdb.DecentDBActivator Bundle-Vendor: DecentDB Contributors Require-Bundle: org.eclipse.core.runtime, @@ -11,5 +11,5 @@ Require-Bundle: org.eclipse.core.runtime, org.jkiss.dbeaver.ext.generic Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ClassPath: ., - lib/decentdb-jdbc-2.17.0.jar + lib/decentdb-jdbc-2.17.1.jar Export-Package: org.jkiss.dbeaver.ext.decentdb.model diff --git a/bindings/java/dbeaver-extension/build.gradle b/bindings/java/dbeaver-extension/build.gradle index 9c58ad4d..98a76fef 100644 --- a/bindings/java/dbeaver-extension/build.gradle +++ b/bindings/java/dbeaver-extension/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'org.jkiss.dbeaver.ext' -version = '2.17.0' +version = '2.17.1' java { sourceCompatibility = JavaVersion.VERSION_21 diff --git a/bindings/java/driver/build.gradle b/bindings/java/driver/build.gradle index cb2ae38a..c47b38ce 100644 --- a/bindings/java/driver/build.gradle +++ b/bindings/java/driver/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'com.decentdb' -version = '2.17.0' +version = '2.17.1' def repoRoot = file("${rootProject.projectDir}/../..") def nativeLibDirPath = project.findProperty('nativeLibDir') ?: diff --git a/bindings/java/driver/src/main/java/com/decentdb/jdbc/DecentDBDriver.java b/bindings/java/driver/src/main/java/com/decentdb/jdbc/DecentDBDriver.java index 349f1891..3b5b3436 100644 --- a/bindings/java/driver/src/main/java/com/decentdb/jdbc/DecentDBDriver.java +++ b/bindings/java/driver/src/main/java/com/decentdb/jdbc/DecentDBDriver.java @@ -28,7 +28,7 @@ public final class DecentDBDriver implements Driver { public static final String URL_PREFIX = "jdbc:decentdb:"; - public static final String DRIVER_VERSION = "2.17.0"; + public static final String DRIVER_VERSION = "2.17.1"; public static final int DRIVER_MAJOR_VERSION = 1; public static final int DRIVER_MINOR_VERSION = 8; diff --git a/bindings/node/decentdb/package-lock.json b/bindings/node/decentdb/package-lock.json index a983d3b5..52a8b632 100644 --- a/bindings/node/decentdb/package-lock.json +++ b/bindings/node/decentdb/package-lock.json @@ -1,12 +1,12 @@ { "name": "decentdb-native", - "version": "2.17.0", + "version": "2.17.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "decentdb-native", - "version": "2.17.0", + "version": "2.17.1", "devDependencies": { "node-gyp": "^12.2.0" } diff --git a/bindings/node/decentdb/package.json b/bindings/node/decentdb/package.json index 8e7c22c1..e7a6a95f 100644 --- a/bindings/node/decentdb/package.json +++ b/bindings/node/decentdb/package.json @@ -1,6 +1,6 @@ { "name": "decentdb-native", - "version": "2.17.0", + "version": "2.17.1", "private": true, "description": "DecentDB Node.js native addon (N-API) + thin JS wrapper", "main": "index.js", diff --git a/bindings/node/knex-decentdb/package-lock.json b/bindings/node/knex-decentdb/package-lock.json index 5fdc9119..2942fc33 100644 --- a/bindings/node/knex-decentdb/package-lock.json +++ b/bindings/node/knex-decentdb/package-lock.json @@ -1,12 +1,12 @@ { "name": "knex-decentdb", - "version": "2.17.0", + "version": "2.17.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "knex-decentdb", - "version": "2.17.0", + "version": "2.17.1", "dependencies": { "decentdb-native": "file:../decentdb" }, @@ -16,7 +16,7 @@ }, "../decentdb": { "name": "decentdb-native", - "version": "2.17.0", + "version": "2.17.1", "devDependencies": { "node-gyp": "^12.2.0" } diff --git a/bindings/node/knex-decentdb/package.json b/bindings/node/knex-decentdb/package.json index 41f5f06e..f0786b90 100644 --- a/bindings/node/knex-decentdb/package.json +++ b/bindings/node/knex-decentdb/package.json @@ -1,6 +1,6 @@ { "name": "knex-decentdb", - "version": "2.17.0", + "version": "2.17.1", "private": true, "description": "Knex client/dialect for DecentDB", "main": "index.js", diff --git a/bindings/python/.tmp/bench_complex_results.json b/bindings/python/.tmp/bench_complex_results.json index 836d5a48..d93c1bea 100644 --- a/bindings/python/.tmp/bench_complex_results.json +++ b/bindings/python/.tmp/bench_complex_results.json @@ -11,136 +11,136 @@ "comparisons": { "complex": { "aggregate_p50_ms": { - "decentdb": 0.030377, - "decentdb_vs_sqlite": 5.227499569781449, + "decentdb": 0.028414, + "decentdb_vs_sqlite": 4.79885154534707, "direction": "lower_is_better", - "sqlite": 0.005811, + "sqlite": 0.005921, "winner": "sqlite" }, "aggregate_p95_ms": { - "decentdb": 0.049834, - "decentdb_vs_sqlite": 1.0626039489956929, + "decentdb": 0.047369, + "decentdb_vs_sqlite": 1.195764123794618, "direction": "lower_is_better", - "sqlite": 0.046898, + "sqlite": 0.039614, "winner": "sqlite" }, "catalog_insert_s": { - "decentdb": 0.0013478309992933646, - "decentdb_vs_sqlite": 0.8216026942948889, + "decentdb": 0.0015343609848059714, + "decentdb_vs_sqlite": 1.2572493794446347, "direction": "lower_is_better", - "sqlite": 0.0016404899943154305, - "winner": "decentdb" + "sqlite": 0.0012204110098537058, + "winner": "sqlite" }, "delete_p50_ms": { - "decentdb": 0.030598, - "decentdb_vs_sqlite": 1.7051939366919302, + "decentdb": 0.02681, + "decentdb_vs_sqlite": 1.7756142790913307, "direction": "lower_is_better", - "sqlite": 0.017944, + "sqlite": 0.015099, "winner": "sqlite" }, "delete_p95_ms": { - "decentdb": 0.039795, - "decentdb_vs_sqlite": 1.3364790435249865, + "decentdb": 0.038032, + "decentdb_vs_sqlite": 1.9833124739257408, "direction": "lower_is_better", - "sqlite": 0.029776, + "sqlite": 0.019176, "winner": "sqlite" }, "history_p50_ms": { - "decentdb": 0.007254, - "decentdb_vs_sqlite": 1.4143107818288165, + "decentdb": 0.007394, + "decentdb_vs_sqlite": 1.782545805207329, "direction": "lower_is_better", - "sqlite": 0.005129, + "sqlite": 0.004148, "winner": "sqlite" }, "history_p95_ms": { - "decentdb": 0.01078, - "decentdb_vs_sqlite": 0.970209702097021, + "decentdb": 0.011021, + "decentdb_vs_sqlite": 1.237480350325623, "direction": "lower_is_better", - "sqlite": 0.011111, - "winner": "decentdb" + "sqlite": 0.008906, + "winner": "sqlite" }, "join_p50_ms": { - "decentdb": 0.063399, - "decentdb_vs_sqlite": 2.019140736966145, + "decentdb": 0.06377, + "decentdb_vs_sqlite": 2.1517748684032934, "direction": "lower_is_better", - "sqlite": 0.031399, + "sqlite": 0.029636, "winner": "sqlite" }, "join_p95_ms": { - "decentdb": 0.09068, - "decentdb_vs_sqlite": 2.0077493634451455, + "decentdb": 0.091181, + "decentdb_vs_sqlite": 2.447812080536913, "direction": "lower_is_better", - "sqlite": 0.045165, + "sqlite": 0.03725, "winner": "sqlite" }, "orders_insert_rps": { - "decentdb": 119128.27195457293, - "decentdb_vs_sqlite": 0.3556588194498997, + "decentdb": 467016.09162784467, + "decentdb_vs_sqlite": 1.1178297659763277, "direction": "higher_is_better", - "sqlite": 334950.98515715025, - "winner": "sqlite" + "sqlite": 417788.20518341317, + "winner": "decentdb" }, "point_lookup_p50_ms": { - "decentdb": 0.002244, - "decentdb_vs_sqlite": 0.41787709497206704, + "decentdb": 0.002274, + "decentdb_vs_sqlite": 0.436468330134357, "direction": "lower_is_better", - "sqlite": 0.00537, + "sqlite": 0.00521, "winner": "decentdb" }, "point_lookup_p95_ms": { - "decentdb": 0.002544, - "decentdb_vs_sqlite": 0.41966347740019794, + "decentdb": 0.002795, + "decentdb_vs_sqlite": 0.3457019171304886, "direction": "lower_is_better", - "sqlite": 0.006062, + "sqlite": 0.008085, "winner": "decentdb" }, "range_scan_p50_ms": { - "decentdb": 0.01027, - "decentdb_vs_sqlite": 1.9340866290018832, + "decentdb": 0.010771, + "decentdb_vs_sqlite": 1.7975634178905207, "direction": "lower_is_better", - "sqlite": 0.00531, + "sqlite": 0.005992, "winner": "sqlite" }, "range_scan_p95_ms": { - "decentdb": 0.015339, - "decentdb_vs_sqlite": 0.5349445490688428, + "decentdb": 0.016952, + "decentdb_vs_sqlite": 0.5099572829552974, "direction": "lower_is_better", - "sqlite": 0.028674, + "sqlite": 0.033242, "winner": "decentdb" }, "report_query_s": { - "decentdb": 4.4002998038195074e-05, - "decentdb_vs_sqlite": 0.29902280181830715, + "decentdb": 6.251799641177058e-05, + "decentdb_vs_sqlite": 0.5312045604268426, "direction": "lower_is_better", - "sqlite": 0.0001471559953643009, + "sqlite": 0.00011769100092351437, "winner": "decentdb" }, "table_scan_p50_ms": { - "decentdb": 0.002795, - "decentdb_vs_sqlite": 0.5071674832153874, + "decentdb": 0.002665, + "decentdb_vs_sqlite": 0.7964734010759115, "direction": "lower_is_better", - "sqlite": 0.005511, + "sqlite": 0.003346, "winner": "decentdb" }, "table_scan_p95_ms": { - "decentdb": 0.003006, - "decentdb_vs_sqlite": 0.5155204939118504, + "decentdb": 0.002915, + "decentdb_vs_sqlite": 0.8530875036581798, "direction": "lower_is_better", - "sqlite": 0.005831, + "sqlite": 0.003417, "winner": "decentdb" }, "update_p50_ms": { - "decentdb": 0.004579, - "decentdb_vs_sqlite": 1.0703599812996727, + "decentdb": 0.004028, + "decentdb_vs_sqlite": 1.2107003306281936, "direction": "lower_is_better", - "sqlite": 0.004278, + "sqlite": 0.003327, "winner": "sqlite" }, "update_p95_ms": { - "decentdb": 0.109946, - "decentdb_vs_sqlite": 15.699842924460947, + "decentdb": 0.110006, + "decentdb_vs_sqlite": 20.10711021751051, "direction": "lower_is_better", - "sqlite": 0.007003, + "sqlite": 0.005471, "winner": "sqlite" } }, @@ -153,24 +153,24 @@ "winner": "tie" }, "movie_bulk_load_rps": { - "decentdb": 111449.98572717741, - "decentdb_vs_sqlite": 0.7908184461133867, + "decentdb": 113760.8644209266, + "decentdb_vs_sqlite": 0.7733373161456712, "direction": "higher_is_better", - "sqlite": 140929.9268054729, + "sqlite": 147103.80844921985, "winner": "sqlite" }, "movie_bulk_load_s": { - "decentdb": 0.38672055199276656, - "decentdb_vs_sqlite": 1.2645127398262799, + "decentdb": 0.3788649129855912, + "decentdb_vs_sqlite": 1.2930967885838225, "direction": "lower_is_better", - "sqlite": 0.30582574600703083, + "sqlite": 0.29299037499004044, "winner": "sqlite" }, "movie_busiest_people_s": { - "decentdb": 0.00038047500129323453, - "decentdb_vs_sqlite": 0.197371494013998, + "decentdb": 0.0004071650037076324, + "decentdb_vs_sqlite": 0.21213095592601444, "direction": "lower_is_better", - "sqlite": 0.001927709992742166, + "sqlite": 0.0019194039923604578, "winner": "decentdb" }, "movie_busiest_people_s_rows": { @@ -181,17 +181,17 @@ "winner": "tie" }, "movie_checkpoint_after_mutations_s": { - "decentdb": 0.014619162000599317, - "decentdb_vs_sqlite": 0.5552231874755827, + "decentdb": 0.01721704500960186, + "decentdb_vs_sqlite": 0.6846439620790864, "direction": "lower_is_better", - "sqlite": 0.026330243999836966, + "sqlite": 0.025147442996967584, "winner": "decentdb" }, "movie_checkpoint_s": { - "decentdb": 0.03230415099824313, - "decentdb_vs_sqlite": 0.5631420116336481, + "decentdb": 0.033217706019058824, + "decentdb_vs_sqlite": 0.6732579161157612, "direction": "lower_is_better", - "sqlite": 0.05736412899568677, + "sqlite": 0.04933875298593193, "winner": "decentdb" }, "movie_delete_cascade_rows": { @@ -202,10 +202,10 @@ "winner": "tie" }, "movie_delete_cascade_s": { - "decentdb": 0.016309216007357463, - "decentdb_vs_sqlite": 1.4430836287180877, + "decentdb": 0.018880558025557548, + "decentdb_vs_sqlite": 1.520318684798407, "direction": "lower_is_better", - "sqlite": 0.011301643011393026, + "sqlite": 0.012418816011631861, "winner": "sqlite" }, "movie_final_file_size_bytes": { @@ -223,17 +223,17 @@ "winner": "tie" }, "movie_point_reads_s": { - "decentdb": 0.008052481003687717, - "decentdb_vs_sqlite": 1.1524896637811044, + "decentdb": 0.008146423992002383, + "decentdb_vs_sqlite": 1.0951605429101683, "direction": "lower_is_better", - "sqlite": 0.006987030996242538, + "sqlite": 0.00743856601184234, "winner": "sqlite" }, "movie_tag_search_s": { - "decentdb": 0.0007193310011643916, - "decentdb_vs_sqlite": 1.9979973160931463, + "decentdb": 0.0007301209843717515, + "decentdb_vs_sqlite": 2.0676695621069046, "direction": "lower_is_better", - "sqlite": 0.0003600260097300634, + "sqlite": 0.00035311299143359065, "winner": "sqlite" }, "movie_tag_search_s_rows": { @@ -244,10 +244,10 @@ "winner": "tie" }, "movie_top_rated_s": { - "decentdb": 0.0005835150077473372, - "decentdb_vs_sqlite": 0.5094688074526784, + "decentdb": 0.00032629299676045775, + "decentdb_vs_sqlite": 0.23712912584636875, "direction": "lower_is_better", - "sqlite": 0.0011453400074969977, + "sqlite": 0.0013760139991063625, "winner": "decentdb" }, "movie_top_rated_s_rows": { @@ -265,25 +265,25 @@ "winner": "tie" }, "movie_update_batch_s": { - "decentdb": 0.018434918005368672, - "decentdb_vs_sqlite": 2.33504283655138, + "decentdb": 0.01715468699694611, + "decentdb_vs_sqlite": 2.355357025582051, "direction": "lower_is_better", - "sqlite": 0.00789489499584306, + "sqlite": 0.007283263985300437, "winner": "sqlite" }, "movie_vacuum_s": { - "decentdb": 0.011149796991958283, - "decentdb_vs_sqlite": 0.14406117462596227, + "decentdb": 0.010237530019367114, + "decentdb_vs_sqlite": 0.17354194209754123, "direction": "lower_is_better", - "sqlite": 0.07739626600232441, + "sqlite": 0.05899167599272914, "winner": "decentdb" }, "movie_watchlist_s": { - "decentdb": 0.00017649099754635245, - "decentdb_vs_sqlite": 1.0044448312955876, + "decentdb": 0.00015404901932924986, + "decentdb_vs_sqlite": 0.7522842472217232, "direction": "lower_is_better", - "sqlite": 0.000175709996256046, - "winner": "sqlite" + "sqlite": 0.00020477501675486565, + "winner": "decentdb" }, "movie_watchlist_s_rows": { "decentdb": 2, @@ -372,7 +372,7 @@ "decentdb": { "abi_version": 7, "native_library": "/home/steven/src/github/decentdb/target/release/libdecentdb.so", - "native_version": "2.16.1", + "native_version": "2.17.0", "python_package_version": null }, "sqlite": { @@ -620,54 +620,54 @@ "status": "skipped" } }, - "generated_at": "2026-08-02T13:28:31.230674+00:00", + "generated_at": "2026-08-06T14:16:08.781649+00:00", "python": { - "executable": "/usr/bin/python3", + "executable": "/usr/bin/python", "version": "3.14.6" }, "results": { "complex": { "decentdb": { - "aggregate_p50_ms": 0.030377, - "aggregate_p95_ms": 0.049834, - "catalog_insert_s": 0.0013478309992933646, - "delete_p50_ms": 0.030598, - "delete_p95_ms": 0.039795, - "history_p50_ms": 0.007254, - "history_p95_ms": 0.01078, - "join_p50_ms": 0.063399, - "join_p95_ms": 0.09068, - "orders_insert_rps": 119128.27195457293, - "point_lookup_p50_ms": 0.002244, - "point_lookup_p95_ms": 0.002544, - "range_scan_p50_ms": 0.01027, - "range_scan_p95_ms": 0.015339, - "report_query_s": 4.4002998038195074e-05, - "table_scan_p50_ms": 0.002795, - "table_scan_p95_ms": 0.003006, - "update_p50_ms": 0.004579, - "update_p95_ms": 0.109946 + "aggregate_p50_ms": 0.028414, + "aggregate_p95_ms": 0.047369, + "catalog_insert_s": 0.0015343609848059714, + "delete_p50_ms": 0.02681, + "delete_p95_ms": 0.038032, + "history_p50_ms": 0.007394, + "history_p95_ms": 0.011021, + "join_p50_ms": 0.06377, + "join_p95_ms": 0.091181, + "orders_insert_rps": 467016.09162784467, + "point_lookup_p50_ms": 0.002274, + "point_lookup_p95_ms": 0.002795, + "range_scan_p50_ms": 0.010771, + "range_scan_p95_ms": 0.016952, + "report_query_s": 6.251799641177058e-05, + "table_scan_p50_ms": 0.002665, + "table_scan_p95_ms": 0.002915, + "update_p50_ms": 0.004028, + "update_p95_ms": 0.110006 }, "sqlite": { - "aggregate_p50_ms": 0.005811, - "aggregate_p95_ms": 0.046898, - "catalog_insert_s": 0.0016404899943154305, - "delete_p50_ms": 0.017944, - "delete_p95_ms": 0.029776, - "history_p50_ms": 0.005129, - "history_p95_ms": 0.011111, - "join_p50_ms": 0.031399, - "join_p95_ms": 0.045165, - "orders_insert_rps": 334950.98515715025, - "point_lookup_p50_ms": 0.00537, - "point_lookup_p95_ms": 0.006062, - "range_scan_p50_ms": 0.00531, - "range_scan_p95_ms": 0.028674, - "report_query_s": 0.0001471559953643009, - "table_scan_p50_ms": 0.005511, - "table_scan_p95_ms": 0.005831, - "update_p50_ms": 0.004278, - "update_p95_ms": 0.007003 + "aggregate_p50_ms": 0.005921, + "aggregate_p95_ms": 0.039614, + "catalog_insert_s": 0.0012204110098537058, + "delete_p50_ms": 0.015099, + "delete_p95_ms": 0.019176, + "history_p50_ms": 0.004148, + "history_p95_ms": 0.008906, + "join_p50_ms": 0.029636, + "join_p95_ms": 0.03725, + "orders_insert_rps": 417788.20518341317, + "point_lookup_p50_ms": 0.00521, + "point_lookup_p95_ms": 0.008085, + "range_scan_p50_ms": 0.005992, + "range_scan_p95_ms": 0.033242, + "report_query_s": 0.00011769100092351437, + "table_scan_p50_ms": 0.003346, + "table_scan_p95_ms": 0.003417, + "update_p50_ms": 0.003327, + "update_p95_ms": 0.005471 } }, "movie": { @@ -789,25 +789,25 @@ } }, "busiest_people_rows": 20, - "movie_bulk_load_rps": 111449.98572717741, - "movie_bulk_load_s": 0.38672055199276656, - "movie_busiest_people_s": 0.00038047500129323453, + "movie_bulk_load_rps": 113760.8644209266, + "movie_bulk_load_s": 0.3788649129855912, + "movie_busiest_people_s": 0.0004071650037076324, "movie_busiest_people_s_rows": 20, - "movie_checkpoint_after_mutations_s": 0.014619162000599317, - "movie_checkpoint_s": 0.03230415099824313, + "movie_checkpoint_after_mutations_s": 0.01721704500960186, + "movie_checkpoint_s": 0.033217706019058824, "movie_delete_cascade_rows": 10, - "movie_delete_cascade_s": 0.016309216007357463, + "movie_delete_cascade_s": 0.018880558025557548, "movie_final_file_size_bytes": 7630848, "movie_point_reads_rows": 1000, - "movie_point_reads_s": 0.008052481003687717, - "movie_tag_search_s": 0.0007193310011643916, + "movie_point_reads_s": 0.008146423992002383, + "movie_tag_search_s": 0.0007301209843717515, "movie_tag_search_s_rows": 50, - "movie_top_rated_s": 0.0005835150077473372, + "movie_top_rated_s": 0.00032629299676045775, "movie_top_rated_s_rows": 0, "movie_update_batch_rows": 1000, - "movie_update_batch_s": 0.018434918005368672, - "movie_vacuum_s": 0.011149796991958283, - "movie_watchlist_s": 0.00017649099754635245, + "movie_update_batch_s": 0.01715468699694611, + "movie_vacuum_s": 0.010237530019367114, + "movie_watchlist_s": 0.00015404901932924986, "movie_watchlist_s_rows": 2, "movies_after": 1990, "movies_before": 2000, @@ -943,25 +943,25 @@ } }, "busiest_people_rows": 20, - "movie_bulk_load_rps": 140929.9268054729, - "movie_bulk_load_s": 0.30582574600703083, - "movie_busiest_people_s": 0.001927709992742166, + "movie_bulk_load_rps": 147103.80844921985, + "movie_bulk_load_s": 0.29299037499004044, + "movie_busiest_people_s": 0.0019194039923604578, "movie_busiest_people_s_rows": 20, - "movie_checkpoint_after_mutations_s": 0.026330243999836966, - "movie_checkpoint_s": 0.05736412899568677, + "movie_checkpoint_after_mutations_s": 0.025147442996967584, + "movie_checkpoint_s": 0.04933875298593193, "movie_delete_cascade_rows": 10, - "movie_delete_cascade_s": 0.011301643011393026, + "movie_delete_cascade_s": 0.012418816011631861, "movie_final_file_size_bytes": 9400320, "movie_point_reads_rows": 1000, - "movie_point_reads_s": 0.006987030996242538, - "movie_tag_search_s": 0.0003600260097300634, + "movie_point_reads_s": 0.00743856601184234, + "movie_tag_search_s": 0.00035311299143359065, "movie_tag_search_s_rows": 50, - "movie_top_rated_s": 0.0011453400074969977, + "movie_top_rated_s": 0.0013760139991063625, "movie_top_rated_s_rows": 0, "movie_update_batch_rows": 1000, - "movie_update_batch_s": 0.00789489499584306, - "movie_vacuum_s": 0.07739626600232441, - "movie_watchlist_s": 0.000175709996256046, + "movie_update_batch_s": 0.007283263985300437, + "movie_vacuum_s": 0.05899167599272914, + "movie_watchlist_s": 0.00020477501675486565, "movie_watchlist_s_rows": 2, "movies_after": 1990, "movies_before": 2000, diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index fd79c431..1aabf1ca 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "decentdb" -version = "2.17.0" +version = "2.17.1" description = "Python DB-API 2.0 driver and SQLAlchemy dialect for DecentDB" readme = "README.md" authors = [ diff --git a/bindings/web/package-lock.json b/bindings/web/package-lock.json index 67922e81..7c85197c 100644 --- a/bindings/web/package-lock.json +++ b/bindings/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "@decentdb/web", - "version": "0.0.1", + "version": "2.17.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@decentdb/web", - "version": "0.0.1", + "version": "2.17.1", "license": "Apache-2.0", "devDependencies": { "@playwright/test": "^1.48.0", diff --git a/bindings/web/package.json b/bindings/web/package.json index 7e40fe5e..bbd16ff6 100644 --- a/bindings/web/package.json +++ b/bindings/web/package.json @@ -1,6 +1,6 @@ { "name": "@decentdb/web", - "version": "0.0.1", + "version": "2.17.1", "description": "Web binding scaffold for DecentDB via browser worker + WASM runtime", "license": "Apache-2.0", "private": false, diff --git a/crates/decentdb-benchmark/Cargo.toml b/crates/decentdb-benchmark/Cargo.toml index 7dcb539b..f008e263 100644 --- a/crates/decentdb-benchmark/Cargo.toml +++ b/crates/decentdb-benchmark/Cargo.toml @@ -2,6 +2,7 @@ name = "decentdb-benchmark" version.workspace = true edition.workspace = true +rust-version.workspace = true authors.workspace = true license.workspace = true diff --git a/crates/decentdb-cli/Cargo.toml b/crates/decentdb-cli/Cargo.toml index f9e7d4e2..e2037a31 100644 --- a/crates/decentdb-cli/Cargo.toml +++ b/crates/decentdb-cli/Cargo.toml @@ -2,6 +2,7 @@ name = "decentdb-cli" version.workspace = true edition.workspace = true +rust-version.workspace = true authors.workspace = true license.workspace = true diff --git a/crates/decentdb-migrate/Cargo.toml b/crates/decentdb-migrate/Cargo.toml index 9335b4fc..32083436 100644 --- a/crates/decentdb-migrate/Cargo.toml +++ b/crates/decentdb-migrate/Cargo.toml @@ -2,6 +2,7 @@ name = "decentdb-migrate" version.workspace = true edition.workspace = true +rust-version.workspace = true authors.workspace = true license.workspace = true repository.workspace = true diff --git a/crates/decentdb/Cargo.toml b/crates/decentdb/Cargo.toml index bf6acd55..299d0652 100644 --- a/crates/decentdb/Cargo.toml +++ b/crates/decentdb/Cargo.toml @@ -2,12 +2,14 @@ name = "decentdb" version.workspace = true edition.workspace = true +rust-version.workspace = true authors.workspace = true license.workspace = true [features] default = ["lua-extensions"] bench-internals = [] +fuzz-internals = [] lua-extensions = ["dep:mlua"] [dependencies] diff --git a/crates/decentdb/src/db.rs b/crates/decentdb/src/db.rs index af464dd6..91423955 100644 --- a/crates/decentdb/src/db.rs +++ b/crates/decentdb/src/db.rs @@ -95,6 +95,13 @@ mod open; mod query_api; mod schema; mod sync_api; + +mod branch_ops; +mod pragmas; +mod prepared_fast_paths; +mod reactive_ops; +mod sync_ops; + use audit::*; use branches::*; use open::*; @@ -301,7 +308,7 @@ pub struct PreparedStatement { } #[derive(Clone, Debug)] -struct PreparedPlanBundle { +pub(crate) struct PreparedPlanBundle { statement: Arc, simple_row_id_projection: Option, simple_indexed_projection: Option, @@ -316,7 +323,7 @@ struct PreparedPlanBundle { } #[derive(Clone, Debug)] -struct PreparedPlanCacheEntry { +pub(crate) struct PreparedPlanCacheEntry { key_hash: u64, bundle: PreparedPlanBundle, plan_size_bytes: u64, @@ -512,7 +519,7 @@ impl PreparedPlanCache { } #[derive(Clone, Debug)] -struct PreparedSimpleRowIdProjection { +pub(crate) struct PreparedSimpleRowIdProjection { table_name: String, projection_indexes: Vec, column_names: Arc<[String]>, @@ -520,7 +527,7 @@ struct PreparedSimpleRowIdProjection { } #[derive(Clone, Debug)] -struct PreparedSimpleIndexedProjection { +pub(crate) struct PreparedSimpleIndexedProjection { table_name: String, projection_indexes: Vec, column_names: Arc<[String]>, @@ -545,7 +552,7 @@ struct PreparedSimpleRangeBoundParam { } #[derive(Clone, Debug)] -struct PreparedSimpleRowIdRangeProjection { +pub(crate) struct PreparedSimpleRowIdRangeProjection { table_name: String, projection_indexes: Vec, column_names: Arc<[String]>, @@ -556,7 +563,7 @@ struct PreparedSimpleRowIdRangeProjection { } #[derive(Clone, Debug)] -struct PreparedSimpleOrderedRowIdProjection { +pub(crate) struct PreparedSimpleOrderedRowIdProjection { table_name: String, order_column: String, projection_indexes: Vec, @@ -567,7 +574,7 @@ struct PreparedSimpleOrderedRowIdProjection { } #[derive(Clone, Debug)] -struct PreparedSimpleRowIdJoinProjection { +pub(crate) struct PreparedSimpleRowIdJoinProjection { left_table_name: String, right_table_name: String, left_projection_indexes: Vec, @@ -578,7 +585,7 @@ struct PreparedSimpleRowIdJoinProjection { } #[derive(Clone, Debug)] -struct PreparedSimpleScalarFilteredAggregate { +pub(crate) struct PreparedSimpleScalarFilteredAggregate { table_name: String, param_index: usize, cache: Arc>, @@ -2502,52 +2509,6 @@ impl Db { self.write_queue().snapshot() } - /// Subscribes to committed changes for one or more persistent user tables. - pub fn watch_table(&self, options: TableWatchOptions) -> Result { - let tables = self.validate_watch_tables(&options.tables)?; - self.reactive_hub().watch_table( - tables, - options.queue_capacity, - self.inner.wal.latest_snapshot(), - self.schema_cookie()?, - ) - } - - /// Subscribes to committed changes intersecting a primary-key range. - pub fn watch_range(&self, mut options: RangeWatchOptions) -> Result { - let canonical = self.validate_watch_range_table(&options.table)?; - options.table = canonical; - self.reactive_hub().watch_range( - options, - self.inner.wal.latest_snapshot(), - self.schema_cookie()?, - ) - } - - /// Executes a SELECT and subscribes to invalidations for its dependencies. - pub fn watch_query( - &self, - sql: &str, - params: &[Value], - options: QueryWatchOptions, - ) -> Result { - let statement = self.parsed_statement(sql)?; - if !statement_is_read_only(&statement) { - return Err(DbError::sql( - "query subscriptions require a read-only SELECT", - )); - } - let dependencies = self.query_watch_dependencies(&statement)?; - let result = self.execute_with_params(sql, params)?; - self.reactive_hub().watch_query( - dependencies, - options.queue_capacity, - self.inner.wal.latest_snapshot(), - self.schema_cookie()?, - result, - ) - } - /// Subscribes to ordered committed change events. pub fn change_stream(&self, options: ChangeStreamOptions) -> Result { let tables = if options.tables.is_empty() { @@ -2563,22 +2524,6 @@ impl Db { ) } - /// Returns current reactive subscription counters. - #[must_use] - pub fn reactive_metrics(&self) -> ReactiveMetricsSnapshot { - self.reactive_hub_if_initialized() - .map_or_else(ReactiveMetricsSnapshot::default, |hub| { - hub.metrics_snapshot() - }) - } - - /// Returns current reactive subscription details. - #[must_use] - pub fn reactive_subscriptions(&self) -> Vec { - self.reactive_hub_if_initialized() - .map_or_else(Vec::new, |hub| hub.subscription_snapshots()) - } - /// Executes one or more read-only SQL statements against a retained WAL LSN. pub fn execute_batch_at_snapshot_lsn( &self, @@ -2803,130 +2748,6 @@ impl Db { ))) } - /// Compares two refs (`main`, branch name, named snapshot, or branch head ID). - pub fn branch_diff( - &self, - left_ref: &str, - right_ref: &str, - ) -> Result { - let left_db = self.materialize_ref_db(left_ref)?; - let right_db = self.materialize_ref_db(right_ref)?; - diff_materialized_refs(left_ref, right_ref, &left_db, &right_db) - } - - /// Restores a non-main branch head to another branch, named snapshot, or head ID. - pub fn branch_restore( - &self, - branch_name: &str, - target_ref: &str, - dry_run: bool, - ) -> Result { - if self.inner.sql_txn_active.load(Ordering::Acquire) { - return Err(DbError::transaction( - "cannot restore a branch while a SQL transaction is active", - )); - } - if branch_name == crate::branch::DEFAULT_BRANCH_NAME { - return Err(DbError::transaction( - "restore currently targets non-main branches; create a branch from the restore point to inspect main rollback candidates", - )); - } - let branch = crate::branch::branch_by_name(self, branch_name)? - .ok_or_else(|| DbError::transaction(format!("unknown branch '{branch_name}'")))?; - let target_head = self.resolve_branch_target_head(target_ref)?; - let diff = self.branch_diff(branch_name, target_ref)?; - if dry_run { - return Ok(crate::branch::BranchRestoreReport { - branch: branch_name.to_string(), - target_ref: target_ref.to_string(), - dry_run: true, - previous_head_id: branch.current_head_id, - target_head_id: target_head.head_id, - new_head_id: None, - changed_table_count: diff.changed_table_count, - added_row_count: diff.added_row_count, - updated_row_count: diff.updated_row_count, - deleted_row_count: diff.deleted_row_count, - }); - } - let new_head = crate::branch::restore_branch_head(self, &branch, &target_head, target_ref)?; - self.refresh_named_snapshot_retention()?; - Ok(crate::branch::BranchRestoreReport { - branch: branch_name.to_string(), - target_ref: target_ref.to_string(), - dry_run: false, - previous_head_id: branch.current_head_id, - target_head_id: target_head.head_id, - new_head_id: Some(new_head.head_id), - changed_table_count: diff.changed_table_count, - added_row_count: diff.added_row_count, - updated_row_count: diff.updated_row_count, - deleted_row_count: diff.deleted_row_count, - }) - } - - /// Merges clean primary-key row changes from a source branch into a target ref. - pub fn branch_merge( - &self, - source_branch: &str, - target_ref: &str, - dry_run: bool, - ) -> Result { - if self.inner.sql_txn_active.load(Ordering::Acquire) { - return Err(DbError::transaction( - "cannot merge a branch while a SQL transaction is active", - )); - } - if source_branch == crate::branch::DEFAULT_BRANCH_NAME { - return Err(DbError::transaction( - "merge source must be a non-main branch", - )); - } - let source = crate::branch::branch_by_name(self, source_branch)? - .ok_or_else(|| DbError::transaction(format!("unknown branch '{source_branch}'")))?; - let base_head_id = source.base_head_id.clone().ok_or_else(|| { - DbError::transaction(format!("branch '{source_branch}' has no merge base")) - })?; - if target_ref != crate::branch::DEFAULT_BRANCH_NAME - && crate::branch::branch_by_name(self, target_ref)?.is_none() - { - return Err(DbError::transaction(format!( - "merge target must be 'main' or a branch; got '{target_ref}'" - ))); - } - - let base_db = self.materialize_branch_head_db(&base_head_id)?; - let source_db = self.materialize_branch_db(&source)?; - let target_db = self.materialize_ref_db(target_ref)?; - let plan = build_merge_plan( - source_branch, - target_ref, - &base_head_id, - &base_db, - &source_db, - &target_db, - )?; - if dry_run || !plan.conflicts.is_empty() { - return Ok(plan.into_report(dry_run)); - } - let sql = plan - .changes - .iter() - .map(|change| change.sql.as_str()) - .collect::>() - .join("\n"); - if !sql.trim().is_empty() { - if target_ref == crate::branch::DEFAULT_BRANCH_NAME { - crate::reactive::with_change_source(ChangeSource::BranchMerge, || { - self.execute_batch(&sql) - })?; - } else { - self.execute_batch_on_branch(&sql, target_ref)?; - } - } - Ok(plan.into_report(false)) - } - /// Executes one or more read-only SQL statements against a named snapshot. pub fn execute_batch_at_snapshot( &self, @@ -3766,30 +3587,6 @@ impl Db { .get_or_init(|| WriteQueue::new(&self.inner.config)) } - fn reactive_hub(&self) -> Arc { - Arc::clone(self.inner.reactive_hub.get_or_init(|| { - crate::reactive::acquire_hub( - self.inner.reactive_registry_key.clone(), - &self.inner.config, - ) - })) - } - - fn reactive_hub_if_initialized(&self) -> Option<&Arc> { - self.inner.reactive_hub.get() - } - - fn reactive_hub_if_available(&self) -> Option> { - self.reactive_hub_if_initialized() - .map(Arc::clone) - .or_else(|| crate::reactive::existing_hub(self.inner.reactive_registry_key.as_ref())) - } - - fn reactive_has_watchers(&self) -> bool { - self.reactive_hub_if_available() - .is_some_and(|hub| hub.has_watchers()) - } - fn reject_transaction_control_for_queued_sql(&self, sql: &str) -> Result<()> { for statement_sql in split_sql_batch(sql) { let trimmed = statement_sql.trim(); @@ -4034,106 +3831,6 @@ impl Db { crate::branch::branch_head_lsn_by_id(self, reference) } - /// Creates a branch from `main`, another branch, a named snapshot, or a branch head. - pub fn branch_create( - &self, - name: &str, - from: Option<&str>, - ) -> Result { - let source = from.unwrap_or(crate::branch::DEFAULT_BRANCH_NAME); - let (source_lsn, parent_head_id) = if source == crate::branch::DEFAULT_BRANCH_NAME { - let initial_lsn = self.inner.wal.latest_snapshot(); - self.inner.wal.set_retained_snapshot_lsn(Some(initial_lsn)); - self.checkpoint_wal()?; - let source_lsn = self.inner.wal.latest_snapshot(); - let parent_head_id = crate::branch::main_branch_head(self)?.map(|head| head.head_id); - (source_lsn, parent_head_id) - } else if let Some(branch) = crate::branch::branch_by_name(self, source)? { - let source_lsn = self.branch_lsn(source)?.ok_or_else(|| { - DbError::transaction(format!("branch '{source}' has no current head")) - })?; - (source_lsn, branch.current_head_id) - } else if let Some(snapshot) = self.snapshot_get(source)? { - (snapshot.snapshot_lsn, Some(snapshot.head_id)) - } else if let Some(source_lsn) = crate::branch::branch_head_lsn_by_id(self, source)? { - (source_lsn, Some(source.to_string())) - } else { - return Err(DbError::transaction(format!( - "unknown branch, snapshot, or head '{source}'" - ))); - }; - self.inner.wal.set_retained_snapshot_lsn(Some(source_lsn)); - let schema_cookie = self.current_schema_cookie_at_snapshot(source_lsn)?; - let result = crate::branch::create_branch( - self, - name, - source_lsn, - schema_cookie, - parent_head_id.as_deref(), - ); - self.refresh_named_snapshot_retention()?; - result - } - - /// Lists branches. - pub fn branch_list(&self) -> Result> { - crate::branch::list_branches(self) - } - - /// Deletes a non-main branch. - pub fn branch_delete(&self, name: &str) -> Result { - let deleted = crate::branch::delete_branch(self, name)?; - if deleted { - self.refresh_named_snapshot_retention()?; - } - Ok(deleted) - } - - /// Renames a non-main branch. - pub fn branch_rename(&self, old_name: &str, new_name: &str) -> Result { - crate::branch::rename_branch(self, old_name, new_name) - } - - /// Resolves a branch name to its current retained WAL LSN. - pub fn branch_lsn(&self, name: &str) -> Result> { - crate::branch::branch_lsn_by_name(self, name) - } - - /// Adds a named commit marker to a non-main branch. - pub fn branch_commit( - &self, - name: &str, - message: &str, - ) -> Result { - if self.inner.sql_txn_active.load(Ordering::Acquire) { - return Err(DbError::transaction( - "cannot create a branch commit marker while a SQL transaction is active", - )); - } - if name == crate::branch::DEFAULT_BRANCH_NAME { - return Err(DbError::transaction( - "branch commit markers are only supported on non-main branches", - )); - } - let branch = crate::branch::branch_by_name(self, name)? - .ok_or_else(|| DbError::transaction(format!("unknown branch '{name}'")))?; - let head = crate::branch::commit_branch(self, &branch, message)?; - self.refresh_named_snapshot_retention()?; - Ok(crate::branch::BranchLogEntry { - head_id: head.head_id, - branch_id: head.branch_id, - parent_head_id: head.parent_head_id, - message: head.message, - created_at_micros: head.created_at_micros, - sql: None, - }) - } - - /// Returns branch head history newest first. - pub fn branch_log(&self, name: &str) -> Result> { - crate::branch::branch_log(self, name) - } - /// Deletes a named snapshot and refreshes the WAL retention floor. pub fn snapshot_delete(&self, name: &str) -> Result { let deleted = crate::branch::delete_named_snapshot(self, name)?; @@ -4801,14 +4498,6 @@ impl Db { .map_err(|_| DbError::internal("plan cache lock poisoned")) } - fn prepared_plan_cache_entries(&self) -> Result> { - self.inner - .prepared_plan_cache - .lock() - .map(|cache| cache.snapshot_entries()) - .map_err(|_| DbError::internal("prepared plan cache lock poisoned")) - } - /// Flushes the connection-local plan cache and resets its counters. pub fn flush_plan_cache(&self) -> Result<()> { self.inner @@ -5213,14 +4902,6 @@ impl Db { Ok(()) } - fn execute_pragma_command(&self, command: PragmaCommand) -> Result { - match command { - PragmaCommand::Query(target) => self.execute_pragma_query(target), - PragmaCommand::Call { target, argument } => self.execute_pragma_call(target, argument), - PragmaCommand::Set(target, value) => self.execute_pragma_set(target, value), - } - } - fn ensure_security_catalog(&self) -> Result<()> { for ddl in [ crate::security::POLICIES_DDL, @@ -5365,233 +5046,6 @@ impl Db { Ok(()) } - fn execute_pragma_query(&self, target: PragmaTarget) -> Result { - match target.name { - PragmaName::PageSize => Ok(QueryResult::with_rows( - vec!["page_size".to_string()], - vec![QueryRow::new(vec![Value::Int64(i64::from( - self.inner.config.page_size, - ))])], - )), - PragmaName::CacheSize => Ok(QueryResult::with_rows( - vec!["cache_size".to_string()], - vec![QueryRow::new(vec![Value::Int64(cache_size_pages( - &self.inner.config, - ))])], - )), - PragmaName::DatabaseList => { - let file_name = if is_memory_path(&self.inner.path) { - ":memory:".to_string() - } else { - self.inner.path.display().to_string() - }; - Ok(QueryResult::with_rows( - vec!["seq".to_string(), "name".to_string(), "file".to_string()], - vec![QueryRow::new(vec![ - Value::Int64(0), - Value::Text("main".to_string()), - Value::Text(file_name), - ])], - )) - } - PragmaName::TableInfo => Err(DbError::sql( - "PRAGMA table_info(table_name) requires a table name argument", - )), - PragmaName::TableXInfo => Err(DbError::sql( - "PRAGMA table_xinfo(table_name) requires a table name argument", - )), - PragmaName::IndexList => Err(DbError::sql( - "PRAGMA index_list(table_name) requires a table name argument", - )), - PragmaName::IndexInfo => Err(DbError::sql( - "PRAGMA index_info(index_name) requires an index name argument", - )), - PragmaName::IndexXInfo => Err(DbError::sql( - "PRAGMA index_xinfo(index_name) requires an index name argument", - )), - PragmaName::ForeignKeyList => Err(DbError::sql( - "PRAGMA foreign_key_list(table_name) requires a table name argument", - )), - PragmaName::TableList => self.execute_compatibility_select(&format!( - "SELECT * FROM {}pragma_table_list()", - pragma_schema_function_prefix(target.schema) - )), - PragmaName::IntegrityCheck | PragmaName::QuickCheck => self.integrity_check_results(), - PragmaName::ForeignKeys => Ok(QueryResult::with_rows( - vec!["foreign_keys".to_string()], - vec![QueryRow::new(vec![Value::Int64(1)])], - )), - PragmaName::JournalMode => Ok(QueryResult::with_rows( - vec!["journal_mode".to_string()], - vec![QueryRow::new(vec![Value::Text("wal".to_string())])], - )), - PragmaName::Synchronous => Ok(QueryResult::with_rows( - vec!["synchronous".to_string()], - vec![QueryRow::new(vec![Value::Int64( - pragma_synchronous_mode_value(self.inner.config.wal_sync_mode), - )])], - )), - PragmaName::WalCheckpoint => self.execute_pragma_wal_checkpoint(None), - PragmaName::SchemaVersion => { - let runtime = self.runtime_for_metadata_inspection()?; - let version = match target.schema { - Some(PragmaSchema::Temp) => runtime.temp_schema_cookie, - _ => runtime.catalog.schema_cookie, - }; - Ok(QueryResult::with_rows( - vec!["schema_version".to_string()], - vec![QueryRow::new(vec![Value::Int64(i64::from(version))])], - )) - } - PragmaName::UserVersion => self.execute_application_pragma_query("user_version"), - PragmaName::ApplicationId => self.execute_application_pragma_query("application_id"), - PragmaName::Encoding => Ok(QueryResult::with_rows( - vec!["encoding".to_string()], - vec![QueryRow::new(vec![Value::Text("UTF-8".to_string())])], - )), - PragmaName::LockingMode => Ok(QueryResult::with_rows( - vec!["locking_mode".to_string()], - vec![QueryRow::new(vec![Value::Text("normal".to_string())])], - )), - PragmaName::TempStore => Ok(QueryResult::with_rows( - vec!["temp_store".to_string()], - vec![QueryRow::new(vec![Value::Int64(1)])], - )), - PragmaName::BusyTimeout => Ok(QueryResult::with_rows( - vec!["busy_timeout".to_string()], - vec![QueryRow::new(vec![Value::Int64( - i64::try_from(self.inner.busy_timeout_ms.load(Ordering::Acquire)) - .unwrap_or(i64::MAX), - )])], - )), - PragmaName::FlushPlanCache => { - self.flush_plan_cache()?; - Ok(QueryResult::with_affected_rows(0)) - } - } - } - - fn execute_pragma_call( - &self, - target: PragmaTarget, - argument: Option, - ) -> Result { - match target.name { - PragmaName::TableInfo => { - let table_name = pragma_required_argument(&target, argument)?; - self.execute_pragma_table_info(&table_name, target.schema, false) - } - PragmaName::TableXInfo => { - let table_name = pragma_required_argument(&target, argument)?; - self.execute_compatibility_select(&format!( - "SELECT * FROM {}pragma_table_xinfo({})", - pragma_schema_function_prefix(target.schema), - sql_string_literal(&table_name) - )) - } - PragmaName::IndexList => { - let table_name = pragma_required_argument(&target, argument)?; - self.execute_compatibility_select(&format!( - "SELECT * FROM {}pragma_index_list({})", - pragma_schema_function_prefix(target.schema), - sql_string_literal(&table_name) - )) - } - PragmaName::IndexInfo => { - let index_name = pragma_required_argument(&target, argument)?; - self.execute_compatibility_select(&format!( - "SELECT * FROM {}pragma_index_info({})", - pragma_schema_function_prefix(target.schema), - sql_string_literal(&index_name) - )) - } - PragmaName::IndexXInfo => { - let index_name = pragma_required_argument(&target, argument)?; - self.execute_compatibility_select(&format!( - "SELECT * FROM {}pragma_index_xinfo({})", - pragma_schema_function_prefix(target.schema), - sql_string_literal(&index_name) - )) - } - PragmaName::ForeignKeyList => { - let table_name = pragma_required_argument(&target, argument)?; - self.execute_compatibility_select(&format!( - "SELECT * FROM {}pragma_foreign_key_list({})", - pragma_schema_function_prefix(target.schema), - sql_string_literal(&table_name) - )) - } - PragmaName::FlushPlanCache => { - self.flush_plan_cache()?; - Ok(QueryResult::with_affected_rows(0)) - } - PragmaName::WalCheckpoint => self.execute_pragma_wal_checkpoint(argument.as_deref()), - other => Err(DbError::sql(format!( - "PRAGMA {} does not accept call syntax", - pragma_name_sql(&other) - ))), - } - } - - fn execute_pragma_table_info( - &self, - table_name: &str, - schema: Option, - extended: bool, - ) -> Result { - let runtime = self.runtime_for_metadata_inspection()?; - let table = match schema { - Some(PragmaSchema::Temp) => runtime - .temp_table_schema(table_name) - .ok_or_else(|| DbError::sql(format!("unknown temporary table {table_name}")))?, - Some(PragmaSchema::Main) => runtime - .catalog - .table(table_name) - .ok_or_else(|| DbError::sql(format!("unknown table {table_name}")))?, - None => runtime - .table_schema(table_name) - .ok_or_else(|| DbError::sql(format!("unknown table {table_name}")))?, - }; - let rows = table - .columns - .iter() - .enumerate() - .map(|(cid, column)| { - let mut values = vec![ - Value::Int64(i64::try_from(cid).unwrap_or(i64::MAX)), - Value::Text(column.name.clone()), - Value::Text(column.column_type.as_str().to_string()), - Value::Int64(if column.nullable { 0 } else { 1 }), - column.default_sql.clone().map_or(Value::Null, Value::Text), - Value::Int64(if column.primary_key { 1 } else { 0 }), - ]; - if extended { - let hidden = if column.generated_sql.is_none() { - 0 - } else if column.generated_stored { - 3 - } else { - 2 - }; - values.push(Value::Int64(hidden)); - } - QueryRow::new(values) - }) - .collect(); - let mut columns = vec![ - "cid".to_string(), - "name".to_string(), - "type".to_string(), - "notnull".to_string(), - "dflt_value".to_string(), - "pk".to_string(), - ]; - if extended { - columns.push("hidden".to_string()); - } - Ok(QueryResult::with_rows(columns, rows)) - } - fn execute_compatibility_select(&self, sql: &str) -> Result { self.execute(sql) } @@ -5648,38 +5102,6 @@ impl Db { Ok(QueryResult::with_affected_rows(0)) } - fn execute_pragma_wal_checkpoint(&self, mode: Option<&str>) -> Result { - if let Some(mode) = mode { - match mode.trim().to_ascii_uppercase().as_str() { - "PASSIVE" | "FULL" | "RESTART" | "TRUNCATE" => {} - other => { - return Err(DbError::sql(format!( - "PRAGMA wal_checkpoint mode {other} is not supported; expected PASSIVE, FULL, RESTART, or TRUNCATE" - ))) - } - } - } - let active_readers = self.inner.wal.active_reader_count()?; - let retained_snapshot = self.inner.wal.retained_snapshot_lsn().is_some(); - let before_versions = self.inner.wal.version_count()?; - self.prepare_resident_payload_offset_caches_for_wal_checkpoint()?; - self.checkpoint_wal()?; - let after_versions = self.inner.wal.version_count()?; - let checkpointed = before_versions.saturating_sub(after_versions); - Ok(QueryResult::with_rows( - vec![ - "busy".to_string(), - "log".to_string(), - "checkpointed".to_string(), - ], - vec![QueryRow::new(vec![ - Value::Int64(i64::from(active_readers > 0 || retained_snapshot)), - Value::Int64(i64::try_from(before_versions).unwrap_or(i64::MAX)), - Value::Int64(i64::try_from(checkpointed).unwrap_or(i64::MAX)), - ])], - )) - } - fn prepare_resident_payload_offset_caches_for_wal_checkpoint(&self) -> Result<()> { let mut runtime = self .inner @@ -5751,165 +5173,6 @@ impl Db { } } - fn execute_pragma_set(&self, target: PragmaTarget, value: PragmaValue) -> Result { - match target.name { - PragmaName::PageSize => { - let value = pragma_value_i64(&value)?; - if value == i64::from(self.inner.config.page_size) { - Ok(QueryResult::with_affected_rows(0)) - } else { - Err(DbError::sql( - "PRAGMA page_size cannot be changed on an open database; reopen with DbConfig::page_size", - )) - } - } - PragmaName::CacheSize => { - if pragma_value_i64(&value)? == cache_size_pages(&self.inner.config) { - Ok(QueryResult::with_affected_rows(0)) - } else { - Err(DbError::sql( - "PRAGMA cache_size cannot be changed on an open connection; reopen with DbConfig::cache_size_mb", - )) - } - } - PragmaName::IntegrityCheck - | PragmaName::DatabaseList - | PragmaName::TableInfo - | PragmaName::TableXInfo - | PragmaName::TableList - | PragmaName::IndexList - | PragmaName::IndexInfo - | PragmaName::IndexXInfo - | PragmaName::ForeignKeyList - | PragmaName::WalCheckpoint - | PragmaName::QuickCheck => Err(DbError::sql(format!( - "PRAGMA {} does not support assignment", - pragma_name_sql(&target.name) - ))), - PragmaName::FlushPlanCache => { - let value = parse_pragma_text_or_mode(&value, "PRAGMA flush_plan_cache")?; - if value == "LOCAL" { - self.flush_plan_cache()?; - Ok(QueryResult::with_affected_rows(0)) - } else { - Err(DbError::sql( - "PRAGMA flush_plan_cache accepts only local in this release", - )) - } - } - PragmaName::ForeignKeys => { - let value = parse_pragma_bool_value(&value, "PRAGMA foreign_keys")?; - if value { - Ok(QueryResult::with_affected_rows(0)) - } else { - Err(DbError::sql( - "PRAGMA foreign_keys cannot disable foreign key enforcement in DecentDB", - )) - } - } - PragmaName::JournalMode => { - let mode = parse_pragma_text_or_mode(&value, "PRAGMA journal_mode")?; - if mode == "WAL" { - Ok(QueryResult::with_rows( - vec!["journal_mode".to_string()], - vec![QueryRow::new(vec![Value::Text("wal".to_string())])], - )) - } else { - Err(DbError::sql( - "PRAGMA journal_mode supports only WAL in this compatibility slice", - )) - } - } - PragmaName::Synchronous => { - let requested = parse_pragma_synchronous_request(&value, "PRAGMA synchronous")?; - let current = self.inner.config.wal_sync_mode; - match requested { - SynchronousRequest::Full => { - if current == WalSyncMode::Full { - Ok(QueryResult::with_affected_rows(0)) - } else { - Err(DbError::sql( - "PRAGMA synchronous = FULL requires reopening with DbConfig::wal_sync_mode = Full", - )) - } - } - SynchronousRequest::Normal => { - if current == WalSyncMode::Normal - || matches!(current, WalSyncMode::AsyncCommit { .. }) - { - Ok(QueryResult::with_affected_rows(0)) - } else { - Err(DbError::sql( - "PRAGMA synchronous = NORMAL requires reopening with DbConfig::wal_sync_mode = Normal or AsyncCommit", - )) - } - } - SynchronousRequest::Off => { - if current == WalSyncMode::TestingOnlyUnsafeNoSync { - Ok(QueryResult::with_affected_rows(0)) - } else { - Err(DbError::sql( - "PRAGMA synchronous = OFF requires reopening with DbConfig::wal_sync_mode = TestingOnlyUnsafeNoSync", - )) - } - } - SynchronousRequest::Extra => Err(DbError::sql( - "PRAGMA synchronous = EXTRA is not supported by DecentDB", - )), - } - } - PragmaName::SchemaVersion => Err(DbError::sql( - "PRAGMA schema_version does not support assignment", - )), - PragmaName::UserVersion => self.execute_application_pragma_set("user_version", &value), - PragmaName::ApplicationId => { - self.execute_application_pragma_set("application_id", &value) - } - PragmaName::Encoding => { - let mode = parse_pragma_text_or_mode(&value, "PRAGMA encoding")?; - if mode == "UTF-8" || mode == "UTF8" { - Ok(QueryResult::with_affected_rows(0)) - } else { - Err(DbError::sql( - "PRAGMA encoding can only be set to UTF-8 in this compatibility slice", - )) - } - } - PragmaName::LockingMode => { - let mode = parse_pragma_text_or_mode(&value, "PRAGMA locking_mode")?; - if mode == "NORMAL" { - Ok(QueryResult::with_affected_rows(0)) - } else { - Err(DbError::sql( - "PRAGMA locking_mode supports only NORMAL in this compatibility slice", - )) - } - } - PragmaName::TempStore => { - let value = parse_pragma_text_or_mode(&value, "PRAGMA temp_store")?; - if matches!(value.as_str(), "DEFAULT" | "FILE" | "0" | "1") { - Ok(QueryResult::with_affected_rows(0)) - } else if value == "MEMORY" { - Err(DbError::sql( - "PRAGMA temp_store = MEMORY is not supported in this compatibility slice", - )) - } else { - Err(DbError::sql( - "PRAGMA temp_store accepts 0, 1, 'DEFAULT', or 'FILE' only", - )) - } - } - PragmaName::BusyTimeout => { - let value = pragma_value_i64(&value)?; - let value = u64::try_from(value).map_err(|_| { - DbError::sql("PRAGMA busy_timeout requires a non-negative integer") - })?; - self.inner.busy_timeout_ms.store(value, Ordering::Release); - Ok(QueryResult::with_affected_rows(0)) - } - } - } - fn execute_prepared_statement( &self, prepared: &PreparedStatement, @@ -6029,314 +5292,6 @@ impl Db { Ok(total_affected) } - fn prepared_insert_uses_direct_positional_params( - prepared_insert: &PreparedSimpleInsert, - param_count: usize, - ) -> bool { - prepared_insert.direct_positional_param_count == Some(param_count) - } - - fn prepared_statement_cache_key(prepared: &PreparedStatement) -> usize { - if let Some(insert) = prepared.prepared_insert.as_ref() { - return Arc::as_ptr(insert) as usize; - } - Arc::as_ptr(&prepared.statement) as usize - } - - fn prepared_insert_plan_for_runtime_state( - &self, - prepared: &PreparedStatement, - runtime: &mut EngineRuntime, - snapshot_lsn: u64, - indexes_maybe_stale: &mut bool, - prepared_insert_runtime_cache: &mut HashMap>, - ) -> Result>> { - let Some(prepared_insert) = prepared.prepared_insert.as_ref() else { - return Ok(None); - }; - - if *indexes_maybe_stale { - runtime.rebuild_stale_indexes(self.inner.config.page_size)?; - *indexes_maybe_stale = false; - } - - let cache_key = Self::prepared_statement_cache_key(prepared); - if let Some(plan) = prepared_insert_runtime_cache.get(&cache_key) { - if Self::prepared_insert_target_loaded(runtime, plan) { - return Ok(Some(Arc::clone(plan))); - } - prepared_insert_runtime_cache.remove(&cache_key); - } - - let needs_refresh = - prepared_insert.use_generic_validation || prepared_insert.use_generic_index_updates; - if !needs_refresh - && runtime.can_reuse_prepared_simple_insert(prepared_insert) - && Self::prepared_insert_target_loaded(runtime, prepared_insert) - { - prepared_insert_runtime_cache.insert(cache_key, Arc::clone(prepared_insert)); - return Ok(Some(Arc::clone(prepared_insert))); - } - - let table_names = - self.insert_dependency_table_names(runtime, &prepared_insert.table_name)?; - let table_refs = table_names.iter().map(String::as_str).collect::>(); - self.load_runtime_table_row_sources_at_snapshot(runtime, &table_refs, snapshot_lsn)?; - - if !needs_refresh && runtime.can_reuse_prepared_simple_insert(prepared_insert) { - prepared_insert_runtime_cache.insert(cache_key, Arc::clone(prepared_insert)); - return Ok(Some(Arc::clone(prepared_insert))); - } - - let SqlStatement::Insert(insert) = prepared.statement.as_ref() else { - return Ok(None); - }; - let Some(refreshed) = runtime.prepare_simple_insert(insert)? else { - return Ok(None); - }; - let refreshed = Arc::new(refreshed); - if runtime.can_reuse_prepared_simple_insert(refreshed.as_ref()) { - prepared_insert_runtime_cache.insert(cache_key, Arc::clone(&refreshed)); - } - Ok(Some(refreshed)) - } - - fn prepared_insert_changes_persistent_table( - _runtime: &EngineRuntime, - prepared_insert: &PreparedSimpleInsert, - ) -> bool { - prepared_insert.catalog_table_name.is_some() - } - - fn prepared_insert_target_loaded( - runtime: &EngineRuntime, - prepared_insert: &PreparedSimpleInsert, - ) -> bool { - if let Some(table_name) = prepared_insert.catalog_table_name.as_deref() { - runtime.tables.contains_key(table_name) - } else { - runtime.prepared_insert_target_loaded(&prepared_insert.table_name) - } - } - - fn try_execute_prepared_simple_ordered_row_id_projection( - &self, - prepared: &PreparedStatement, - ) -> Result> { - if self.inner.sql_txn_active.load(Ordering::Acquire) - || self.inner.config.extension_unsigned_development_mode - || !self.inner.config.extension_trust_anchors.is_empty() - { - return Ok(None); - } - let Some(plan) = prepared.simple_ordered_row_id_projection.as_ref() else { - return Ok(None); - }; - let Some(runtime) = self.try_resident_read_for_single_process_statement( - prepared.statement.as_ref(), - Some(prepared), - )? - else { - return Ok(None); - }; - let result = runtime.execute_resolved_simple_ordered_row_id_projection( - ResolvedSimpleOrderedRowIdProjectionRequest { - table_name: plan.table_name.as_str(), - order_column: plan.order_column.as_str(), - projection_indexes: &plan.projection_indexes, - column_names: Arc::clone(&plan.column_names), - limit: plan.limit, - offset: plan.offset, - descending: plan.descending, - }, - )?; - drop(runtime); - if let Some(result) = result { - return self - .finalize_row_source_autocommit_statement(prepared.statement.as_ref(), Ok(result)) - .map(Some); - } - Ok(None) - } - - fn try_execute_prepared_simple_row_id_projection( - &self, - prepared: &PreparedStatement, - params: &[Value], - ) -> Result> { - if self.inner.sql_txn_active.load(Ordering::Acquire) { - return Ok(None); - } - let Some(plan) = prepared.simple_row_id_projection.as_ref() else { - return Ok(None); - }; - let Some(Value::Int64(lookup_row_id)) = params.get(plan.param_index) else { - return Ok(None); - }; - - if !self.inner.config.extension_unsigned_development_mode - && self.inner.config.extension_trust_anchors.is_empty() - { - if let Some(runtime) = self.try_resident_read_for_prepared_table_statement( - prepared, - plan.table_name.as_str(), - )? { - let result = runtime.execute_resolved_simple_row_id_projection_at_snapshot( - ResolvedSimpleRowIdProjectionRequest { - table_name: plan.table_name.as_str(), - projection_indexes: &plan.projection_indexes, - column_names: Arc::clone(&plan.column_names), - lookup_row_id: *lookup_row_id, - pager: &self.inner.pager, - wal: &self.inner.wal, - snapshot_lsn: 0, - use_persistent_pk_index: self.inner.config.persistent_pk_index, - }, - )?; - drop(runtime); - if let Some(result) = result { - return Ok(Some(result)); - } - } - } - - let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - if let Some(runtime) = self.runtime_read_for_prepared_row_sources_at_snapshot( - &[plan.table_name.as_str()], - snapshot_lsn, - )? { - self.validate_prepared_schema_cookie( - prepared, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - let result = runtime.execute_resolved_simple_row_id_projection_at_snapshot( - ResolvedSimpleRowIdProjectionRequest { - table_name: plan.table_name.as_str(), - projection_indexes: &plan.projection_indexes, - column_names: Arc::clone(&plan.column_names), - lookup_row_id: *lookup_row_id, - pager: &self.inner.pager, - wal: &self.inner.wal, - snapshot_lsn, - use_persistent_pk_index: self.inner.config.persistent_pk_index, - }, - )?; - if result.is_some() { - drop(runtime); - drop(reader); - return Ok(result); - } - drop(runtime); - } - self.refresh_engine_from_snapshot(snapshot_lsn)?; - self.try_load_prepared_read_row_sources_at_snapshot( - &[plan.table_name.as_str()], - snapshot_lsn, - )?; - let Some(runtime) = self.runtime_read_for_fast_read_at_snapshot(snapshot_lsn)? else { - drop(reader); - return Ok(None); - }; - self.validate_prepared_schema_cookie( - prepared, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - let result = runtime.execute_resolved_simple_row_id_projection_at_snapshot( - ResolvedSimpleRowIdProjectionRequest { - table_name: plan.table_name.as_str(), - projection_indexes: &plan.projection_indexes, - column_names: Arc::clone(&plan.column_names), - lookup_row_id: *lookup_row_id, - pager: &self.inner.pager, - wal: &self.inner.wal, - snapshot_lsn, - use_persistent_pk_index: self.inner.config.persistent_pk_index, - }, - )?; - drop(runtime); - drop(reader); - Ok(result) - } - - fn try_execute_prepared_simple_indexed_projection( - &self, - prepared: &PreparedStatement, - params: &[Value], - ) -> Result> { - if self.inner.sql_txn_active.load(Ordering::Acquire) { - return Ok(None); - } - let Some(plan) = prepared.simple_indexed_projection.as_ref() else { - return Ok(None); - }; - - if !self.inner.config.extension_unsigned_development_mode - && self.inner.config.extension_trust_anchors.is_empty() - { - if let Some(runtime) = self.try_resident_read_for_single_process_statement( - prepared.statement.as_ref(), - Some(prepared), - )? { - let result = self.execute_prepared_simple_indexed_projection_in_runtime( - &runtime, plan, params, - )?; - drop(runtime); - if let Some(result) = result { - return self - .finalize_row_source_autocommit_statement( - prepared.statement.as_ref(), - Ok(result), - ) - .map(Some); - } - } - } - - let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - if let Some(runtime) = self.runtime_read_for_prepared_row_sources_at_snapshot( - &[plan.table_name.as_str()], - snapshot_lsn, - )? { - self.validate_prepared_schema_cookie( - prepared, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - let result = - self.execute_prepared_simple_indexed_projection_in_runtime(&runtime, plan, params)?; - if result.is_some() { - drop(runtime); - drop(reader); - return Ok(result); - } - drop(runtime); - } - - self.refresh_engine_from_snapshot(snapshot_lsn)?; - self.try_load_prepared_read_row_sources_at_snapshot( - &[plan.table_name.as_str()], - snapshot_lsn, - )?; - let Some(runtime) = self.runtime_read_for_fast_read_at_snapshot(snapshot_lsn)? else { - drop(reader); - return Ok(None); - }; - self.validate_prepared_schema_cookie( - prepared, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - let result = - self.execute_prepared_simple_indexed_projection_in_runtime(&runtime, plan, params)?; - drop(runtime); - drop(reader); - Ok(result) - } - fn execute_prepared_simple_indexed_projection_in_runtime( &self, runtime: &EngineRuntime, @@ -6443,282 +5398,15 @@ impl Db { ))) } - fn try_execute_prepared_simple_row_id_range_projection( + fn execute_prepared_read_statement( &self, prepared: &PreparedStatement, params: &[Value], - ) -> Result> { - if self.inner.sql_txn_active.load(Ordering::Acquire) { - return Ok(None); - } - let Some(plan) = prepared.simple_row_id_range_projection.as_ref() else { - return Ok(None); - }; - let lower_bound = if let Some(bound) = plan.lower_bound { - let Some(Value::Int64(value)) = params.get(bound.param_index) else { - return Ok(None); - }; - Some(SimpleRangeBoundValue { - inclusive: bound.inclusive, - value: Value::Int64(*value), - }) - } else { - None - }; - let upper_bound = if let Some(bound) = plan.upper_bound { - let Some(Value::Int64(value)) = params.get(bound.param_index) else { - return Ok(None); - }; - Some(SimpleRangeBoundValue { - inclusive: bound.inclusive, - value: Value::Int64(*value), - }) - } else { - None - }; - let Some(Value::Int64(limit_value)) = params.get(plan.limit_param_index) else { - return Ok(None); - }; - let limit = Some(usize::try_from((*limit_value).max(0)).unwrap_or(usize::MAX)); - - let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - if let Some(runtime) = self.runtime_read_for_prepared_row_sources_at_snapshot( - &[plan.table_name.as_str()], - snapshot_lsn, - )? { - self.validate_prepared_schema_cookie( - prepared, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - let result = runtime.execute_resolved_simple_row_id_range_projection_at_snapshot( - ResolvedSimpleRowIdRangeProjectionRequest { - table_name: plan.table_name.as_str(), - projection_indexes: &plan.projection_indexes, - column_names: Arc::clone(&plan.column_names), - filter_column: plan.filter_column.as_str(), - lower_bound: lower_bound.clone(), - upper_bound: upper_bound.clone(), - limit, - pager: &self.inner.pager, - wal: &self.inner.wal, - snapshot_lsn, - use_persistent_pk_index: self.inner.config.persistent_pk_index, - }, - )?; - if result.is_some() { - drop(runtime); - drop(reader); - return Ok(result); - } - drop(runtime); - } - self.refresh_engine_from_snapshot(snapshot_lsn)?; - self.try_load_prepared_read_row_sources_at_snapshot( - &[plan.table_name.as_str()], - snapshot_lsn, - )?; - let Some(runtime) = self.runtime_read_for_fast_read_at_snapshot(snapshot_lsn)? else { - drop(reader); - return Ok(None); - }; - self.validate_prepared_schema_cookie( - prepared, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - let result = runtime.execute_resolved_simple_row_id_range_projection_at_snapshot( - ResolvedSimpleRowIdRangeProjectionRequest { - table_name: plan.table_name.as_str(), - projection_indexes: &plan.projection_indexes, - column_names: Arc::clone(&plan.column_names), - filter_column: plan.filter_column.as_str(), - lower_bound, - upper_bound, - limit, - pager: &self.inner.pager, - wal: &self.inner.wal, - snapshot_lsn, - use_persistent_pk_index: self.inner.config.persistent_pk_index, - }, - )?; - drop(runtime); - drop(reader); - Ok(result) - } - - fn try_execute_prepared_simple_row_id_join_projection( - &self, - prepared: &PreparedStatement, - params: &[Value], - ) -> Result> { - if self.inner.sql_txn_active.load(Ordering::Acquire) { - return Ok(None); - } - let Some(plan) = prepared.simple_row_id_join_projection.as_ref() else { - return Ok(None); - }; - let Some(Value::Int64(lookup_row_id)) = params.get(plan.param_index) else { - return Ok(None); - }; - - let join_tables = [ - plan.left_table_name.as_str(), - plan.right_table_name.as_str(), - ]; - let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - if let Some(runtime) = - self.runtime_read_for_prepared_row_sources_at_snapshot(&join_tables, snapshot_lsn)? - { - self.validate_prepared_schema_cookie( - prepared, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - let result = runtime.execute_resolved_simple_row_id_join_projection_at_snapshot( - ResolvedSimpleRowIdJoinProjectionRequest { - left_table_name: plan.left_table_name.as_str(), - right_table_name: plan.right_table_name.as_str(), - left_projection_indexes: &plan.left_projection_indexes, - right_projection_indexes: &plan.right_projection_indexes, - projections: &plan.projections, - column_names: Arc::clone(&plan.column_names), - lookup_row_id: *lookup_row_id, - pager: &self.inner.pager, - wal: &self.inner.wal, - snapshot_lsn, - use_persistent_pk_index: self.inner.config.persistent_pk_index, - }, - )?; - if result.is_some() { - drop(runtime); - drop(reader); - return Ok(result); - } - drop(runtime); - } - self.refresh_engine_from_snapshot(snapshot_lsn)?; - self.try_load_prepared_read_row_sources_at_snapshot(&join_tables, snapshot_lsn)?; - let Some(runtime) = self.runtime_read_for_fast_read_at_snapshot(snapshot_lsn)? else { - drop(reader); - return Ok(None); - }; - self.validate_prepared_schema_cookie( - prepared, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - let result = runtime.execute_resolved_simple_row_id_join_projection_at_snapshot( - ResolvedSimpleRowIdJoinProjectionRequest { - left_table_name: plan.left_table_name.as_str(), - right_table_name: plan.right_table_name.as_str(), - left_projection_indexes: &plan.left_projection_indexes, - right_projection_indexes: &plan.right_projection_indexes, - projections: &plan.projections, - column_names: Arc::clone(&plan.column_names), - lookup_row_id: *lookup_row_id, - pager: &self.inner.pager, - wal: &self.inner.wal, - snapshot_lsn, - use_persistent_pk_index: self.inner.config.persistent_pk_index, - }, - )?; - drop(runtime); - drop(reader); - Ok(result) - } - - fn try_execute_prepared_simple_scalar_filtered_aggregate( - &self, - prepared: &PreparedStatement, - params: &[Value], - ) -> Result> { - if self.inner.sql_txn_active.load(Ordering::Acquire) { - return Ok(None); - } - let Some(plan) = prepared.simple_scalar_filtered_aggregate.as_ref() else { - return Ok(None); - }; - let Some(Value::Int64(param_value)) = params.get(plan.param_index) else { - return Ok(None); - }; - let SqlStatement::Query(query) = prepared.statement.as_ref() else { - return Ok(None); - }; - - let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - self.refresh_engine_from_snapshot(snapshot_lsn)?; - let Some(runtime) = self.runtime_read_for_fast_read_at_snapshot(snapshot_lsn)? else { - return Ok(None); - }; - self.validate_prepared_schema_cookie( - prepared, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - let has_resident_source = runtime.table_row_source(plan.table_name.as_str()).is_some(); - if !has_resident_source && !runtime.has_deferred_tables() { - return Ok(None); - } - let state = runtime.persisted_table_state(plan.table_name.as_str()); - if !has_resident_source && state.is_none() { - return Ok(None); - }; - let state = state.unwrap_or_default(); - let key = PreparedScalarAggregateCacheKey { - snapshot_lsn, - pointer_head_page_id: state.pointer.head_page_id, - pointer_logical_len: state.pointer.logical_len, - pointer_flags: state.pointer.flags, - checksum: state.checksum, - row_count: state.row_count, - param_value: *param_value, - }; - if let Some(result) = plan - .cache - .lock() - .map_err(|_| DbError::internal("prepared aggregate cache lock poisoned"))? - .get(&key) - { - drop(runtime); - drop(reader); - return Ok(Some(result)); - } - - let result = if has_resident_source { - runtime.try_execute_simple_grouped_numeric_aggregate_query(query, params)? - } else { - runtime.try_execute_simple_deferred_paged_grouped_numeric_aggregate_query( - query, - params, - &self.inner.pager, - &self.inner.wal, - snapshot_lsn, - )? - }; - if let Some(result) = result.as_ref() { - plan.cache - .lock() - .map_err(|_| DbError::internal("prepared aggregate cache lock poisoned"))? - .insert(key, result.clone()); - } - drop(runtime); - drop(reader); - Ok(result) - } - - fn execute_prepared_read_statement( - &self, - prepared: &PreparedStatement, - params: &[Value], - ) -> Result { - if params.is_empty() { - if let Some(result) = - self.try_execute_prepared_simple_ordered_row_id_projection(prepared)? - { + ) -> Result { + if params.is_empty() { + if let Some(result) = + self.try_execute_prepared_simple_ordered_row_id_projection(prepared)? + { return Ok(result); } } @@ -7131,260 +5819,126 @@ impl Db { } } - fn execute_autocommit_temp_only_statement( - &self, - statement: &crate::sql::ast::Statement, - params: &[Value], - ) -> Result { - let mut working = self.engine_snapshot()?; - let result = working.execute_statement(statement, params, self.inner.config.page_size)?; - self.install_temp_runtime(working)?; - Ok(result) + fn can_use_autocommit_prepared_insert_fast_path(&self, _table_name: &str) -> Result { + Ok(true) } - fn execute_autocommit_insert_in_place( + fn try_execute_zero_row_index_delete_against_current_runtime( &self, - statement: &crate::sql::ast::Statement, + prepared_statement: &PreparedStatement, + prepared_delete: &PreparedSimpleDelete, params: &[Value], - ) -> Result { - let insert_table_names = if let crate::sql::ast::Statement::Insert(insert) = statement { - let table_names = { - self.refresh_engine_from_storage()?; - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - self.insert_dependency_table_names(&runtime, &insert.table_name)? - }; - let table_refs = table_names.iter().map(String::as_str).collect::>(); - self.load_simple_write_row_sources_at_latest_snapshot(&table_refs)?; - Some(table_names) - } else { - None + ) -> Result> { + let PreparedDeleteLookup::Index { + index_name, + value_source, + } = &prepared_delete.lookup + else { + return Ok(None); }; - let result = self.execute_autocommit_in_place(|runtime| { - runtime.execute_statement(statement, params, self.inner.config.page_size) - })?; - if let Some(table_names) = &insert_table_names { - let table_refs = table_names.iter().map(String::as_str).collect::>(); - self.redefer_persisted_tables_after_write(&table_refs)?; + let value = resolve_prepared_simple_value_for_fast_path(value_source, params)?; + if matches!(value, Value::Null) { + return Ok(Some(QueryResult::with_affected_rows(0))); } - Ok(result) - } - fn can_use_autocommit_prepared_insert_fast_path(&self, _table_name: &str) -> Result { - Ok(true) - } + let latest_lsn = self.inner.wal.latest_snapshot(); + let latest_checkpoint_epoch = self.inner.wal.checkpoint_epoch(); + let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); + let last_seen_checkpoint_epoch = self + .inner + .last_seen_checkpoint_epoch + .load(Ordering::Acquire); + if last_runtime_lsn != latest_lsn || last_seen_checkpoint_epoch != latest_checkpoint_epoch { + return Ok(None); + } - fn execute_autocommit_prepared_insert_in_place( - &self, - prepared: &PreparedSimpleInsert, - params: &[Value], - ) -> Result { - let table_names = { - self.refresh_engine_from_storage()?; - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - self.insert_dependency_table_names(&runtime, &prepared.table_name)? - }; - let table_refs = table_names.iter().map(String::as_str).collect::>(); - self.load_simple_write_row_sources_at_latest_snapshot(&table_refs)?; - let mut runtime = self + let runtime = self .inner .engine - .write() + .read() .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - self.configure_runtime_sync_capture(&mut runtime)?; - if !runtime.can_reuse_prepared_simple_insert(prepared) { - drop(runtime); - let result = self.execute_autocommit_in_place(|runtime| { - runtime.execute_prepared_simple_insert( - prepared, - params, - self.inner.config.page_size, - ) - })?; - self.redefer_persisted_tables_after_write(&table_refs)?; - return Ok(result); + self.validate_prepared_schema_cookie( + prepared_statement, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + if !runtime.can_reuse_prepared_simple_delete(prepared_delete) { + return Ok(None); } - let result = match runtime.execute_prepared_simple_insert( - prepared, - params, - self.inner.config.page_size, - ) { - Ok(result) => result, - Err(error) => { - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } + let Some(index_schema) = runtime.catalog.indexes.get(index_name) else { + return Ok(None); }; - runtime.rebuild_stale_indexes(self.inner.config.page_size)?; - let reactive_pending = self.take_reactive_pending_commit(&mut runtime); - self.begin_write()?; - if let Err(error) = runtime.persist_to_db(self) { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); + if !index_schema.fresh || index_schema.kind != IndexKind::Btree { + return Ok(None); } - let committed_lsn = match self.commit() { - Ok(lsn) => lsn, - Err(error) => { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } + let Some(RuntimeIndex::Btree { keys, .. }) = runtime.index(index_name) else { + return Ok(None); }; - if !runtime.sync_mutations.is_empty() { - self.sync_post_commit(&mut runtime, committed_lsn)?; + if keys.row_ids_for_value_set(&value)?.is_empty() { + return Ok(Some(QueryResult::with_affected_rows(0))); } - self.sync_temp_state_from_runtime(&runtime)?; - self.inner - .last_runtime_lsn - .store(committed_lsn, Ordering::Release); - self.inner - .writer_last_commit_lsn - .store(committed_lsn, Ordering::Release); - drop(runtime); - self.publish_reactive_commit(reactive_pending, committed_lsn); - self.redefer_persisted_tables_after_write(&table_refs)?; - Ok(result) + Ok(None) } - fn execute_autocommit_simple_update_in_place( - &self, - prepared_update: &PreparedSimpleUpdate, - params: &[Value], - ) -> Result { - self.load_simple_write_row_sources_at_latest_snapshot(&[prepared_update - .table_name - .as_str()])?; - let mut runtime = self - .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - self.configure_runtime_sync_capture(&mut runtime)?; - if !runtime.can_reuse_prepared_simple_update(prepared_update) { - drop(runtime); - let result = self.execute_autocommit_in_place(|runtime| { - runtime.execute_prepared_simple_update( - prepared_update, - params, - self.inner.config.page_size, - ) - })?; - self.redefer_persisted_tables_after_write(&[prepared_update.table_name.as_str()])?; - return Ok(result); - } - let result = match runtime.execute_prepared_simple_update( - prepared_update, - params, - self.inner.config.page_size, - ) { - Ok(result) => result, - Err(error) => { - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } - }; - if result.affected_rows() == 0 && !self.runtime_has_persistent_commit_work(&runtime)? { - self.sync_temp_state_from_runtime(&runtime)?; - drop(runtime); - self.redefer_persisted_tables_after_write(&[prepared_update.table_name.as_str()])?; - return Ok(result); - } - if !prepared_update - .indexes - .iter() - .all(|index| runtime.prepared_btree_index_is_fresh(index)) - { - runtime.rebuild_stale_indexes(self.inner.config.page_size)?; + fn runtime_has_persistent_commit_work(&self, runtime: &EngineRuntime) -> Result { + if !runtime.dirty_tables.is_empty() { + return Ok(true); } - let reactive_pending = self.take_reactive_pending_commit(&mut runtime); - self.begin_write()?; - if let Err(error) = runtime.persist_to_db(self) { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); + Ok(self.inner.catalog.schema_cookie()? != runtime.catalog.schema_cookie) + } + + fn runtime_has_stale_indexes(runtime: &EngineRuntime) -> bool { + runtime.catalog.indexes.iter().any(|(name, index)| { + let table_deferred = runtime + .deferred_table_names() + .any(|table_name| identifiers_equal(table_name, &index.table_name)); + !table_deferred && (!index.fresh || !runtime.indexes.contains_key(name)) + }) + } + + fn backfill_missing_persistent_pk_index_for_table(&self, table_name: &str) -> Result<()> { + if !self.inner.config.persistent_pk_index { + return Ok(()); } - let committed_lsn = match self.commit() { - Ok(lsn) => lsn, - Err(error) => { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } - }; - if !runtime.sync_mutations.is_empty() { - self.sync_post_commit(&mut runtime, committed_lsn)?; + if self.inner.catalog.schema_cookie()? == 0 { + return Ok(()); } - self.sync_temp_state_from_runtime(&runtime)?; - self.inner - .last_runtime_lsn - .store(committed_lsn, Ordering::Release); - self.inner - .writer_last_commit_lsn - .store(committed_lsn, Ordering::Release); - drop(runtime); - self.publish_reactive_commit(reactive_pending, committed_lsn); - self.redefer_persisted_tables_after_write(&[prepared_update.table_name.as_str()])?; - Ok(result) - } - fn execute_autocommit_simple_delete_in_place( - &self, - prepared_delete: &PreparedSimpleDelete, - params: &[Value], - ) -> Result { - let table_names = prepared_delete.affected_table_names(); - let row_source_table_names = prepared_delete.required_row_source_table_names(); - let child_index_targets = prepared_delete.child_index_hydration_targets(); - self.load_simple_write_row_sources_and_child_indexes_at_latest_snapshot( - &row_source_table_names, - &child_index_targets, - )?; let mut runtime = self .inner .engine .write() .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - self.configure_runtime_sync_capture(&mut runtime)?; - if !runtime.can_reuse_prepared_simple_delete(prepared_delete) { - drop(runtime); - let result = self.execute_autocommit_in_place(|runtime| { - runtime.execute_prepared_simple_delete( - prepared_delete, - params, - self.inner.config.page_size, - ) - })?; - self.redefer_persisted_tables_after_write(&table_names)?; - return Ok(result); + let needs_backfill = runtime + .catalog + .tables + .iter() + .find(|(candidate, _)| identifiers_equal(candidate, table_name)) + .is_some_and(|(canonical_name, table)| { + table.pk_index_root.is_none() + && runtime + .persisted_tables + .get(canonical_name) + .is_some_and(|state| state.pointer.head_page_id != 0) + }); + if !needs_backfill { + return Ok(()); } - let result = match runtime.execute_prepared_simple_delete( - prepared_delete, - params, - self.inner.config.page_size, - ) { - Ok(result) => result, + + self.begin_write()?; + let changed = match runtime.backfill_missing_persistent_pk_index_for_table(self, table_name) + { + Ok(changed) => changed, Err(error) => { + let _ = self.rollback(); self.restore_runtime_from_storage(&mut runtime)?; return Err(error); } }; - if result.affected_rows() == 0 && !self.runtime_has_persistent_commit_work(&runtime)? { - self.sync_temp_state_from_runtime(&runtime)?; - drop(runtime); - self.redefer_persisted_tables_after_write(&table_names)?; - return Ok(result); + if !changed { + self.rollback()?; + return Ok(()); } - runtime.rebuild_stale_indexes(self.inner.config.page_size)?; - let reactive_pending = self.take_reactive_pending_commit(&mut runtime); - self.begin_write()?; if let Err(error) = runtime.persist_to_db(self) { let _ = self.rollback(); self.restore_runtime_from_storage(&mut runtime)?; @@ -7398,8 +5952,11 @@ impl Db { return Err(error); } }; - if !runtime.sync_mutations.is_empty() { - self.sync_post_commit(&mut runtime, committed_lsn)?; + let runtime_schema_cookie = runtime.catalog.schema_cookie; + if self.inner.catalog.schema_cookie()? != runtime_schema_cookie { + self.inner + .catalog + .replace(runtime.catalog.as_ref().clone())?; } self.sync_temp_state_from_runtime(&runtime)?; self.inner @@ -7408,83 +5965,67 @@ impl Db { self.inner .writer_last_commit_lsn .store(committed_lsn, Ordering::Release); - drop(runtime); - self.publish_reactive_commit(reactive_pending, committed_lsn); - self.redefer_persisted_tables_after_write(&table_names)?; - Ok(result) + Ok(()) } - fn try_execute_autocommit_prepared_insert_in_place( - &self, - prepared_statement: &PreparedStatement, - prepared_insert: &PreparedSimpleInsert, - params: &[Value], - ) -> Result> { - if !self.can_use_autocommit_prepared_insert_fast_path(&prepared_insert.table_name)? { - return Ok(None); + fn backfill_paged_row_storage(&self) -> Result<()> { + if !self.inner.config.paged_row_storage { + return Ok(()); } - let single_table = [prepared_insert.table_name.as_str()]; - let mut dependency_tables = Vec::new(); - let table_refs: &[&str] = if prepared_insert.row_source_dependency_tables.is_empty() { - &single_table - } else { - dependency_tables.reserve(prepared_insert.row_source_dependency_tables.len() + 1); - dependency_tables.push(prepared_insert.table_name.as_str()); - for parent_table_name in &prepared_insert.row_source_dependency_tables { - if !dependency_tables - .iter() - .any(|name| identifiers_equal(name, parent_table_name)) - { - dependency_tables.push(parent_table_name.as_str()); - } - } - &dependency_tables - }; - self.load_simple_write_row_sources_at_latest_snapshot(table_refs)?; + if self.inner.catalog.schema_cookie()? == 0 { + return Ok(()); + } + let mut runtime = self .inner .engine .write() .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - self.configure_runtime_sync_capture(&mut runtime)?; - self.validate_prepared_schema_cookie( - prepared_statement, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - if !runtime.can_reuse_prepared_simple_insert(prepared_insert) { - return Ok(None); + let needs_backfill = runtime.persisted_tables.values().any(|state| { + state.pointer.head_page_id != 0 && !state.pointer.is_table_paged_manifest() + }); + if !needs_backfill { + return Ok(()); } - let result = match runtime.execute_prepared_simple_insert( - prepared_insert, - params, - self.inner.config.page_size, - ) { - Ok(result) => result, + + let base_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); + let base_checkpoint_epoch = self + .inner + .last_seen_checkpoint_epoch + .load(Ordering::Acquire); + self.begin_write()?; + let changed = match runtime.backfill_paged_row_storage(self) { + Ok(changed) => changed, Err(error) => { + let _ = self.rollback(); self.restore_runtime_from_storage(&mut runtime)?; return Err(error); } }; - runtime.rebuild_stale_indexes(self.inner.config.page_size)?; - let reactive_pending = self.take_reactive_pending_commit(&mut runtime); - self.begin_write()?; + if !changed { + self.rollback()?; + return Ok(()); + } if let Err(error) = runtime.persist_to_db(self) { let _ = self.rollback(); self.restore_runtime_from_storage(&mut runtime)?; return Err(error); } - let committed_lsn = match self.commit() { + let committed_lsn = match self.commit_if_latest(base_lsn, base_checkpoint_epoch) { Ok(lsn) => lsn, Err(error) => { - let _ = self.rollback(); self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); + return if matches!(&error, DbError::Transaction { message } if message.starts_with("transaction conflict: WAL advanced")) + { + Ok(()) + } else { + Err(error) + }; } }; - if !runtime.sync_mutations.is_empty() { - self.sync_post_commit(&mut runtime, committed_lsn)?; - } + self.inner + .catalog + .replace(runtime.catalog.as_ref().clone())?; self.sync_temp_state_from_runtime(&runtime)?; self.inner .last_runtime_lsn @@ -7492,73 +6033,37 @@ impl Db { self.inner .writer_last_commit_lsn .store(committed_lsn, Ordering::Release); - drop(runtime); - self.publish_reactive_commit(reactive_pending, committed_lsn); - self.redefer_persisted_tables_after_write(table_refs)?; - Ok(Some(result)) + Ok(()) } - fn try_execute_autocommit_prepared_insert_in_place_mut( - &self, - prepared_statement: &PreparedStatement, - prepared_insert: &PreparedSimpleInsert, - params: &mut [Value], - ) -> Result> { - if !self.can_use_autocommit_prepared_insert_fast_path(&prepared_insert.table_name)? - || !Self::prepared_insert_uses_direct_positional_params(prepared_insert, params.len()) - { - return Ok(None); + fn compact_persisted_payloads_before_checkpoint(&self) -> Result<()> { + if self.inner.sql_txn_active.load(Ordering::Acquire) { + return Ok(()); } - let single_table = [prepared_insert.table_name.as_str()]; - let mut dependency_tables = Vec::new(); - let table_refs: &[&str] = if prepared_insert.row_source_dependency_tables.is_empty() { - &single_table - } else { - dependency_tables.reserve(prepared_insert.row_source_dependency_tables.len() + 1); - dependency_tables.push(prepared_insert.table_name.as_str()); - for parent_table_name in &prepared_insert.row_source_dependency_tables { - if !dependency_tables - .iter() - .any(|name| identifiers_equal(name, parent_table_name)) - { - dependency_tables.push(parent_table_name.as_str()); - } - } - &dependency_tables - }; - self.load_simple_write_row_sources_at_latest_snapshot(table_refs)?; + self.refresh_engine_from_storage()?; let mut runtime = self .inner .engine .write() .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - self.configure_runtime_sync_capture(&mut runtime)?; - self.validate_prepared_schema_cookie( - prepared_statement, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - if !runtime.can_reuse_prepared_simple_insert(prepared_insert) { - return Ok(None); + { + let store = PagerReadStore::new(self)?; + if !runtime.has_checkpoint_compaction_candidates(&store, self.config())? { + return Ok(()); + } } - let affected = match runtime.execute_prepared_simple_insert_positional_params_in_place( - prepared_insert, - params, - self.inner.config.page_size, - ) { - Ok(affected) => affected, + self.begin_write()?; + let changed = match runtime.compact_persisted_payloads_for_checkpoint(self) { + Ok(changed) => changed, Err(error) => { + let _ = self.rollback(); self.restore_runtime_from_storage(&mut runtime)?; return Err(error); } }; - runtime.rebuild_stale_indexes(self.inner.config.page_size)?; - let reactive_pending = self.take_reactive_pending_commit(&mut runtime); - self.begin_write()?; - if let Err(error) = runtime.persist_to_db(self) { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); + if !changed { + self.rollback()?; + return Ok(()); } let committed_lsn = match self.commit() { Ok(lsn) => lsn, @@ -7568,9 +6073,6 @@ impl Db { return Err(error); } }; - if !runtime.sync_mutations.is_empty() { - self.sync_post_commit(&mut runtime, committed_lsn)?; - } self.sync_temp_state_from_runtime(&runtime)?; self.inner .last_runtime_lsn @@ -7578,3960 +6080,1152 @@ impl Db { self.inner .writer_last_commit_lsn .store(committed_lsn, Ordering::Release); - let redefer_after_write = - self.runtime_should_redefer_persisted_tables_after_write(&runtime, table_refs); - drop(runtime); - self.publish_reactive_commit(reactive_pending, committed_lsn); - if redefer_after_write { - self.redefer_persisted_tables_after_write(table_refs)?; - } - Ok(Some(QueryResult::with_affected_rows(affected))) + Ok(()) } - fn try_execute_autocommit_prepared_update_in_place( - &self, - prepared_statement: &PreparedStatement, - prepared_update: &PreparedSimpleUpdate, - params: &[Value], - ) -> Result> { - self.load_simple_write_row_sources_at_latest_snapshot(&[prepared_update - .table_name - .as_str()])?; - let mut runtime = self + fn engine_snapshot(&self) -> Result { + let mut snapshot = self.engine_snapshot_without_index_rebuild()?; + snapshot.rebuild_stale_indexes(self.inner.config.page_size)?; + Ok(snapshot) + } + + fn engine_snapshot_without_index_rebuild(&self) -> Result { + let runtime = self .inner .engine - .write() + .read() .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - self.configure_runtime_sync_capture(&mut runtime)?; - self.validate_prepared_schema_cookie( - prepared_statement, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - if !runtime.can_reuse_prepared_simple_update(prepared_update) { - return Ok(None); - } - let result = match runtime.execute_prepared_simple_update( - prepared_update, - params, - self.inner.config.page_size, - ) { - Ok(result) => result, - Err(error) => { - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } - }; - if result.affected_rows() == 0 && !self.runtime_has_persistent_commit_work(&runtime)? { - self.sync_temp_state_from_runtime(&runtime)?; - drop(runtime); - self.redefer_persisted_tables_after_write(&[prepared_update.table_name.as_str()])?; - return Ok(Some(result)); - } - if !prepared_update - .indexes - .iter() - .all(|index| runtime.prepared_btree_index_is_fresh(index)) - { - runtime.rebuild_stale_indexes(self.inner.config.page_size)?; - } - let reactive_pending = self.take_reactive_pending_commit(&mut runtime); - self.begin_write()?; - if let Err(error) = runtime.persist_to_db(self) { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } - let committed_lsn = match self.commit() { - Ok(lsn) => lsn, - Err(error) => { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } - }; - if !runtime.sync_mutations.is_empty() { - self.sync_post_commit(&mut runtime, committed_lsn)?; - } - let runtime_schema_cookie = runtime.catalog.schema_cookie; - if self.inner.catalog.schema_cookie()? != runtime_schema_cookie { - self.inner - .catalog - .replace(runtime.catalog.as_ref().clone())?; - } - self.sync_temp_state_from_runtime(&runtime)?; - self.inner - .last_runtime_lsn - .store(committed_lsn, Ordering::Release); + let mut snapshot = runtime.clone(); + self.apply_temp_state_to_runtime(&mut snapshot)?; + Ok(snapshot) + } + + #[cfg(test)] + pub(crate) fn debug_engine_snapshot(&self) -> Result { + self.engine_snapshot() + } + + fn apply_temp_state_to_runtime(&self, runtime: &mut EngineRuntime) -> Result<()> { self.inner - .writer_last_commit_lsn - .store(committed_lsn, Ordering::Release); - drop(runtime); - self.publish_reactive_commit(reactive_pending, committed_lsn); - self.redefer_persisted_tables_after_write(&[prepared_update.table_name.as_str()])?; - Ok(Some(result)) + .temp_state + .lock() + .map_err(|_| DbError::internal("temp schema lock poisoned"))? + .apply_to_runtime(runtime); + Ok(()) } - fn try_execute_autocommit_prepared_delete_in_place( - &self, - prepared_statement: &PreparedStatement, - prepared_delete: &PreparedSimpleDelete, - params: &[Value], - ) -> Result> { - if let Some(result) = self.try_execute_zero_row_index_delete_against_current_runtime( - prepared_statement, - prepared_delete, - params, - )? { - return Ok(Some(result)); - } - let table_names = prepared_delete.affected_table_names(); - let row_source_table_names = prepared_delete.required_row_source_table_names(); - let child_index_targets = prepared_delete.child_index_hydration_targets(); - self.load_simple_write_row_sources_and_child_indexes_at_latest_snapshot( - &row_source_table_names, - &child_index_targets, - )?; - let mut runtime = self + fn install_temp_runtime(&self, runtime: EngineRuntime) -> Result<()> { + self.sync_temp_state_from_runtime(&runtime)?; + let mut guard = self .inner .engine .write() .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - self.configure_runtime_sync_capture(&mut runtime)?; - self.validate_prepared_schema_cookie( - prepared_statement, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - if !runtime.can_reuse_prepared_simple_delete(prepared_delete) { - return Ok(None); - } - let result = match runtime.execute_prepared_simple_delete( - prepared_delete, - params, - self.inner.config.page_size, - ) { - Ok(result) => result, - Err(error) => { - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } - }; - if result.affected_rows() == 0 && !self.runtime_has_persistent_commit_work(&runtime)? { - self.sync_temp_state_from_runtime(&runtime)?; - drop(runtime); - self.redefer_persisted_tables_after_write(&table_names)?; - return Ok(Some(result)); - } - runtime.rebuild_stale_indexes(self.inner.config.page_size)?; - let reactive_pending = self.take_reactive_pending_commit(&mut runtime); - self.begin_write()?; - if let Err(error) = runtime.persist_to_db(self) { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } - let committed_lsn = match self.commit() { - Ok(lsn) => lsn, - Err(error) => { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } - }; - if !runtime.sync_mutations.is_empty() { - self.sync_post_commit(&mut runtime, committed_lsn)?; - } - let runtime_schema_cookie = runtime.catalog.schema_cookie; - if self.inner.catalog.schema_cookie()? != runtime_schema_cookie { - self.inner - .catalog - .replace(runtime.catalog.as_ref().clone())?; - } - self.sync_temp_state_from_runtime(&runtime)?; - self.inner - .last_runtime_lsn - .store(committed_lsn, Ordering::Release); - self.inner - .writer_last_commit_lsn - .store(committed_lsn, Ordering::Release); - drop(runtime); - self.publish_reactive_commit(reactive_pending, committed_lsn); - self.redefer_persisted_tables_after_write(&table_names)?; - Ok(Some(result)) + *guard = runtime; + Ok(()) } - fn try_execute_zero_row_index_delete_against_current_runtime( + fn statement_is_temp_only( &self, - prepared_statement: &PreparedStatement, - prepared_delete: &PreparedSimpleDelete, - params: &[Value], - ) -> Result> { - let PreparedDeleteLookup::Index { - index_name, - value_source, - } = &prepared_delete.lookup - else { - return Ok(None); + runtime: &EngineRuntime, + statement: &crate::sql::ast::Statement, + ) -> bool { + let temp_has_table = |name: &str| { + runtime + .temp_tables + .keys() + .any(|entry| identifiers_equal(entry, name)) }; - let value = resolve_prepared_simple_value_for_fast_path(value_source, params)?; - if matches!(value, Value::Null) { - return Ok(Some(QueryResult::with_affected_rows(0))); + let temp_has_view = |name: &str| { + runtime + .temp_views + .keys() + .any(|entry| identifiers_equal(entry, name)) + }; + match statement { + crate::sql::ast::Statement::CreateTable(statement) => statement.temporary, + crate::sql::ast::Statement::CreateTableAs(statement) => { + statement.temporary && self.query_is_temp_only(runtime, &statement.query) + } + crate::sql::ast::Statement::CreateView(statement) => { + statement.temporary && self.query_is_temp_only(runtime, &statement.query) + } + crate::sql::ast::Statement::DropTable { name, .. } => temp_has_table(name), + crate::sql::ast::Statement::DropView { name, .. } => temp_has_view(name), + crate::sql::ast::Statement::Query(_) + | crate::sql::ast::Statement::Insert(_) + | crate::sql::ast::Statement::Update(_) + | crate::sql::ast::Statement::Delete(_) => { + self.safe_referenced_names_are_temp_only(runtime, statement) + } + _ => false, } + } - let latest_lsn = self.inner.wal.latest_snapshot(); - let latest_checkpoint_epoch = self.inner.wal.checkpoint_epoch(); - let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); - let last_seen_checkpoint_epoch = self - .inner - .last_seen_checkpoint_epoch - .load(Ordering::Acquire); - if last_runtime_lsn != latest_lsn || last_seen_checkpoint_epoch != latest_checkpoint_epoch { - return Ok(None); - } + fn query_is_temp_only(&self, runtime: &EngineRuntime, query: &crate::sql::ast::Query) -> bool { + self.safe_referenced_names_are_temp_only( + runtime, + &crate::sql::ast::Statement::Query(query.clone()), + ) + } - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - self.validate_prepared_schema_cookie( - prepared_statement, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - if !runtime.can_reuse_prepared_simple_delete(prepared_delete) { - return Ok(None); - } - let Some(index_schema) = runtime.catalog.indexes.get(index_name) else { - return Ok(None); - }; - if !index_schema.fresh || index_schema.kind != IndexKind::Btree { - return Ok(None); - } - let Some(RuntimeIndex::Btree { keys, .. }) = runtime.index(index_name) else { - return Ok(None); + fn safe_referenced_names_are_temp_only( + &self, + runtime: &EngineRuntime, + statement: &crate::sql::ast::Statement, + ) -> bool { + let Some(names) = crate::sql::ast::safe_referenced_tables(statement) else { + return false; }; - if keys.row_ids_for_value_set(&value)?.is_empty() { - return Ok(Some(QueryResult::with_affected_rows(0))); - } - Ok(None) + let mut visiting_views = BTreeSet::new(); + names + .into_iter() + .all(|name| self.referenced_name_is_temp_only(runtime, &name, &mut visiting_views)) } - fn execute_autocommit_in_place(&self, apply: F) -> Result - where - F: FnOnce(&mut EngineRuntime) -> Result, - { - let mut runtime = self - .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - self.configure_runtime_sync_capture(&mut runtime)?; - let result = match apply(&mut runtime) { - Ok(result) => result, - Err(error) => { - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } - }; - if !self.runtime_has_persistent_commit_work(&runtime)? - && runtime.sync_mutations.is_empty() - && !Self::runtime_has_stale_indexes(&runtime) + fn referenced_name_is_temp_only( + &self, + runtime: &EngineRuntime, + name: &str, + visiting_views: &mut BTreeSet, + ) -> bool { + if runtime + .temp_tables + .keys() + .any(|entry| identifiers_equal(entry, name)) { - self.sync_temp_state_from_runtime(&runtime)?; - return Ok(result); - } - runtime.rebuild_stale_indexes(self.inner.config.page_size)?; - let reactive_pending = self.take_reactive_pending_commit(&mut runtime); - self.begin_write()?; - if let Err(error) = runtime.persist_to_db(self) { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); + return true; } - let committed_lsn = match self.commit() { - Ok(lsn) => lsn, - Err(error) => { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } + let Some((view_name, view)) = runtime + .temp_views + .iter() + .find(|(entry, _)| identifiers_equal(entry, name)) + else { + return false; }; - self.sync_post_commit(&mut runtime, committed_lsn)?; - let runtime_schema_cookie = runtime.catalog.schema_cookie; - if self.inner.catalog.schema_cookie()? != runtime_schema_cookie { - self.inner - .catalog - .replace(runtime.catalog.as_ref().clone())?; - } - self.sync_temp_state_from_runtime(&runtime)?; - self.inner - .last_runtime_lsn - .store(committed_lsn, Ordering::Release); - self.inner - .writer_last_commit_lsn - .store(committed_lsn, Ordering::Release); - drop(runtime); - self.publish_reactive_commit(reactive_pending, committed_lsn); - Ok(result) - } - - fn runtime_has_persistent_commit_work(&self, runtime: &EngineRuntime) -> Result { - if !runtime.dirty_tables.is_empty() { - return Ok(true); + if !visiting_views.insert(view_name.clone()) { + return false; } - Ok(self.inner.catalog.schema_cookie()? != runtime.catalog.schema_cookie) - } - - fn runtime_has_stale_indexes(runtime: &EngineRuntime) -> bool { - runtime.catalog.indexes.iter().any(|(name, index)| { - let table_deferred = runtime - .deferred_table_names() - .any(|table_name| identifiers_equal(table_name, &index.table_name)); - !table_deferred && (!index.fresh || !runtime.indexes.contains_key(name)) - }) + let temp_only = view.dependencies.iter().all(|dependency| { + self.referenced_name_is_temp_only(runtime, dependency, visiting_views) + }); + visiting_views.remove(view_name); + temp_only } - fn backfill_missing_persistent_pk_index_for_table(&self, table_name: &str) -> Result<()> { - if !self.inner.config.persistent_pk_index { - return Ok(()); - } - if self.inner.catalog.schema_cookie()? == 0 { - return Ok(()); + fn parsed_statement(&self, sql: &str) -> Result> { + // Try the connection-local plan cache first. The cache is keyed + // by the prepared SQL text plus the current schema cookies and + // policy/mask generation; on a hit we still get a fresh + // `PreparedStatement` (which is cheap to construct) but we + // skip the parse step. + let prepared_sql = prepared_statement_sql(sql)?; + let parameter_shape = parameter_shape_for_prepared_sql(&prepared_sql); + if parameter_shape.arity() == 0 { + return self + .inner + .statement_cache + .lock() + .map_err(|_| DbError::internal("statement cache lock poisoned"))? + .get_or_parse(&prepared_sql); } - let mut runtime = self + let temp_cookie = self .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let needs_backfill = runtime - .catalog - .tables - .iter() - .find(|(candidate, _)| identifiers_equal(candidate, table_name)) - .is_some_and(|(canonical_name, table)| { - table.pk_index_root.is_none() - && runtime - .persisted_tables - .get(canonical_name) - .is_some_and(|state| state.pointer.head_page_id != 0) - }); - if !needs_backfill { - return Ok(()); - } - - self.begin_write()?; - let changed = match runtime.backfill_missing_persistent_pk_index_for_table(self, table_name) - { - Ok(changed) => changed, - Err(error) => { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } - }; - if !changed { - self.rollback()?; - return Ok(()); - } - if let Err(error) = runtime.persist_to_db(self) { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); + .temp_state + .lock() + .map(|s| s.schema_cookie) + .unwrap_or(0); + let persistent_cookie = self.inner.catalog.schema_cookie()?; + let policy_gen = self.inner.policy_mask_generation.current(); + let mut plan_cache = self + .inner + .plan_cache + .lock() + .map_err(|_| DbError::internal("plan cache lock poisoned"))?; + let key = crate::plan_cache::PlanCacheKey::new( + prepared_sql, + parameter_shape, + persistent_cookie, + temp_cookie, + policy_gen, + ); + let current_key = key.clone(); + if let Some(statement) = plan_cache.get(&key, persistent_cookie, temp_cookie, policy_gen) { + return Ok(statement); } - let committed_lsn = match self.commit() { - Ok(lsn) => lsn, - Err(error) => { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); + drop(plan_cache); + // Fall back to the existing narrow statement cache for parse + // work, then store the parsed statement in the plan cache. + let statement = self + .inner + .statement_cache + .lock() + .map_err(|_| DbError::internal("statement cache lock poisoned"))? + .get_or_parse(¤t_key.sql_text)?; + if Self::statement_can_enter_plan_cache(statement.as_ref()) { + if let Ok(mut plan_cache) = self.inner.plan_cache.lock() { + if plan_cache.should_admit_missed_key(¤t_key) { + let size = crate::plan_cache::statement_accounted_size(&statement); + plan_cache.insert(current_key, Arc::clone(&statement), size); + } } - }; - let runtime_schema_cookie = runtime.catalog.schema_cookie; - if self.inner.catalog.schema_cookie()? != runtime_schema_cookie { - self.inner - .catalog - .replace(runtime.catalog.as_ref().clone())?; } - self.sync_temp_state_from_runtime(&runtime)?; - self.inner - .last_runtime_lsn - .store(committed_lsn, Ordering::Release); - self.inner - .writer_last_commit_lsn - .store(committed_lsn, Ordering::Release); - Ok(()) + Ok(statement) } - fn backfill_paged_row_storage(&self) -> Result<()> { - if !self.inner.config.paged_row_storage { - return Ok(()); - } - if self.inner.catalog.schema_cookie()? == 0 { - return Ok(()); - } - - let mut runtime = self - .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let needs_backfill = runtime.persisted_tables.values().any(|state| { - state.pointer.head_page_id != 0 && !state.pointer.is_table_paged_manifest() - }); - if !needs_backfill { - return Ok(()); - } - - let base_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); - let base_checkpoint_epoch = self + fn prepare_with_runtime( + &self, + sql: &str, + runtime: &EngineRuntime, + ) -> Result { + let prepared_sql = prepared_statement_sql(sql)?; + let policy_gen = self.inner.policy_mask_generation.current(); + let key = crate::plan_cache::PlanCacheKey::new( + prepared_sql.clone(), + parameter_shape_for_prepared_sql(&prepared_sql), + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + policy_gen, + ); + if let Some(bundle) = self .inner - .last_seen_checkpoint_epoch - .load(Ordering::Acquire); - self.begin_write()?; - let changed = match runtime.backfill_paged_row_storage(self) { - Ok(changed) => changed, - Err(error) => { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } - }; - if !changed { - self.rollback()?; - return Ok(()); - } - if let Err(error) = runtime.persist_to_db(self) { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); + .prepared_plan_cache + .lock() + .map_err(|_| DbError::internal("prepared plan cache lock poisoned"))? + .get( + &key, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + policy_gen, + ) + { + return Ok(PreparedStatement { + db: self.clone(), + schema_cookie: runtime.catalog.schema_cookie, + temp_schema_cookie: runtime.temp_schema_cookie, + statement: Arc::clone(&bundle.statement), + prepared_sql, + simple_row_id_projection: bundle.simple_row_id_projection, + simple_indexed_projection: bundle.simple_indexed_projection, + simple_row_id_range_projection: bundle.simple_row_id_range_projection, + simple_ordered_row_id_projection: bundle.simple_ordered_row_id_projection, + simple_row_id_join_projection: bundle.simple_row_id_join_projection, + simple_scalar_filtered_aggregate: bundle.simple_scalar_filtered_aggregate, + prepared_insert: bundle.prepared_insert, + prepared_update: bundle.prepared_update, + prepared_delete: bundle.prepared_delete, + read_only: bundle.read_only, + }); } - let committed_lsn = match self.commit_if_latest(base_lsn, base_checkpoint_epoch) { - Ok(lsn) => lsn, - Err(error) => { - self.restore_runtime_from_storage(&mut runtime)?; - return if matches!(&error, DbError::Transaction { message } if message.starts_with("transaction conflict: WAL advanced")) - { - Ok(()) - } else { - Err(error) + if let Some(request) = parse_simple_row_id_range_delete_sql(&prepared_sql) { + if let Some(prepared_delete) = runtime.prepare_simple_row_id_range_delete( + &request.table_name, + &request.column_name, + request.low, + request.high, + )? { + let statement = Arc::new(simple_row_id_range_delete_statement(&request)); + let bundle = PreparedPlanBundle { + statement: Arc::clone(&statement), + simple_row_id_projection: None, + simple_indexed_projection: None, + simple_row_id_range_projection: None, + simple_ordered_row_id_projection: None, + simple_row_id_join_projection: None, + simple_scalar_filtered_aggregate: None, + prepared_insert: None, + prepared_update: None, + prepared_delete: Some(Arc::new(prepared_delete)), + read_only: false, }; - } - }; - self.inner - .catalog - .replace(runtime.catalog.as_ref().clone())?; - self.sync_temp_state_from_runtime(&runtime)?; - self.inner - .last_runtime_lsn - .store(committed_lsn, Ordering::Release); - self.inner - .writer_last_commit_lsn - .store(committed_lsn, Ordering::Release); - Ok(()) - } - - fn compact_persisted_payloads_before_checkpoint(&self) -> Result<()> { - if self.inner.sql_txn_active.load(Ordering::Acquire) { - return Ok(()); - } - self.refresh_engine_from_storage()?; - let mut runtime = self - .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - { - let store = PagerReadStore::new(self)?; - if !runtime.has_checkpoint_compaction_candidates(&store, self.config())? { - return Ok(()); + if let Ok(mut cache) = self.inner.prepared_plan_cache.lock() { + cache.insert( + key, + bundle.clone(), + Self::prepared_plan_accounted_size(&bundle), + ); + } + return Ok(PreparedStatement { + db: self.clone(), + schema_cookie: runtime.catalog.schema_cookie, + temp_schema_cookie: runtime.temp_schema_cookie, + statement, + prepared_sql: prepared_sql.clone(), + simple_row_id_projection: None, + simple_indexed_projection: None, + simple_row_id_range_projection: None, + simple_ordered_row_id_projection: None, + simple_row_id_join_projection: None, + simple_scalar_filtered_aggregate: None, + prepared_insert: None, + prepared_update: None, + prepared_delete: bundle.prepared_delete, + read_only: false, + }); } } - self.begin_write()?; - let changed = match runtime.compact_persisted_payloads_for_checkpoint(self) { - Ok(changed) => changed, - Err(error) => { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } + let statement = self.parsed_statement(&prepared_sql)?; + let read_only = statement_is_read_only(statement.as_ref()); + let (prepared_insert, prepared_update, prepared_delete) = match statement.as_ref() { + SqlStatement::Insert(insert) => ( + self.prepared_simple_insert(&prepared_sql, insert, runtime)?, + None, + None, + ), + SqlStatement::Update(update) => ( + None, + runtime.prepare_simple_update(update)?.map(Arc::new), + None, + ), + SqlStatement::Delete(delete) => ( + None, + None, + runtime.prepare_simple_delete(delete)?.map(Arc::new), + ), + _ => (None, None, None), }; - if !changed { - self.rollback()?; - return Ok(()); - } - let committed_lsn = match self.commit() { - Ok(lsn) => lsn, - Err(error) => { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut runtime)?; - return Err(error); - } + let simple_row_id_projection = + Self::prepared_simple_row_id_projection(&prepared_sql, runtime); + let simple_indexed_projection = + Self::prepared_simple_indexed_projection(statement.as_ref(), runtime); + let simple_row_id_range_projection = + Self::prepared_simple_row_id_range_projection(&prepared_sql, runtime); + let simple_ordered_row_id_projection = + Self::prepared_simple_ordered_row_id_projection(statement.as_ref(), runtime); + let simple_row_id_join_projection = + Self::prepared_simple_row_id_join_projection(statement.as_ref(), runtime); + let simple_scalar_filtered_aggregate = + Self::prepared_simple_scalar_filtered_aggregate(statement.as_ref(), runtime); + let bundle = PreparedPlanBundle { + statement: Arc::clone(&statement), + simple_row_id_projection, + simple_indexed_projection, + simple_row_id_range_projection, + simple_ordered_row_id_projection, + simple_row_id_join_projection, + simple_scalar_filtered_aggregate, + prepared_insert, + prepared_update, + prepared_delete, + read_only, }; - self.sync_temp_state_from_runtime(&runtime)?; - self.inner - .last_runtime_lsn - .store(committed_lsn, Ordering::Release); - self.inner - .writer_last_commit_lsn - .store(committed_lsn, Ordering::Release); - Ok(()) - } - - fn engine_snapshot(&self) -> Result { - let mut snapshot = self.engine_snapshot_without_index_rebuild()?; - snapshot.rebuild_stale_indexes(self.inner.config.page_size)?; - Ok(snapshot) - } - - fn engine_snapshot_without_index_rebuild(&self) -> Result { - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let mut snapshot = runtime.clone(); - self.apply_temp_state_to_runtime(&mut snapshot)?; - Ok(snapshot) - } - - #[cfg(test)] - pub(crate) fn debug_engine_snapshot(&self) -> Result { - self.engine_snapshot() - } - - fn apply_temp_state_to_runtime(&self, runtime: &mut EngineRuntime) -> Result<()> { - self.inner - .temp_state - .lock() - .map_err(|_| DbError::internal("temp schema lock poisoned"))? - .apply_to_runtime(runtime); - Ok(()) + if Self::statement_can_enter_plan_cache(bundle.statement.as_ref()) { + if let Ok(mut cache) = self.inner.prepared_plan_cache.lock() { + cache.insert( + key, + bundle.clone(), + Self::prepared_plan_accounted_size(&bundle), + ); + } + } + Ok(PreparedStatement { + db: self.clone(), + schema_cookie: runtime.catalog.schema_cookie, + temp_schema_cookie: runtime.temp_schema_cookie, + statement: Arc::clone(&statement), + prepared_sql: prepared_sql.clone(), + simple_row_id_projection: bundle.simple_row_id_projection, + simple_indexed_projection: bundle.simple_indexed_projection, + simple_row_id_range_projection: bundle.simple_row_id_range_projection, + simple_ordered_row_id_projection: bundle.simple_ordered_row_id_projection, + simple_row_id_join_projection: bundle.simple_row_id_join_projection, + simple_scalar_filtered_aggregate: bundle.simple_scalar_filtered_aggregate, + prepared_insert: bundle.prepared_insert, + prepared_update: bundle.prepared_update, + prepared_delete: bundle.prepared_delete, + read_only, + }) } - fn sync_temp_state_from_runtime(&self, runtime: &EngineRuntime) -> Result<()> { - if runtime.temp_schema_cookie == 0 - && runtime.temp_tables.is_empty() - && runtime.temp_table_data.is_empty() - && runtime.temp_views.is_empty() - && runtime.temp_indexes.is_empty() - { - return Ok(()); - } - let changed = { - let mut state = self - .inner - .temp_state - .lock() - .map_err(|_| DbError::internal("temp schema lock poisoned"))?; - let before = state.schema_cookie; - state.update_from_runtime(runtime); - before != state.schema_cookie - }; - if changed { - crate::plan_cache::PlanCacheInvalidator::on_temp_schema_change(&*self.inner); - } - Ok(()) + fn persist_runtime(&self, runtime: EngineRuntime) -> Result { + self.persist_runtime_if_latest(runtime, None, true) } - fn install_temp_runtime(&self, runtime: EngineRuntime) -> Result<()> { - self.sync_temp_state_from_runtime(&runtime)?; - let mut guard = self + fn build_exclusive_sql_txn_state(&self) -> Result> { + let (snapshot_reader, current_lsn, current_epoch) = self.begin_sql_snapshot()?; + let mut runtime = self .inner .engine .write() .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - *guard = runtime; - Ok(()) - } - - fn statement_is_temp_only( - &self, - runtime: &EngineRuntime, - statement: &crate::sql::ast::Statement, - ) -> bool { - let temp_has_table = |name: &str| { - runtime - .temp_tables - .keys() - .any(|entry| identifiers_equal(entry, name)) - }; - let temp_has_view = |name: &str| { - runtime - .temp_views - .keys() - .any(|entry| identifiers_equal(entry, name)) - }; - match statement { - crate::sql::ast::Statement::CreateTable(statement) => statement.temporary, - crate::sql::ast::Statement::CreateTableAs(statement) => { - statement.temporary && self.query_is_temp_only(runtime, &statement.query) - } - crate::sql::ast::Statement::CreateView(statement) => { - statement.temporary && self.query_is_temp_only(runtime, &statement.query) - } - crate::sql::ast::Statement::DropTable { name, .. } => temp_has_table(name), - crate::sql::ast::Statement::DropView { name, .. } => temp_has_view(name), - crate::sql::ast::Statement::Query(_) - | crate::sql::ast::Statement::Insert(_) - | crate::sql::ast::Statement::Update(_) - | crate::sql::ast::Statement::Delete(_) => { - self.safe_referenced_names_are_temp_only(runtime, statement) - } - _ => false, - } - } - - fn query_is_temp_only(&self, runtime: &EngineRuntime, query: &crate::sql::ast::Query) -> bool { - self.safe_referenced_names_are_temp_only( - runtime, - &crate::sql::ast::Statement::Query(query.clone()), - ) - } - - fn safe_referenced_names_are_temp_only( - &self, - runtime: &EngineRuntime, - statement: &crate::sql::ast::Statement, - ) -> bool { - let Some(names) = crate::sql::ast::safe_referenced_tables(statement) else { - return false; - }; - let mut visiting_views = BTreeSet::new(); - names - .into_iter() - .all(|name| self.referenced_name_is_temp_only(runtime, &name, &mut visiting_views)) - } - - fn referenced_name_is_temp_only( - &self, - runtime: &EngineRuntime, - name: &str, - visiting_views: &mut BTreeSet, - ) -> bool { - if runtime - .temp_tables - .keys() - .any(|entry| identifiers_equal(entry, name)) - { - return true; - } - let Some((view_name, view)) = runtime - .temp_views - .iter() - .find(|(entry, _)| identifiers_equal(entry, name)) - else { - return false; - }; - if !visiting_views.insert(view_name.clone()) { - return false; - } - let temp_only = view.dependencies.iter().all(|dependency| { - self.referenced_name_is_temp_only(runtime, dependency, visiting_views) - }); - visiting_views.remove(view_name); - temp_only - } - - fn parsed_statement(&self, sql: &str) -> Result> { - // Try the connection-local plan cache first. The cache is keyed - // by the prepared SQL text plus the current schema cookies and - // policy/mask generation; on a hit we still get a fresh - // `PreparedStatement` (which is cheap to construct) but we - // skip the parse step. - let prepared_sql = prepared_statement_sql(sql)?; - let parameter_shape = parameter_shape_for_prepared_sql(&prepared_sql); - if parameter_shape.arity() == 0 { - return self - .inner - .statement_cache - .lock() - .map_err(|_| DbError::internal("statement cache lock poisoned"))? - .get_or_parse(&prepared_sql); - } - - let temp_cookie = self - .inner - .temp_state - .lock() - .map(|s| s.schema_cookie) - .unwrap_or(0); - let persistent_cookie = self.inner.catalog.schema_cookie()?; - let policy_gen = self.inner.policy_mask_generation.current(); - let mut plan_cache = self - .inner - .plan_cache - .lock() - .map_err(|_| DbError::internal("plan cache lock poisoned"))?; - let key = crate::plan_cache::PlanCacheKey::new( - prepared_sql, - parameter_shape, - persistent_cookie, - temp_cookie, - policy_gen, - ); - let current_key = key.clone(); - if let Some(statement) = plan_cache.get(&key, persistent_cookie, temp_cookie, policy_gen) { - return Ok(statement); - } - drop(plan_cache); - // Fall back to the existing narrow statement cache for parse - // work, then store the parsed statement in the plan cache. - let statement = self - .inner - .statement_cache - .lock() - .map_err(|_| DbError::internal("statement cache lock poisoned"))? - .get_or_parse(¤t_key.sql_text)?; - if Self::statement_can_enter_plan_cache(statement.as_ref()) { - if let Ok(mut plan_cache) = self.inner.plan_cache.lock() { - if plan_cache.should_admit_missed_key(¤t_key) { - let size = crate::plan_cache::statement_accounted_size(&statement); - plan_cache.insert(current_key, Arc::clone(&statement), size); - } - } - } - Ok(statement) - } - - fn try_prepare_from_plan_cache(&self, prepared_sql: &str) -> Result> { - let persistent_cookie = self.inner.catalog.schema_cookie()?; - let temp_cookie = self - .inner - .temp_state - .lock() - .map_err(|_| DbError::internal("temp schema lock poisoned"))? - .schema_cookie; - let policy_gen = self.inner.policy_mask_generation.current(); - let key = crate::plan_cache::PlanCacheKey::new( - prepared_sql.to_string(), - parameter_shape_for_prepared_sql(prepared_sql), - persistent_cookie, - temp_cookie, - policy_gen, - ); - let Some(bundle) = self - .inner - .prepared_plan_cache - .lock() - .map_err(|_| DbError::internal("prepared plan cache lock poisoned"))? - .get(&key, persistent_cookie, temp_cookie, policy_gen) - else { - return Ok(None); - }; - Ok(Some(PreparedStatement { - db: self.clone(), - schema_cookie: persistent_cookie, - temp_schema_cookie: temp_cookie, - statement: Arc::clone(&bundle.statement), - prepared_sql: prepared_sql.to_string(), - simple_row_id_projection: bundle.simple_row_id_projection, - simple_indexed_projection: bundle.simple_indexed_projection, - simple_row_id_range_projection: bundle.simple_row_id_range_projection, - simple_ordered_row_id_projection: bundle.simple_ordered_row_id_projection, - simple_row_id_join_projection: bundle.simple_row_id_join_projection, - simple_scalar_filtered_aggregate: bundle.simple_scalar_filtered_aggregate, - prepared_insert: bundle.prepared_insert, - prepared_update: bundle.prepared_update, - prepared_delete: bundle.prepared_delete, - read_only: bundle.read_only, - })) - } - - fn prepare_with_runtime( - &self, - sql: &str, - runtime: &EngineRuntime, - ) -> Result { - let prepared_sql = prepared_statement_sql(sql)?; - let policy_gen = self.inner.policy_mask_generation.current(); - let key = crate::plan_cache::PlanCacheKey::new( - prepared_sql.clone(), - parameter_shape_for_prepared_sql(&prepared_sql), - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - policy_gen, - ); - if let Some(bundle) = self - .inner - .prepared_plan_cache - .lock() - .map_err(|_| DbError::internal("prepared plan cache lock poisoned"))? - .get( - &key, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - policy_gen, - ) - { - return Ok(PreparedStatement { - db: self.clone(), - schema_cookie: runtime.catalog.schema_cookie, - temp_schema_cookie: runtime.temp_schema_cookie, - statement: Arc::clone(&bundle.statement), - prepared_sql, - simple_row_id_projection: bundle.simple_row_id_projection, - simple_indexed_projection: bundle.simple_indexed_projection, - simple_row_id_range_projection: bundle.simple_row_id_range_projection, - simple_ordered_row_id_projection: bundle.simple_ordered_row_id_projection, - simple_row_id_join_projection: bundle.simple_row_id_join_projection, - simple_scalar_filtered_aggregate: bundle.simple_scalar_filtered_aggregate, - prepared_insert: bundle.prepared_insert, - prepared_update: bundle.prepared_update, - prepared_delete: bundle.prepared_delete, - read_only: bundle.read_only, - }); - } - if let Some(request) = parse_simple_row_id_range_delete_sql(&prepared_sql) { - if let Some(prepared_delete) = runtime.prepare_simple_row_id_range_delete( - &request.table_name, - &request.column_name, - request.low, - request.high, - )? { - let statement = Arc::new(simple_row_id_range_delete_statement(&request)); - let bundle = PreparedPlanBundle { - statement: Arc::clone(&statement), - simple_row_id_projection: None, - simple_indexed_projection: None, - simple_row_id_range_projection: None, - simple_ordered_row_id_projection: None, - simple_row_id_join_projection: None, - simple_scalar_filtered_aggregate: None, - prepared_insert: None, - prepared_update: None, - prepared_delete: Some(Arc::new(prepared_delete)), - read_only: false, - }; - if let Ok(mut cache) = self.inner.prepared_plan_cache.lock() { - cache.insert( - key, - bundle.clone(), - Self::prepared_plan_accounted_size(&bundle), - ); - } - return Ok(PreparedStatement { - db: self.clone(), - schema_cookie: runtime.catalog.schema_cookie, - temp_schema_cookie: runtime.temp_schema_cookie, - statement, - prepared_sql: prepared_sql.clone(), - simple_row_id_projection: None, - simple_indexed_projection: None, - simple_row_id_range_projection: None, - simple_ordered_row_id_projection: None, - simple_row_id_join_projection: None, - simple_scalar_filtered_aggregate: None, - prepared_insert: None, - prepared_update: None, - prepared_delete: bundle.prepared_delete, - read_only: false, - }); - } - } - let statement = self.parsed_statement(&prepared_sql)?; - let read_only = statement_is_read_only(statement.as_ref()); - let (prepared_insert, prepared_update, prepared_delete) = match statement.as_ref() { - SqlStatement::Insert(insert) => ( - self.prepared_simple_insert(&prepared_sql, insert, runtime)?, - None, - None, - ), - SqlStatement::Update(update) => ( - None, - runtime.prepare_simple_update(update)?.map(Arc::new), - None, - ), - SqlStatement::Delete(delete) => ( - None, - None, - runtime.prepare_simple_delete(delete)?.map(Arc::new), - ), - _ => (None, None, None), - }; - let simple_row_id_projection = - Self::prepared_simple_row_id_projection(&prepared_sql, runtime); - let simple_indexed_projection = - Self::prepared_simple_indexed_projection(statement.as_ref(), runtime); - let simple_row_id_range_projection = - Self::prepared_simple_row_id_range_projection(&prepared_sql, runtime); - let simple_ordered_row_id_projection = - Self::prepared_simple_ordered_row_id_projection(statement.as_ref(), runtime); - let simple_row_id_join_projection = - Self::prepared_simple_row_id_join_projection(statement.as_ref(), runtime); - let simple_scalar_filtered_aggregate = - Self::prepared_simple_scalar_filtered_aggregate(statement.as_ref(), runtime); - let bundle = PreparedPlanBundle { - statement: Arc::clone(&statement), - simple_row_id_projection, - simple_indexed_projection, - simple_row_id_range_projection, - simple_ordered_row_id_projection, - simple_row_id_join_projection, - simple_scalar_filtered_aggregate, - prepared_insert, - prepared_update, - prepared_delete, - read_only, - }; - if Self::statement_can_enter_plan_cache(bundle.statement.as_ref()) { - if let Ok(mut cache) = self.inner.prepared_plan_cache.lock() { - cache.insert( - key, - bundle.clone(), - Self::prepared_plan_accounted_size(&bundle), - ); - } - } - Ok(PreparedStatement { - db: self.clone(), - schema_cookie: runtime.catalog.schema_cookie, - temp_schema_cookie: runtime.temp_schema_cookie, - statement: Arc::clone(&statement), - prepared_sql: prepared_sql.clone(), - simple_row_id_projection: bundle.simple_row_id_projection, - simple_indexed_projection: bundle.simple_indexed_projection, - simple_row_id_range_projection: bundle.simple_row_id_range_projection, - simple_ordered_row_id_projection: bundle.simple_ordered_row_id_projection, - simple_row_id_join_projection: bundle.simple_row_id_join_projection, - simple_scalar_filtered_aggregate: bundle.simple_scalar_filtered_aggregate, - prepared_insert: bundle.prepared_insert, - prepared_update: bundle.prepared_update, - prepared_delete: bundle.prepared_delete, - read_only, - }) - } - - fn prepared_simple_row_id_projection( - sql: &str, - runtime: &EngineRuntime, - ) -> Option { - let plan = parse_simple_row_id_projection_sql(sql)?; - if runtime.temp_table_schema(plan.table_name).is_some() - || runtime - .catalog - .views - .keys() - .any(|view_name| identifiers_equal(view_name, plan.table_name)) - { - return None; - } - let table = runtime.catalog.table(plan.table_name)?; - if !row_id_alias_column_name(table) - .is_some_and(|column_name| identifiers_equal(column_name, plan.filter_column)) - { - return None; - } - - let mut projection_indexes = Vec::with_capacity(plan.projection_columns.len()); - let mut column_names = Vec::with_capacity(plan.projection_columns.len()); - for projection_column in plan.projection_columns { - let index = table - .columns - .iter() - .position(|column| identifiers_equal(&column.name, projection_column))?; - projection_indexes.push(index); - column_names.push(projection_column.to_string()); - } - - Some(PreparedSimpleRowIdProjection { - table_name: table.name.clone(), - projection_indexes, - column_names: Arc::from(column_names), - param_index: plan.param_index, - }) - } - - fn prepared_simple_indexed_projection( - statement: &SqlStatement, - runtime: &EngineRuntime, - ) -> Option { - let SqlStatement::Query(query) = statement else { - return None; - }; - if !query.ctes.is_empty() - || !query.order_by.is_empty() - || query.limit.is_some() - || query.offset.is_some() - { - return None; - } - let QueryBody::Select(select) = &query.body else { - return None; - }; - if select.distinct - || !select.distinct_on.is_empty() - || !select.group_by.is_empty() - || select.having.is_some() - || select.from.len() != 1 - { - return None; - } - let filter = select.filter.as_ref()?; - let FromItem::Table { name, alias } = &select.from[0] else { - return None; - }; - if runtime.temp_table_schema(name).is_some() - || runtime - .catalog - .views - .keys() - .any(|view_name| identifiers_equal(view_name, name)) - { - return None; - } - let table = runtime.catalog.table(name)?; - if !prepared_table_generated_columns_are_stored(table) { - return None; - } - let binding_name = alias.as_deref().unwrap_or(name); - - let (filter_table, filter_column, value_expr) = match filter { - Expr::Binary { left, op, right } if *op == BinaryOp::Eq => match (&**left, &**right) { - (Expr::Column { table, column }, value_expr) => { - (table.as_deref(), column.as_str(), value_expr) - } - (value_expr, Expr::Column { table, column }) => { - (table.as_deref(), column.as_str(), value_expr) - } - _ => return None, - }, - _ => return None, - }; - if let Some(filter_table) = filter_table { - if !identifiers_equal(filter_table, name) - && !identifiers_equal(filter_table, binding_name) - { - return None; - } - } - let value_source = prepared_simple_value_source(value_expr)?; - - let mut projection_indexes = Vec::with_capacity(select.projection.len()); - let mut column_names = Vec::with_capacity(select.projection.len()); - for item in &select.projection { - match item { - SelectItem::Expr { - expr, - alias: select_alias, - } => { - let Expr::Column { - table: projection_table, - column, - } = expr - else { - return None; - }; - if let Some(projection_table) = projection_table.as_deref() { - if !identifiers_equal(projection_table, name) - && !identifiers_equal(projection_table, binding_name) - { - return None; - } - } - let index = table - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, column))?; - projection_indexes.push(index); - column_names.push(select_alias.clone().unwrap_or_else(|| column.clone())); - } - SelectItem::Wildcard => { - for (index, column) in table.columns.iter().enumerate() { - projection_indexes.push(index); - column_names.push(column.name.clone()); - } - } - SelectItem::QualifiedWildcard(qualified_name) => { - if !identifiers_equal(qualified_name, name) - && !identifiers_equal(qualified_name, binding_name) - { - return None; - } - for (index, column) in table.columns.iter().enumerate() { - projection_indexes.push(index); - column_names.push(column.name.clone()); - } - } - } - } - - let lookup = - if row_id_alias_column_name(table) - .is_some_and(|column_name| identifiers_equal(column_name, filter_column)) - { - PreparedSimpleIndexedProjectionLookup::RowId { value_source } - } else { - let index_name = - runtime - .catalog - .indexes - .values() - .find(|index| { - index.fresh - && index.kind == crate::catalog::IndexKind::Btree - && identifiers_equal(&index.table_name, &table.name) - && index.predicate_sql.is_none() - && index.columns.len() == 1 - && index.columns[0].expression_sql.is_none() - && index.columns[0].column_name.as_ref().is_some_and( - |column_name| identifiers_equal(column_name, filter_column), - ) - }) - .map(|index| index.name.clone())?; - PreparedSimpleIndexedProjectionLookup::Index { - index_name, - value_source, - } - }; - - Some(PreparedSimpleIndexedProjection { - table_name: table.name.clone(), - projection_indexes, - column_names: Arc::from(column_names), - lookup, - }) - } - - fn prepared_simple_row_id_range_projection( - sql: &str, - runtime: &EngineRuntime, - ) -> Option { - let plan = parse_simple_row_id_range_projection_sql(sql)?; - if runtime.temp_table_schema(plan.table_name).is_some() - || runtime - .catalog - .views - .keys() - .any(|view_name| identifiers_equal(view_name, plan.table_name)) - { - return None; - } - let table = runtime.catalog.table(plan.table_name)?; - let filter_column_index = table - .columns - .iter() - .position(|column| identifiers_equal(&column.name, plan.filter_column))?; - if !table - .primary_key_columns - .iter() - .any(|column| identifiers_equal(column, plan.filter_column)) - || table.columns[filter_column_index].column_type != ColumnType::Int64 - { - return None; - } - - let mut projection_indexes = Vec::with_capacity(plan.projection_columns.len()); - let mut column_names = Vec::with_capacity(plan.projection_columns.len()); - for projection_column in plan.projection_columns { - let index = table - .columns - .iter() - .position(|column| identifiers_equal(&column.name, projection_column))?; - projection_indexes.push(index); - column_names.push(projection_column.to_string()); - } - - Some(PreparedSimpleRowIdRangeProjection { - table_name: table.name.clone(), - projection_indexes, - column_names: Arc::from(column_names), - filter_column: table.columns[filter_column_index].name.clone(), - lower_bound: plan.lower_bound, - upper_bound: plan.upper_bound, - limit_param_index: plan.limit_param_index, - }) - } - - fn prepared_simple_ordered_row_id_projection( - statement: &SqlStatement, - runtime: &EngineRuntime, - ) -> Option { - let SqlStatement::Query(query) = statement else { - return None; - }; - if !query.ctes.is_empty() || query.order_by.len() != 1 { - return None; - } - let crate::sql::ast::QueryBody::Select(select) = &query.body else { - return None; - }; - if select.filter.is_some() - || !select.group_by.is_empty() - || select.having.is_some() - || select.distinct - || !select.distinct_on.is_empty() - || select.from.len() != 1 - { - return None; - } - let crate::sql::ast::FromItem::Table { name, alias } = &select.from[0] else { - return None; - }; - if runtime.temp_table_schema(name).is_some() - || runtime - .catalog - .views - .keys() - .any(|view_name| identifiers_equal(view_name, name)) - { - return None; - } - let table = runtime.catalog.table(name)?; - if !prepared_table_generated_columns_are_stored(table) { - return None; - } - let mut projection_indexes = Vec::with_capacity(select.projection.len()); - let mut column_names = Vec::with_capacity(select.projection.len()); - for item in &select.projection { - let crate::sql::ast::SelectItem::Expr { - expr, - alias: select_alias, - } = item - else { - return None; - }; - let crate::sql::ast::Expr::Column { - table: projection_table, - column, - } = expr - else { - return None; - }; - if !prepared_scalar_column_matches_table(projection_table.as_deref(), name, alias) { - return None; - } - let index = table - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, column))?; - projection_indexes.push(index); - column_names.push(select_alias.clone().unwrap_or_else(|| column.clone())); - } - - let order = &query.order_by[0]; - if order.collation.is_some() { - return None; - } - let crate::sql::ast::Expr::Column { - table: order_table, - column: order_column, - } = &order.expr - else { - return None; - }; - if !prepared_scalar_column_matches_table(order_table.as_deref(), name, alias) { - return None; - } - let order_column_index = table - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, order_column))?; - if !row_id_alias_column_name(table) - .is_some_and(|column_name| identifiers_equal(column_name, order_column)) - || table.columns[order_column_index].column_type != ColumnType::Int64 - { - return None; - } - let limit = match query.limit.as_ref() { - Some(expr) => Some(prepared_usize_literal(expr)?), - None => None, - }; - let offset = match query.offset.as_ref() { - Some(expr) => prepared_usize_literal(expr)?, - None => 0, - }; - Some(PreparedSimpleOrderedRowIdProjection { - table_name: table.name.clone(), - order_column: table.columns[order_column_index].name.clone(), - projection_indexes, - column_names: Arc::from(column_names), - limit, - offset, - descending: order.descending, - }) - } - - fn prepared_simple_row_id_join_projection( - statement: &SqlStatement, - runtime: &EngineRuntime, - ) -> Option { - let SqlStatement::Query(query) = statement else { - return None; - }; - if !query.ctes.is_empty() - || !query.order_by.is_empty() - || query.limit.is_some() - || query.offset.is_some() - { - return None; - } - let crate::sql::ast::QueryBody::Select(select) = &query.body else { - return None; - }; - if !select.group_by.is_empty() - || select.having.is_some() - || select.distinct - || !select.distinct_on.is_empty() - || select.from.len() != 1 - { - return None; - } - let filter = select.filter.as_ref()?; - let crate::sql::ast::FromItem::Join { - left, - right, - kind: crate::sql::ast::JoinKind::Inner, - constraint, - } = &select.from[0] - else { - return None; - }; - let crate::sql::ast::FromItem::Table { - name: left_name, - alias: left_alias, - } = &**left - else { - return None; - }; - let crate::sql::ast::FromItem::Table { - name: right_name, - alias: right_alias, - } = &**right - else { - return None; - }; - if runtime.temp_table_schema(left_name).is_some() - || runtime.temp_table_schema(right_name).is_some() - || runtime.catalog.views.keys().any(|view_name| { - identifiers_equal(view_name, left_name) || identifiers_equal(view_name, right_name) - }) - { - return None; - } - let left_schema = runtime.catalog.table(left_name)?; - let right_schema = runtime.catalog.table(right_name)?; - let left_rowid_column = row_id_alias_column_name(left_schema)?; - let right_rowid_column = row_id_alias_column_name(right_schema)?; - - let (join_a, join_b) = prepared_join_column_equality(constraint)?; - let join_a_side = - prepared_join_column_side(join_a.0, left_name, left_alias, right_name, right_alias)?; - let join_b_side = - prepared_join_column_side(join_b.0, left_name, left_alias, right_name, right_alias)?; - let (left_join_column, right_join_column) = match (join_a_side, join_b_side) { - (SimpleJoinProjectionSide::Left, SimpleJoinProjectionSide::Right) => { - (join_a.1, join_b.1) - } - (SimpleJoinProjectionSide::Right, SimpleJoinProjectionSide::Left) => { - (join_b.1, join_a.1) - } - _ => return None, - }; - if !identifiers_equal(left_join_column, left_rowid_column) - || !identifiers_equal(right_join_column, right_rowid_column) - { - return None; - } - - let (filter_table, filter_column, param_index) = prepared_join_filter_param(filter)?; - let filter_side = prepared_join_column_side( - filter_table, - left_name, - left_alias, - right_name, - right_alias, - )?; - match filter_side { - SimpleJoinProjectionSide::Left - if !identifiers_equal(filter_column, left_rowid_column) => - { - return None; - } - SimpleJoinProjectionSide::Right - if !identifiers_equal(filter_column, right_rowid_column) => - { - return None; - } - _ => {} - } - let mut projections = Vec::with_capacity(select.projection.len()); - let mut left_projection_indexes = Vec::new(); - let mut right_projection_indexes = Vec::new(); - let mut column_names = Vec::with_capacity(select.projection.len()); - for item in &select.projection { - let crate::sql::ast::SelectItem::Expr { expr, alias } = item else { - return None; - }; - let crate::sql::ast::Expr::Column { table, column } = expr else { - return None; - }; - let side = prepared_join_column_side( - table.as_deref(), - left_name, - left_alias, - right_name, - right_alias, - )?; - let schema = match side { - SimpleJoinProjectionSide::Left => left_schema, - SimpleJoinProjectionSide::Right => right_schema, - }; - let index = schema - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, column))?; - let projected_index = match side { - SimpleJoinProjectionSide::Left => { - push_prepared_join_projection_index(&mut left_projection_indexes, index) - } - SimpleJoinProjectionSide::Right => { - push_prepared_join_projection_index(&mut right_projection_indexes, index) - } - }; - projections.push(ResolvedSimpleJoinProjection { - side, - index: projected_index, - }); - column_names.push(alias.clone().unwrap_or_else(|| column.clone())); - } - - Some(PreparedSimpleRowIdJoinProjection { - left_table_name: left_schema.name.clone(), - right_table_name: right_schema.name.clone(), - left_projection_indexes, - right_projection_indexes, - projections, - column_names: Arc::from(column_names), - param_index, - }) - } - - fn prepared_simple_scalar_filtered_aggregate( - statement: &SqlStatement, - runtime: &EngineRuntime, - ) -> Option { - let SqlStatement::Query(query) = statement else { - return None; - }; - if !query.ctes.is_empty() - || !query.order_by.is_empty() - || query.limit.is_some() - || query.offset.is_some() - { - return None; - } - let crate::sql::ast::QueryBody::Select(select) = &query.body else { - return None; - }; - if select.distinct - || !select.distinct_on.is_empty() - || !select.group_by.is_empty() - || select.having.is_some() - || select.from.len() != 1 - || select.projection.len() != 2 - { - return None; - } - let crate::sql::ast::FromItem::Table { name, alias } = &select.from[0] else { - return None; - }; - if runtime.temp_table_schema(name).is_some() - || runtime - .catalog - .views - .keys() - .any(|view_name| identifiers_equal(view_name, name)) - { - return None; - } - let table = runtime.catalog.table(name)?; - if !prepared_table_generated_columns_are_stored(table) { - return None; - } - let param_index = prepared_scalar_filter_param(select.filter.as_ref()?, name, alias)?; - let mut saw_count = false; - let mut saw_sum = false; - for item in &select.projection { - let crate::sql::ast::SelectItem::Expr { expr, .. } = item else { - return None; - }; - if prepared_scalar_count_star(expr) { - saw_count = true; - continue; - } - if let Some(sum_column) = prepared_scalar_sum_column(expr, name, alias) { - if table - .columns - .iter() - .any(|column| identifiers_equal(&column.name, sum_column)) - { - saw_sum = true; - continue; - } - } - return None; - } - if !saw_count || !saw_sum { - return None; - } - Some(PreparedSimpleScalarFilteredAggregate { - table_name: table.name.clone(), - param_index, - cache: Arc::new(Mutex::new(PreparedScalarAggregateCache::default())), - }) - } - - fn prepared_simple_insert( - &self, - sql: &str, - statement: &crate::sql::ast::InsertStatement, - runtime: &EngineRuntime, - ) -> Result>> { - self.inner - .prepared_insert_cache - .lock() - .map_err(|_| DbError::internal("prepared insert cache lock poisoned"))? - .get_or_prepare( - sql, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - || runtime.prepare_simple_insert(statement), - ) - } - - fn prepared_plan_accounted_size(bundle: &PreparedPlanBundle) -> u64 { - fn string_bytes(value: &str) -> u64 { - value.len() as u64 - } - fn string_slice_bytes(values: &[String]) -> u64 { - values.iter().map(|value| string_bytes(value)).sum() - } - - let mut total = crate::plan_cache::statement_accounted_size(bundle.statement.as_ref()) - .saturating_add(std::mem::size_of::() as u64); - if let Some(plan) = &bundle.simple_row_id_projection { - total = total - .saturating_add(128) - .saturating_add(string_bytes(&plan.table_name)) - .saturating_add( - (plan.projection_indexes.len() * std::mem::size_of::()) as u64, - ) - .saturating_add(string_slice_bytes(&plan.column_names)); - } - if let Some(plan) = &bundle.simple_indexed_projection { - total = total - .saturating_add(192) - .saturating_add(string_bytes(&plan.table_name)) - .saturating_add( - (plan.projection_indexes.len() * std::mem::size_of::()) as u64, - ) - .saturating_add(string_slice_bytes(&plan.column_names)); - } - if let Some(plan) = &bundle.simple_row_id_range_projection { - total = total - .saturating_add(160) - .saturating_add(string_bytes(&plan.table_name)) - .saturating_add(string_bytes(&plan.filter_column)) - .saturating_add( - (plan.projection_indexes.len() * std::mem::size_of::()) as u64, - ) - .saturating_add(string_slice_bytes(&plan.column_names)); - } - if let Some(plan) = &bundle.simple_ordered_row_id_projection { - total = total - .saturating_add(160) - .saturating_add(string_bytes(&plan.table_name)) - .saturating_add(string_bytes(&plan.order_column)) - .saturating_add( - (plan.projection_indexes.len() * std::mem::size_of::()) as u64, - ) - .saturating_add(string_slice_bytes(&plan.column_names)); - } - if let Some(plan) = &bundle.simple_row_id_join_projection { - total = total - .saturating_add(256) - .saturating_add(string_bytes(&plan.left_table_name)) - .saturating_add(string_bytes(&plan.right_table_name)) - .saturating_add( - ((plan.left_projection_indexes.len() - + plan.right_projection_indexes.len() - + plan.projections.len()) - * std::mem::size_of::()) as u64, - ) - .saturating_add(string_slice_bytes(&plan.column_names)); - } - if let Some(plan) = &bundle.simple_scalar_filtered_aggregate { - total = total - .saturating_add(128) - .saturating_add(string_bytes(&plan.table_name)); - } - if bundle.prepared_insert.is_some() { - total = total.saturating_add(512); - } - if bundle.prepared_update.is_some() { - total = total.saturating_add(384); - } - if bundle.prepared_delete.is_some() { - total = total.saturating_add(384); - } - total - } - - fn persist_runtime(&self, runtime: EngineRuntime) -> Result { - self.persist_runtime_if_latest(runtime, None, true) - } - - fn build_exclusive_sql_txn_state(&self) -> Result> { - let (snapshot_reader, current_lsn, current_epoch) = self.begin_sql_snapshot()?; - let mut runtime = self - .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - self.configure_runtime_sync_capture(&mut runtime)?; - Ok(ExclusiveSqlTxnState { - runtime, - snapshot_reader: Some(snapshot_reader), - base_lsn: current_lsn, - base_checkpoint_epoch: current_epoch, - persistent_changed: false, - indexes_maybe_stale: false, - prepared_insert_runtime_cache: HashMap::new(), - prepared_insert_last_cache_key: None, - prepared_insert_last_plan: None, - prepared_insert_last_next_row_id: None, - prepared_insert_candidate: Vec::new(), - }) - } - - fn commit_exclusive_sql_txn(&self, mut state: ExclusiveSqlTxnState<'_>) -> Result { - Self::flush_exclusive_prepared_insert_next_row_id(&mut state)?; - if !state.persistent_changed { - self.sync_temp_state_from_runtime(&state.runtime)?; - return Ok(state.base_lsn); - } - - let runtime_schema_cookie = state.runtime.catalog.schema_cookie; - if state.indexes_maybe_stale { - state - .runtime - .rebuild_stale_indexes(self.inner.config.page_size)?; - } - let reactive_pending = self.take_reactive_pending_commit(&mut state.runtime); - self.begin_write()?; - if let Err(error) = state.runtime.persist_to_db(self) { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut state.runtime)?; - return Err(error); - } - drop(state.snapshot_reader.take()); - let committed_lsn = match self.commit_if_latest(state.base_lsn, state.base_checkpoint_epoch) - { - Ok(lsn) => lsn, - Err(error) => { - let _ = self.rollback(); - self.restore_runtime_from_storage(&mut state.runtime)?; - return Err(error); - } - }; - self.sync_post_commit(&mut state.runtime, committed_lsn)?; - if self.inner.catalog.schema_cookie()? != runtime_schema_cookie { - self.inner - .catalog - .replace(state.runtime.catalog.as_ref().clone())?; - } - self.sync_temp_state_from_runtime(&state.runtime)?; - if self.should_redefer_paged_row_sources_after_write() { - let freed_bytes = state.runtime.redefer_all_persisted_paged_tables(); - self.release_freed_heap_after_paged_row_source_drop(freed_bytes); - } - self.inner - .last_runtime_lsn - .store(committed_lsn, Ordering::Release); - self.inner - .writer_last_commit_lsn - .store(committed_lsn, Ordering::Release); - drop(state); - self.maybe_demote_wal_after_large_explicit_commit(); - self.publish_reactive_commit(reactive_pending, committed_lsn); - Ok(committed_lsn) - } - - fn rollback_exclusive_sql_txn(&self, mut state: ExclusiveSqlTxnState<'_>) -> Result<()> { - self.restore_runtime_from_storage(&mut state.runtime) - } - - fn persist_runtime_if_latest( - &self, - runtime: EngineRuntime, - expected_latest: Option<(u64, u64)>, - rebuild_stale_indexes: bool, - ) -> Result { - let mut runtime = runtime; - let runtime_schema_cookie = runtime.catalog.schema_cookie; - if rebuild_stale_indexes { - runtime.rebuild_stale_indexes(self.inner.config.page_size)?; - } - let compacted_bytes = runtime.compact_dirty_resident_storage_after_transaction_commit(); - let reactive_pending = self.take_reactive_pending_commit(&mut runtime); - self.begin_write()?; - if let Err(error) = runtime.persist_to_db(self) { - let _ = self.rollback(); - return Err(error); - } - let committed_lsn = match expected_latest { - Some((lsn, epoch)) => self.commit_if_latest(lsn, epoch), - None => self.commit(), - }; - let committed_lsn = match committed_lsn { - Ok(lsn) => lsn, - Err(error) => { - let _ = self.rollback(); - return Err(error); - } - }; - self.sync_post_commit(&mut runtime, committed_lsn)?; - if self.inner.catalog.schema_cookie()? != runtime_schema_cookie { - self.inner - .catalog - .replace(runtime.catalog.as_ref().clone())?; - } - self.sync_temp_state_from_runtime(&runtime)?; - let mut guard = self - .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - *guard = runtime; - if self.should_redefer_paged_row_sources_after_write() { - let freed_bytes = guard.redefer_all_persisted_paged_tables(); - self.release_freed_heap_after_paged_row_source_drop(freed_bytes); - } - self.release_freed_heap_after_runtime_compaction(compacted_bytes); - self.inner - .last_runtime_lsn - .store(committed_lsn, Ordering::Release); - self.inner - .writer_last_commit_lsn - .store(committed_lsn, Ordering::Release); - drop(guard); - self.maybe_demote_wal_after_large_explicit_commit(); - self.publish_reactive_commit(reactive_pending, committed_lsn); - - Ok(committed_lsn) - } - - fn runtime_for_targeted_row_source_inspection(&self) -> Result<(EngineRuntime, Option)> { - if let Some((runtime, snapshot_lsn)) = self.transaction_runtime_snapshot_with_lsn()? { - return Ok((runtime, Some(snapshot_lsn))); - } - if !self.inner.config.defer_table_materialization { - self.refresh_engine_from_storage()?; - return Ok((self.engine_snapshot()?, None)); - } - let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - self.refresh_engine_from_snapshot(snapshot_lsn)?; - drop(reader); - Ok((self.engine_snapshot()?, Some(snapshot_lsn))) - } - - fn validate_watch_tables(&self, tables: &[String]) -> Result> { - if tables.is_empty() { - return Err(DbError::sql("watch table list must not be empty")); - } - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let mut canonical = BTreeSet::new(); - for table in tables { - let schema = runtime - .catalog - .table(table) - .ok_or_else(|| DbError::sql(format!("unknown watch table {table}")))?; - if schema.temporary || crate::sync::is_internal_table_name(&schema.name) { - return Err(DbError::sql(format!( - "table {} is not watchable", - schema.name - ))); - } - canonical.insert(schema.name.clone()); - } - Ok(canonical) - } - - fn validate_watch_range_table(&self, table: &str) -> Result { - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let schema = runtime - .catalog - .table(table) - .ok_or_else(|| DbError::sql(format!("unknown watch table {table}")))?; - if schema.temporary || crate::sync::is_internal_table_name(&schema.name) { - return Err(DbError::sql(format!( - "table {} is not watchable", - schema.name - ))); - } - if schema.primary_key_columns.is_empty() { - return Err(DbError::sql(format!( - "range watch requires a primary key on table {}", - schema.name - ))); - } - Ok(schema.name.clone()) - } - - fn query_watch_dependencies( - &self, - statement: &crate::sql::ast::Statement, - ) -> Result> { - let referenced = crate::sql::ast::safe_referenced_tables(statement) - .ok_or_else(|| DbError::sql("query dependencies are not watchable for this SELECT"))?; - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let mut dependencies = BTreeSet::new(); - for name in referenced { - if let Some(table) = runtime.catalog.table(&name) { - if table.temporary || crate::sync::is_internal_table_name(&table.name) { - return Err(DbError::sql(format!( - "table {} is not watchable", - table.name - ))); - } - dependencies.insert(table.name.clone()); - } else if let Some(view) = runtime.catalog.view(&name) { - if view.temporary || crate::sync::is_internal_table_name(&view.name) { - return Err(DbError::sql(format!("view {} is not watchable", view.name))); - } - for dependency in &view.dependencies { - let table = runtime.catalog.table(dependency).ok_or_else(|| { - DbError::sql(format!( - "view {} depends on unknown table {}", - view.name, dependency - )) - })?; - if !table.temporary && !crate::sync::is_internal_table_name(&table.name) { - dependencies.insert(table.name.clone()); - } - } - } else { - return Err(DbError::sql(format!( - "query dependency {name} is not a watchable table or view" - ))); - } - } - if dependencies.is_empty() { - return Err(DbError::sql( - "query subscription has no watchable table dependencies", - )); - } - Ok(dependencies) - } - - fn ensure_inspection_table_row_source( - &self, - runtime: &mut EngineRuntime, - table_name: &str, - snapshot_lsn: Option, - ) -> Result<()> { - if runtime.table_row_source(table_name).is_some() - || runtime.temp_table_schema(table_name).is_some() - { - return Ok(()); - } - let Some(snapshot_lsn) = snapshot_lsn else { - return Ok(()); - }; - self.load_runtime_table_row_sources_at_snapshot(runtime, &[table_name], snapshot_lsn) - } - - fn insert_dependency_table_names( - &self, - runtime: &EngineRuntime, - table_name: &str, - ) -> Result> { - let table = runtime - .table_schema(table_name) - .ok_or_else(|| DbError::sql(format!("unknown table {table_name}")))?; - let mut names = vec![table_name.to_string()]; - for foreign_key in &table.foreign_keys { - if !names - .iter() - .any(|name| identifiers_equal(name, &foreign_key.referenced_table)) - { - names.push(foreign_key.referenced_table.clone()); - } - } - Ok(names) - } - - fn redefer_inspection_table_row_source( - &self, - runtime: &mut EngineRuntime, - table_name: &str, - snapshot_lsn: Option, - ) { - if snapshot_lsn.is_some() && runtime.persisted_table_state(table_name).is_some() { - let _ = runtime.redefer_persisted_tables(&[table_name]); - } - } - - fn runtime_for_metadata_inspection(&self) -> Result { - if let Some(runtime) = self.transaction_runtime_snapshot()? { - return Ok(runtime); - } - self.refresh_engine_from_storage()?; - self.engine_snapshot() - } - - fn runtime_table_row_count( - &self, - runtime: &EngineRuntime, - table_name: &str, - snapshot_lsn: Option, - ) -> Result { - if let Some(table) = runtime.temp_table_schema(table_name) { - return Ok(runtime - .temp_table_data(&table.name) - .map_or(0, |data| data.rows.len())); - } - - if let Some(source) = runtime.table_row_source(table_name) { - return Ok(source.row_count()); - } - - let state = runtime.persisted_table_state(table_name); - if let Some(state) = state { - // The persisted state belongs to the runtime snapshot selected by - // the caller. Writes keep its non-zero live-row count exact even - // when a paged row source is re-deferred, so do not fault the - // manifest back in merely to recount its chunks. - if state.row_count != 0 { - return Ok(state.row_count); - } - // A missing payload is an unambiguously empty table. A zero count - // paired with a non-empty legacy/paged pointer remains ambiguous: - // older catalogs did not persist the count unless ANALYZE stats - // were present, so that case must fall through to storage. - if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { - return Ok(0); - } - } - - if let Some(table) = runtime.catalog.table(table_name) { - if let Some(stats) = runtime.catalog.table_stats.get(&table.name) { - // Presence, rather than a non-zero value, distinguishes an - // analyzed empty table from a legacy catalog whose row count - // is unknown. Mutations invalidate these stats before the - // runtime is persisted. - return Ok(usize::try_from(stats.row_count.max(0)).unwrap_or(usize::MAX)); - } - } - - let Some(state) = state else { - return Ok(0); - }; - - let store = if let Some(lsn) = snapshot_lsn { - PagerReadStore::with_snapshot_lsn(self, lsn) - } else { - PagerReadStore::new(self)? - }; - if state.pointer.is_table_paged_manifest() { - return read_persisted_table_row_count(&store, state); - } - - let payload = read_overflow(&store, state.pointer)?; - read_table_payload_live_row_count_from_bytes(&payload) - } - - fn runtime_table_row_count_without_storage( - &self, - runtime: &EngineRuntime, - table_name: &str, - ) -> Result> { - if runtime.temp_table_schema(table_name).is_some() { - return Ok(None); - } - - if let Some(source) = runtime.table_row_source(table_name) { - return Ok(Some(source.row_count())); - } - - let state = runtime.persisted_table_state(table_name); - if let Some(state) = state { - if state.row_count != 0 { - return Ok(Some(state.row_count)); - } - if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { - return Ok(Some(0)); - } - } - - if let Some(table) = runtime.catalog.table(table_name) { - if let Some(stats) = runtime.catalog.table_stats.get(&table.name) { - return Ok(Some( - usize::try_from(stats.row_count.max(0)).unwrap_or(usize::MAX), - )); - } - } - - if state.is_none() { - return Ok(Some(0)); - } - - Ok(None) - } - - fn runtime_for_prepare(&self) -> Result { - if let Some(runtime) = self.transaction_runtime_snapshot_for_prepare()? { - return Ok(runtime); - } - self.refresh_engine_from_storage()?; - // ADR 0143 Phase B: prepare() only needs catalog/schema metadata - // to plan a statement. Skip the eager all-tables materialization - // so applications that prepare a large number of statements at - // startup don't fault every persisted table into memory just to - // get a `PreparedStatement` handle. Row data is loaded on first - // execution by the read/write paths. - self.engine_snapshot_without_index_rebuild() - } - - fn transaction_runtime_snapshot_for_prepare(&self) -> Result> { - if !self.inner.sql_txn_active.load(Ordering::Acquire) { - return Ok(None); - } - let txn = self - .inner - .sql_txn - .lock() - .map_err(|_| DbError::internal("SQL transaction lock poisoned"))?; - let state = match &*txn { - SqlTxnSlot::Shared(state) => state, - SqlTxnSlot::Exclusive => return Err(self.exclusive_sql_txn_error()), - SqlTxnSlot::None => return Ok(None), - }; - Ok(Some(state.runtime.clone())) - } - - fn transaction_runtime_snapshot(&self) -> Result> { - self.transaction_runtime_snapshot_with_lsn() - .map(|maybe| maybe.map(|(runtime, _)| runtime)) - } - - fn transaction_runtime_snapshot_with_lsn(&self) -> Result> { - if !self.inner.sql_txn_active.load(Ordering::Acquire) { - return Ok(None); - } - let txn = self - .inner - .sql_txn - .lock() - .map_err(|_| DbError::internal("SQL transaction lock poisoned"))?; - let state = match &*txn { - SqlTxnSlot::Shared(state) => state, - SqlTxnSlot::Exclusive => return Err(self.exclusive_sql_txn_error()), - SqlTxnSlot::None => return Ok(None), - }; - - let mut snapshot = state.runtime.clone(); - if state.indexes_maybe_stale { - snapshot.rebuild_stale_indexes(self.inner.config.page_size)?; - } - Ok(Some((snapshot, state.snapshot_lsn()))) - } - - fn restore_runtime_from_storage(&self, runtime: &mut EngineRuntime) -> Result<()> { - let schema_cookie = self.current_schema_cookie()?; - let (mut restored, restored_lsn) = EngineRuntime::load_from_storage( - &self.inner.pager, - &self.inner.wal, - schema_cookie, - &self.inner.config, - )?; - restored.set_audit_context_handle(Arc::clone(&self.inner.audit_context)); - self.apply_temp_state_to_runtime(&mut restored)?; - self.inner - .catalog - .replace(restored.catalog.as_ref().clone())?; - *runtime = restored; - self.inner - .last_runtime_lsn - .store(restored_lsn, Ordering::Release); - Ok(()) - } - - fn refresh_engine_from_snapshot(&self, snapshot_lsn: u64) -> Result<()> { - let latest_checkpoint_epoch = self.inner.wal.checkpoint_epoch(); - let mut last_seen_checkpoint_epoch = self - .inner - .last_seen_checkpoint_epoch - .load(Ordering::Acquire); - let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); - let writer_last_commit_lsn = self.inner.writer_last_commit_lsn.load(Ordering::Acquire); - let last_explicit_checkpoint_epoch = self - .inner - .last_explicit_checkpoint_epoch - .load(Ordering::Acquire); - let mut checkpoint_lsn_after_refresh = None; - if latest_checkpoint_epoch != last_seen_checkpoint_epoch { - let cached_header = self.inner.pager.header_snapshot()?; - let on_disk_header = self.inner.pager.header_from_disk()?; - checkpoint_lsn_after_refresh = Some(on_disk_header.last_checkpoint_lsn); - if on_disk_header.last_checkpoint_lsn != cached_header.last_checkpoint_lsn { - self.inner.pager.refresh_from_disk(on_disk_header)?; - } - self.inner - .last_seen_checkpoint_epoch - .store(latest_checkpoint_epoch, Ordering::Release); - last_seen_checkpoint_epoch = latest_checkpoint_epoch; - } - if snapshot_lsn == last_runtime_lsn && latest_checkpoint_epoch == last_seen_checkpoint_epoch - { - return Ok(()); - } - - if last_runtime_lsn > 0 - && writer_last_commit_lsn > 0 - && last_runtime_lsn >= writer_last_commit_lsn - && snapshot_lsn == 0 - && last_explicit_checkpoint_epoch == latest_checkpoint_epoch - && checkpoint_lsn_after_refresh.is_some_and(|checkpoint_lsn| { - checkpoint_lsn == last_runtime_lsn && checkpoint_lsn >= writer_last_commit_lsn - }) - { - // An explicit checkpoint from this handle can fold exactly the - // current runtime into the database file and reset the live WAL - // end to 0. Only preserve the hot runtime before any post- - // checkpoint WAL frames exist; otherwise the runtime would no - // longer match the pinned snapshot. - self.inner - .last_runtime_lsn - .store(snapshot_lsn, Ordering::Release); - return Ok(()); - } - - let schema_cookie = self.current_schema_cookie_at_snapshot(snapshot_lsn)?; - let mut runtime = EngineRuntime::load_from_storage_at_snapshot( - &self.inner.pager, - &self.inner.wal, - schema_cookie, - &self.inner.config, - snapshot_lsn, - )?; - self.apply_temp_state_to_runtime(&mut runtime)?; - self.inner - .catalog - .replace(runtime.catalog.as_ref().clone())?; - let mut guard = self - .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - *guard = runtime; - self.inner - .last_runtime_lsn - .store(snapshot_lsn, Ordering::Release); - self.inner - .last_seen_checkpoint_epoch - .store(latest_checkpoint_epoch, Ordering::Release); - Ok(()) - } - - fn observed_current_resident_snapshot_lsn(&self) -> Result> { - let Some(snapshot_lsn) = self.inner.wal.observed_current_snapshot_lsn()? else { - return Ok(None); - }; - if self.observed_current_runtime_is_current(snapshot_lsn) { - return Ok(Some(snapshot_lsn)); - } - self.try_preserve_observed_current_runtime_after_explicit_checkpoint(snapshot_lsn)?; - if self.observed_current_runtime_is_current(snapshot_lsn) { - return Ok(Some(snapshot_lsn)); - } - Ok(None) - } - - fn observed_current_resident_snapshot_still_valid(&self, snapshot_lsn: u64) -> Result { - let Some(current_snapshot_lsn) = self.inner.wal.observed_current_snapshot_lsn()? else { - return Ok(false); - }; - Ok(current_snapshot_lsn == snapshot_lsn - && self.observed_current_runtime_is_current(snapshot_lsn)) - } - - fn observed_current_runtime_is_current(&self, snapshot_lsn: u64) -> bool { - let latest_checkpoint_epoch = self.inner.wal.checkpoint_epoch(); - let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); - let last_seen_checkpoint_epoch = self - .inner - .last_seen_checkpoint_epoch - .load(Ordering::Acquire); - snapshot_lsn == last_runtime_lsn && latest_checkpoint_epoch == last_seen_checkpoint_epoch - } - - fn try_preserve_observed_current_runtime_after_explicit_checkpoint( - &self, - snapshot_lsn: u64, - ) -> Result<()> { - let latest_checkpoint_epoch = self.inner.wal.checkpoint_epoch(); - let last_seen_checkpoint_epoch = self - .inner - .last_seen_checkpoint_epoch - .load(Ordering::Acquire); - if latest_checkpoint_epoch == last_seen_checkpoint_epoch { - return Ok(()); - } - - let cached_header = self.inner.pager.header_snapshot()?; - let on_disk_header = self.inner.pager.header_from_disk()?; - if on_disk_header.last_checkpoint_lsn != cached_header.last_checkpoint_lsn { - self.inner.pager.refresh_from_disk(on_disk_header.clone())?; - } - self.inner - .last_seen_checkpoint_epoch - .store(latest_checkpoint_epoch, Ordering::Release); - - let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); - let writer_last_commit_lsn = self.inner.writer_last_commit_lsn.load(Ordering::Acquire); - let last_explicit_checkpoint_epoch = self - .inner - .last_explicit_checkpoint_epoch - .load(Ordering::Acquire); - if last_runtime_lsn > 0 - && writer_last_commit_lsn > 0 - && last_runtime_lsn >= writer_last_commit_lsn - && snapshot_lsn == 0 - && last_explicit_checkpoint_epoch == latest_checkpoint_epoch - && on_disk_header.last_checkpoint_lsn == last_runtime_lsn - && on_disk_header.last_checkpoint_lsn >= writer_last_commit_lsn - { - self.inner - .last_runtime_lsn - .store(snapshot_lsn, Ordering::Release); - } - Ok(()) - } - - fn refresh_engine_from_storage(&self) -> Result<()> { - self.inner - .wal - .refresh_from_coordination(&self.inner.pager)?; - let latest_lsn = self.inner.wal.latest_snapshot(); - let latest_checkpoint_epoch = self.inner.wal.checkpoint_epoch(); - let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); - let last_seen_checkpoint_epoch = self - .inner - .last_seen_checkpoint_epoch - .load(Ordering::Acquire); - let writer_last_commit_lsn = self.inner.writer_last_commit_lsn.load(Ordering::Acquire); - - if latest_lsn == last_runtime_lsn && latest_checkpoint_epoch == last_seen_checkpoint_epoch { - return Ok(()); - } - - let mut checkpoint_lsn_after_refresh = None; - if latest_checkpoint_epoch != last_seen_checkpoint_epoch { - let cached_header = self.inner.pager.header_snapshot()?; - let on_disk_header = self.inner.pager.header_from_disk()?; - checkpoint_lsn_after_refresh = Some(on_disk_header.last_checkpoint_lsn); - if on_disk_header.last_checkpoint_lsn != cached_header.last_checkpoint_lsn { - self.inner.pager.refresh_from_disk(on_disk_header)?; - } - self.inner - .last_seen_checkpoint_epoch - .store(latest_checkpoint_epoch, Ordering::Release); - } - - let last_explicit_checkpoint_epoch = self - .inner - .last_explicit_checkpoint_epoch - .load(Ordering::Acquire); - if last_runtime_lsn > 0 - && writer_last_commit_lsn > 0 - && last_runtime_lsn >= writer_last_commit_lsn - && latest_lsn == 0 - && last_explicit_checkpoint_epoch == latest_checkpoint_epoch - && checkpoint_lsn_after_refresh.is_some_and(|checkpoint_lsn| { - checkpoint_lsn == last_runtime_lsn && checkpoint_lsn >= writer_last_commit_lsn - }) - { - // An explicit checkpoint from this handle can fold exactly the - // current runtime into the database file and reset the live WAL - // end to 0. Only preserve the runtime before any post-checkpoint - // WAL frames exist; lower nonzero LSNs after WAL reuse must reload. - self.inner - .last_runtime_lsn - .store(latest_lsn, Ordering::Release); - return Ok(()); - } - - let schema_cookie = self.current_schema_cookie()?; - let (mut runtime, runtime_lsn) = EngineRuntime::load_from_storage( - &self.inner.pager, - &self.inner.wal, - schema_cookie, - &self.inner.config, - )?; - runtime.set_audit_context_handle(Arc::clone(&self.inner.audit_context)); - self.apply_temp_state_to_runtime(&mut runtime)?; - self.inner - .catalog - .replace(runtime.catalog.as_ref().clone())?; - let mut guard = self - .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - *guard = runtime; - self.inner - .last_runtime_lsn - .store(runtime_lsn, Ordering::Release); - Ok(()) - } - - fn refresh_and_ensure_all_tables_loaded(&self) -> Result<()> { - if !self.inner.config.defer_table_materialization { - self.refresh_engine_from_storage()?; - self.ensure_all_tables_loaded()?; - return Ok(()); - } - - let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - self.refresh_engine_from_snapshot(snapshot_lsn)?; - self.ensure_all_tables_loaded_at_snapshot(Some(snapshot_lsn))?; - drop(reader); - Ok(()) - } - - fn refresh_and_load_tables_for_statement_at_latest_snapshot( - &self, - statement: &SqlStatement, - ) -> Result<()> { - if !self.inner.config.defer_table_materialization { - self.refresh_engine_from_storage()?; - self.ensure_all_tables_loaded()?; - return Ok(()); - } - - let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - self.refresh_engine_from_snapshot(snapshot_lsn)?; - let targeted_ok = - self.ensure_tables_loaded_for_statement_at_snapshot(statement, Some(snapshot_lsn))?; - if !targeted_ok { - self.ensure_all_tables_loaded_at_snapshot(Some(snapshot_lsn))?; - } - drop(reader); - Ok(()) - } - - /// Materializes deferred tables specified by name. - /// - /// Fast path (no matching deferred tables): one read-lock check. - /// Slow path: drops the read lock, takes a write lock, loads only the - /// specified tables and rebuilds their indexes, then releases. - /// - /// This enables per-table on-demand loading for ADR 0143 Phase B. - fn ensure_tables_loaded_at_snapshot( - &self, - names: &[&str], - snapshot_lsn: Option, - ) -> Result<()> { - { - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let has_deferred = runtime.has_deferred_tables(); - let has_match = names.iter().any(|name| { - runtime - .deferred_table_names() - .any(|dt| dt.eq_ignore_ascii_case(name)) - }); - if !has_deferred || !has_match { - return Ok(()); - } - } - let filter: BTreeSet = names.iter().map(|s| s.to_string()).collect(); - let mut runtime = self - .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - if let Some(snapshot_lsn) = snapshot_lsn { - if self.inner.config.paged_row_storage { - runtime.load_deferred_table_row_sources_filtered_at_snapshot( - &self.inner.pager, - &self.inner.wal, - self.inner.config.page_size, - &filter, - snapshot_lsn, - ) - } else { - runtime.load_deferred_tables_filtered_at_snapshot( - &self.inner.pager, - &self.inner.wal, - self.inner.config.page_size, - &filter, - snapshot_lsn, - ) - } - } else if self.inner.config.paged_row_storage { - runtime.load_deferred_table_row_sources_filtered( - &self.inner.pager, - &self.inner.wal, - self.inner.config.page_size, - &filter, - ) - } else { - runtime.load_deferred_tables_filtered( - &self.inner.pager, - &self.inner.wal, - self.inner.config.page_size, - &filter, - ) - } - } - - fn security_catalog_table_names() -> [&'static str; 2] { - [ - crate::security::POLICIES_TABLE, - crate::security::MASKS_TABLE, - ] - } - - fn runtime_has_deferred_security_tables(runtime: &EngineRuntime) -> bool { - runtime.has_deferred_tables() - && Self::security_catalog_table_names().iter().any(|name| { - runtime - .deferred_table_names() - .any(|candidate| candidate.eq_ignore_ascii_case(name)) - }) - } - - fn runtime_read_for_fast_read_at_snapshot( - &self, - snapshot_lsn: u64, - ) -> Result>> { - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - if Self::runtime_has_deferred_security_tables(&runtime) { - drop(runtime); - self.ensure_tables_loaded_at_snapshot( - &Self::security_catalog_table_names(), - Some(snapshot_lsn), - )?; - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - if runtime.security_rules_active()? { - return Ok(None); - } - return Ok(Some(runtime)); - } - if runtime.security_rules_active()? { - return Ok(None); - } - Ok(Some(runtime)) - } - - fn runtime_read_for_observed_current_resident_fast_read( - &self, - snapshot_lsn: u64, - ) -> Result>> { - if !self.observed_current_runtime_is_current(snapshot_lsn) { - return Ok(None); - } - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - if Self::runtime_has_deferred_security_tables(&runtime) - || runtime.security_rules_active()? - { - return Ok(None); - } - if !self.observed_current_runtime_is_current(snapshot_lsn) { - return Ok(None); - } - Ok(Some(runtime)) - } - - fn ensure_security_tables_loaded_at_snapshot(&self, snapshot_lsn: u64) -> Result { - self.ensure_tables_loaded_at_snapshot( - &Self::security_catalog_table_names(), - Some(snapshot_lsn), - )?; - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - runtime.security_rules_active() - } - - fn load_security_tables_for_runtime_at_snapshot( - &self, - runtime: &mut EngineRuntime, - snapshot_lsn: u64, - ) -> Result { - self.load_runtime_table_row_sources_at_snapshot( - runtime, - &Self::security_catalog_table_names(), - snapshot_lsn, - )?; - runtime.security_rules_active() - } - - fn ensure_table_row_sources_loaded_at_snapshot( - &self, - names: &[&str], - snapshot_lsn: u64, - ) -> Result<()> { - let filter: BTreeSet = names.iter().map(|s| s.to_string()).collect(); - { - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let has_deferred = runtime.has_deferred_tables(); - let has_match = names.iter().any(|name| { - runtime - .deferred_table_names() - .any(|dt| dt.eq_ignore_ascii_case(name)) - }); - if !has_deferred || !has_match { - return Ok(()); - } - } - let mut runtime = self - .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - runtime.load_deferred_table_row_sources_filtered_at_snapshot( - &self.inner.pager, - &self.inner.wal, - self.inner.config.page_size, - &filter, - snapshot_lsn, - ) - } - - fn hydrate_deferred_runtime_index_at_snapshot( - &self, - table_name: &str, - index_name: &str, - snapshot_lsn: u64, - ) -> Result<()> { - { - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let has_deferred = runtime.has_deferred_tables(); - let has_match = runtime - .deferred_table_names() - .any(|deferred| identifiers_equal(deferred, table_name)); - if !has_deferred || !has_match { - return Ok(()); - } - } - let mut runtime = self - .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - runtime.hydrate_deferred_runtime_index_at_snapshot( - &self.inner.pager, - &self.inner.wal, - self.inner.config.page_size, - table_name, - index_name, - snapshot_lsn, - ) - } - - fn prepared_read_row_source_row_limit(&self) -> usize { - let row_limit = self - .inner - .config - .cache_size_mb - .saturating_mul(PREPARED_READ_ROW_SOURCE_ROWS_PER_CACHE_MB); - if row_limit == 0 { - 0 - } else { - row_limit.max(PREPARED_READ_ROW_SOURCE_MIN_ROW_LIMIT) - } - } - - fn try_load_prepared_read_row_sources_at_snapshot( - &self, - names: &[&str], - snapshot_lsn: u64, - ) -> Result<()> { - if names.is_empty() - || !self.inner.config.defer_table_materialization - || !self.inner.config.paged_row_storage - { - return Ok(()); - } - - let row_limit = self.prepared_read_row_source_row_limit(); - if row_limit == 0 { - return Ok(()); - } - - let mut to_load = BTreeSet::new(); - { - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - for name in names { - let Some(table_name) = runtime.canonical_catalog_table_name(name) else { - continue; - }; - if runtime.table_row_source(&table_name).is_some() { - continue; - } - let Some(state) = runtime.persisted_tables.get(&table_name).copied() else { - continue; - }; - if !runtime.deferred_tables.contains(&table_name) { - continue; - } - if !state.pointer.is_table_paged_manifest() - || state.row_count < PREPARED_READ_ROW_SOURCE_MIN_ROWS - || state.row_count > row_limit - { - return Ok(()); - } - to_load.insert(table_name); - } - } - - if to_load.is_empty() { - return Ok(()); - } - - { - let mut runtime = self - .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - runtime.load_deferred_table_row_sources_filtered_at_snapshot( - &self.inner.pager, - &self.inner.wal, - self.inner.config.page_size, - &to_load, - snapshot_lsn, - )?; - } - - let loaded_refs = to_load.iter().map(String::as_str).collect::>(); - self.touch_read_only_paged_row_sources_by_name(&loaded_refs) - } - - fn touch_read_only_paged_row_sources_by_name(&self, names: &[&str]) -> Result<()> { - if names.is_empty() - || !self.inner.config.defer_table_materialization - || !self.inner.config.paged_row_storage - { - return Ok(()); - } - - let (touched_tables, all_paged_tables) = { - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let mut touched_tables = BTreeSet::new(); - let mut all_paged_tables = BTreeSet::new(); - for (name, state) in runtime.persisted_tables.iter() { - if state.pointer.is_table_paged_manifest() { - all_paged_tables.insert(name.clone()); - } - } - for name in names { - let Some(table_name) = runtime.canonical_catalog_table_name(name) else { - continue; - }; - if all_paged_tables.contains(&table_name) - && runtime.table_row_source(&table_name).is_some() - { - touched_tables.insert(table_name); - } - } - (touched_tables, all_paged_tables) - }; - - if touched_tables.is_empty() { - return Ok(()); - } - - let mut to_redefer: Vec = Vec::new(); - { - let mut residency = self - .inner - .read_only_paged_row_source_residency - .lock() - .map_err(|_| { - DbError::internal("read-only paged row source residency lock poisoned") - })?; - residency - .table_touch_generation - .retain(|name, _| all_paged_tables.contains(name)); - let touch_gen = residency.next_touch_gen; - residency.next_touch_gen = residency.next_touch_gen.saturating_add(1); - for table_name in touched_tables { - residency - .table_touch_generation - .insert(table_name, touch_gen); - } - if residency.table_touch_generation.len() > AUTOCOMMIT_PAGED_ROW_SOURCE_MAX_RESIDENT { - let mut ordered_touch = residency - .table_touch_generation - .iter() - .map(|(name, generation)| (name, *generation)) - .collect::>(); - ordered_touch.sort_by_key(|(_, generation)| *generation); - let overflow = ordered_touch.len() - AUTOCOMMIT_PAGED_ROW_SOURCE_MAX_RESIDENT; - to_redefer.reserve(overflow); - for (name, _) in ordered_touch.iter().take(overflow) { - to_redefer.push((*name).clone()); - } - for name in &to_redefer { - residency.table_touch_generation.remove(name); - } - } - } - - if to_redefer.is_empty() { - Ok(()) - } else { - let redefer_refs = to_redefer.iter().map(String::as_str).collect::>(); - self.redefer_persisted_tables(&redefer_refs) - } - } - - /// Fast path for non-transactional reads when deferred materialization is - /// enabled but the statement's base tables are already resident at the - /// pinned reader snapshot. - /// - /// Returns a read guard over the resident runtime when the statement can - /// be executed without reloading row sources. - /// Returns `Ok(None)` when any referenced base table is not resident, the - /// runtime LSN is stale, a checkpoint has advanced, or the statement's - /// base-table set cannot be resolved (callers fall back to the deferred - /// load path in that case). - fn try_resident_read_for_statement_at_snapshot( - &self, - statement: &SqlStatement, - prepared: Option<&PreparedStatement>, - snapshot_lsn: u64, - ) -> Result>> { - if !self.inner.config.defer_table_materialization { - return Ok(None); - } - let checkpoint_epoch = self.inner.wal.checkpoint_epoch(); - let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); - let last_seen_checkpoint_epoch = self - .inner - .last_seen_checkpoint_epoch - .load(Ordering::Acquire); - if last_runtime_lsn != snapshot_lsn || last_seen_checkpoint_epoch != checkpoint_epoch { - return Ok(None); - } - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let current_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); - let current_seen_checkpoint_epoch = self - .inner - .last_seen_checkpoint_epoch - .load(Ordering::Acquire); - if current_runtime_lsn != snapshot_lsn || current_seen_checkpoint_epoch != checkpoint_epoch - { - return Ok(None); - } - self.validate_prepared_against_runtime(prepared, &runtime)?; - if Self::runtime_has_deferred_security_tables(&runtime) - || runtime.security_rules_active()? - { - return Ok(None); - } - let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(&runtime, statement) - else { - return Ok(None); - }; - if base_tables.is_empty() { - return Ok(Some(runtime)); - } - let all_resident = base_tables.iter().all(|name| { - runtime - .canonical_catalog_table_name(name) - .is_some_and(|table_name| runtime.table_row_source(&table_name).is_some()) - }); - if all_resident { - Ok(Some(runtime)) - } else { - Ok(None) - } - } - - fn try_resident_read_for_single_process_statement( - &self, - statement: &SqlStatement, - prepared: Option<&PreparedStatement>, - ) -> Result>> { - if !self.inner.config.defer_table_materialization { - return Ok(None); - } - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - self.validate_prepared_against_runtime(prepared, &runtime)?; - if Self::runtime_has_deferred_security_tables(&runtime) - || runtime.security_rules_active()? - { - return Ok(None); - } - let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(&runtime, statement) - else { - return Ok(None); - }; - if base_tables.is_empty() { - return Ok(Some(runtime)); - } - let all_resident = base_tables.iter().all(|name| { - runtime - .canonical_catalog_table_name(name) - .is_some_and(|table_name| runtime.table_row_source(&table_name).is_some()) - }); - if all_resident { - Ok(Some(runtime)) - } else { - Ok(None) - } - } - - fn runtime_read_for_prepared_row_sources_at_snapshot( - &self, - names: &[&str], - snapshot_lsn: u64, - ) -> Result>> { - if names.is_empty() { - return Ok(None); - } - let latest_checkpoint_epoch = self.inner.wal.checkpoint_epoch(); - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); - let last_seen_checkpoint_epoch = self - .inner - .last_seen_checkpoint_epoch - .load(Ordering::Acquire); - if last_runtime_lsn != snapshot_lsn - || last_seen_checkpoint_epoch != latest_checkpoint_epoch - || names.iter().any(|name| { - runtime - .canonical_catalog_table_name(name) - .is_none_or(|table_name| runtime.table_row_source(&table_name).is_none()) - }) - { - return Ok(None); - } - Ok(Some(runtime)) - } - - fn try_resident_read_for_prepared_table_statement( - &self, - prepared: &PreparedStatement, - table_name: &str, - ) -> Result>> { - if !self.inner.config.defer_table_materialization { - return Ok(None); - } - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - self.validate_prepared_against_runtime(Some(prepared), &runtime)?; - if Self::runtime_has_deferred_security_tables(&runtime) - || runtime.security_rules_active()? - { - return Ok(None); - } - if runtime - .canonical_catalog_table_name(table_name) - .is_some_and(|canonical| runtime.table_row_source(&canonical).is_some()) - { - Ok(Some(runtime)) - } else { - Ok(None) - } - } - - fn load_simple_write_row_sources_at_latest_snapshot(&self, names: &[&str]) -> Result<()> { - if !self.inner.config.defer_table_materialization { - self.refresh_engine_from_storage()?; - self.ensure_tables_loaded_at_snapshot(names, None)?; - return Ok(()); - } - - if self.simple_write_row_sources_loaded_for_current_runtime(names)? { - return Ok(()); - } - - let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - self.refresh_engine_from_snapshot(snapshot_lsn)?; - self.ensure_table_row_sources_loaded_at_snapshot(names, snapshot_lsn)?; - drop(reader); - Ok(()) - } - - fn load_simple_write_row_sources_and_child_indexes_at_latest_snapshot( - &self, - names: &[&str], - child_index_targets: &[(&str, &str)], - ) -> Result<()> { - if !self.inner.config.defer_table_materialization { - self.refresh_engine_from_storage()?; - self.ensure_tables_loaded_at_snapshot(names, None)?; - return Ok(()); - } - - let indexes_loaded = { - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - child_index_targets - .iter() - .all(|(_, index_name)| runtime.index(index_name).is_some()) - }; - if self.simple_write_row_sources_loaded_for_current_runtime(names)? && indexes_loaded { - return Ok(()); - } - - let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - self.refresh_engine_from_snapshot(snapshot_lsn)?; - for (table_name, index_name) in child_index_targets { - self.hydrate_deferred_runtime_index_at_snapshot(table_name, index_name, snapshot_lsn)?; - } - self.ensure_table_row_sources_loaded_at_snapshot(names, snapshot_lsn)?; - drop(reader); - Ok(()) - } - - fn simple_write_row_sources_loaded_for_current_runtime(&self, names: &[&str]) -> Result { - let latest_lsn = self.inner.wal.latest_snapshot(); - let latest_checkpoint_epoch = self.inner.wal.checkpoint_epoch(); - let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); - let last_seen_checkpoint_epoch = self - .inner - .last_seen_checkpoint_epoch - .load(Ordering::Acquire); - - if latest_lsn > last_runtime_lsn || latest_checkpoint_epoch != last_seen_checkpoint_epoch { - return Ok(false); - } - - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let has_deferred_match = names.iter().any(|name| { - runtime - .deferred_table_names() - .any(|deferred| identifiers_equal(deferred, name)) - }); - Ok(!has_deferred_match) - } - - fn load_statement_row_sources_at_latest_snapshot( - &self, - statement: &SqlStatement, - ) -> Result { - if !self.inner.config.defer_table_materialization { - return self.ensure_tables_loaded_for_statement_at_snapshot(statement, None); - } - - let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - self.refresh_engine_from_snapshot(snapshot_lsn)?; - let mut runtime = self - .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(&runtime, statement) - else { - drop(reader); - return Ok(false); - }; - if base_tables.is_empty() { - drop(reader); - return Ok(true); - } - let base_refs: Vec<&str> = base_tables.iter().map(String::as_str).collect(); - self.load_runtime_table_row_sources_at_snapshot(&mut runtime, &base_refs, snapshot_lsn)?; - drop(reader); - Ok(true) - } - - fn can_execute_statement_with_row_sources_at_latest_snapshot( - &self, - statement: &SqlStatement, - ) -> Result { - if !self.inner.config.defer_table_materialization { - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - return Ok(runtime.can_execute_statement_in_state_without_clone(statement)); - } - - let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - self.refresh_engine_from_snapshot(snapshot_lsn)?; - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(&runtime, statement) - else { - drop(reader); - return Ok(false); - }; - let mut working = runtime.clone(); - drop(runtime); - let base_refs: Vec<&str> = base_tables.iter().map(String::as_str).collect(); - self.load_runtime_table_row_sources_at_snapshot(&mut working, &base_refs, snapshot_lsn)?; - drop(reader); - Ok(working.can_execute_statement_in_state_without_clone(statement)) - } - - fn redefer_persisted_tables(&self, names: &[&str]) -> Result<()> { - self.redefer_persisted_tables_inner(names, true) - } - - fn redefer_persisted_tables_inner( - &self, - names: &[&str], - release_heap_after_drop: bool, - ) -> Result<()> { - if !self.inner.config.defer_table_materialization || names.is_empty() { - return Ok(()); - } - let mut runtime = self - .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let freed_bytes = runtime.redefer_persisted_tables(names); - drop(runtime); - if release_heap_after_drop { - self.release_freed_heap_after_paged_row_source_drop(freed_bytes); - } - Ok(()) - } - - fn should_redefer_paged_row_sources_after_write(&self) -> bool { - self.inner.config.defer_table_materialization - && self.inner.config.paged_row_storage - && !self.inner.config.retain_paged_row_sources_after_commit - } - - fn runtime_should_redefer_persisted_tables_after_write( - &self, - runtime: &EngineRuntime, - names: &[&str], - ) -> bool { - self.should_redefer_paged_row_sources_after_write() - && runtime.has_redeferable_persisted_tables(names) - } - - fn redefer_persisted_tables_after_write(&self, names: &[&str]) -> Result<()> { - if self.should_redefer_paged_row_sources_after_write() { - self.redefer_persisted_tables_inner(names, false) - } else { - Ok(()) - } - } - - fn redefer_all_persisted_paged_tables(&self) -> Result<()> { - if !self.inner.config.defer_table_materialization || !self.inner.config.paged_row_storage { - return Ok(()); - } - let mut runtime = self - .inner - .engine - .write() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let freed_bytes = runtime.redefer_all_persisted_paged_tables(); - drop(runtime); - self.release_freed_heap_after_paged_row_source_drop(freed_bytes); - Ok(()) - } - - fn redefer_read_only_row_sources( - &self, - statement: &SqlStatement, - allow_redefer_all_on_unknown: bool, - ) -> Result<()> { - if !self.inner.config.defer_table_materialization || !self.inner.config.paged_row_storage { - return Ok(()); - } - let (touched_tables, all_paged_tables) = { - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let base_tables = match self.safe_referenced_base_tables_in_runtime(&runtime, statement) - { - Some(base_tables) => base_tables, - None => { - drop(runtime); - return if allow_redefer_all_on_unknown { - self.redefer_all_persisted_paged_tables() - } else { - Ok(()) - }; - } - }; - if base_tables.is_empty() { - return Ok(()); - } - let mut touched_tables = BTreeSet::new(); - let mut all_paged_tables = BTreeSet::new(); - for (name, state) in runtime.persisted_tables.iter() { - if state.pointer.is_table_paged_manifest() { - all_paged_tables.insert(name.clone()); - } - } - for base_table in base_tables { - if let Some(table_name) = runtime.canonical_catalog_table_name(&base_table) { - if runtime - .persisted_tables - .get(&table_name) - .is_some_and(|state| state.pointer.is_table_paged_manifest()) - { - touched_tables.insert(table_name); - } - } - } - (touched_tables, all_paged_tables) - }; - if touched_tables.is_empty() { - if allow_redefer_all_on_unknown { - self.redefer_all_persisted_paged_tables() - } else { - Ok(()) - } - } else { - let mut to_redefer: Vec = Vec::new(); - { - let mut residency = self - .inner - .read_only_paged_row_source_residency - .lock() - .map_err(|_| { - DbError::internal("read-only paged row source residency lock poisoned") - })?; - residency - .table_touch_generation - .retain(|name, _| all_paged_tables.contains(name)); - let touch_gen = residency.next_touch_gen; - residency.next_touch_gen = residency.next_touch_gen.saturating_add(1); - for table_name in touched_tables { - residency - .table_touch_generation - .insert(table_name, touch_gen); - } - if residency.table_touch_generation.len() > AUTOCOMMIT_PAGED_ROW_SOURCE_MAX_RESIDENT - { - let mut ordered_touch = residency - .table_touch_generation - .iter() - .map(|(name, generation)| (name, *generation)) - .collect::>(); - ordered_touch.sort_by_key(|(_, generation)| *generation); - let overflow = ordered_touch.len() - AUTOCOMMIT_PAGED_ROW_SOURCE_MAX_RESIDENT; - to_redefer.reserve(overflow); - for (name, _) in ordered_touch.iter().take(overflow) { - to_redefer.push((*name).clone()); - } - for name in &to_redefer { - residency.table_touch_generation.remove(name); - } - } - } - if to_redefer.is_empty() { - Ok(()) - } else { - let redefer_refs = to_redefer.iter().map(String::as_str).collect::>(); - self.redefer_persisted_tables(&redefer_refs) - } - } + self.configure_runtime_sync_capture(&mut runtime)?; + Ok(ExclusiveSqlTxnState { + runtime, + snapshot_reader: Some(snapshot_reader), + base_lsn: current_lsn, + base_checkpoint_epoch: current_epoch, + persistent_changed: false, + indexes_maybe_stale: false, + prepared_insert_runtime_cache: HashMap::new(), + prepared_insert_last_cache_key: None, + prepared_insert_last_plan: None, + prepared_insert_last_next_row_id: None, + prepared_insert_candidate: Vec::new(), + }) } - fn release_freed_heap_after_paged_row_source_drop(&self, freed_bytes: usize) { - if self.inner.config.paged_row_storage - && should_release_freed_paged_row_source_heap(freed_bytes) - { - self.release_freed_heap_if_configured(); + fn commit_exclusive_sql_txn(&self, mut state: ExclusiveSqlTxnState<'_>) -> Result { + Self::flush_exclusive_prepared_insert_next_row_id(&mut state)?; + if !state.persistent_changed { + self.sync_temp_state_from_runtime(&state.runtime)?; + return Ok(state.base_lsn); } - } - fn release_freed_heap_after_runtime_compaction(&self, freed_bytes: usize) { - if freed_bytes >= RESIDENT_COMMIT_HEAP_RELEASE_THRESHOLD { - self.release_freed_heap_if_configured(); + let runtime_schema_cookie = state.runtime.catalog.schema_cookie; + if state.indexes_maybe_stale { + state + .runtime + .rebuild_stale_indexes(self.inner.config.page_size)?; } + let reactive_pending = self.take_reactive_pending_commit(&mut state.runtime); + self.begin_write()?; + if let Err(error) = state.runtime.persist_to_db(self) { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut state.runtime)?; + return Err(error); + } + drop(state.snapshot_reader.take()); + let committed_lsn = match self.commit_if_latest(state.base_lsn, state.base_checkpoint_epoch) + { + Ok(lsn) => lsn, + Err(error) => { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut state.runtime)?; + return Err(error); + } + }; + self.sync_post_commit(&mut state.runtime, committed_lsn)?; + if self.inner.catalog.schema_cookie()? != runtime_schema_cookie { + self.inner + .catalog + .replace(state.runtime.catalog.as_ref().clone())?; + } + self.sync_temp_state_from_runtime(&state.runtime)?; + if self.should_redefer_paged_row_sources_after_write() { + let freed_bytes = state.runtime.redefer_all_persisted_paged_tables(); + self.release_freed_heap_after_paged_row_source_drop(freed_bytes); + } + self.inner + .last_runtime_lsn + .store(committed_lsn, Ordering::Release); + self.inner + .writer_last_commit_lsn + .store(committed_lsn, Ordering::Release); + drop(state); + self.maybe_demote_wal_after_large_explicit_commit(); + self.publish_reactive_commit(reactive_pending, committed_lsn); + Ok(committed_lsn) } - fn release_freed_heap_if_configured(&self) { - if !self.inner.config.release_freed_memory_after_checkpoint { - return; - } - #[cfg(test)] - PAGED_ROW_SOURCE_HEAP_RELEASE_COUNT.with(|count| count.set(count.get().saturating_add(1))); - crate::wal::platform::release_freed_heap(); + fn rollback_exclusive_sql_txn(&self, mut state: ExclusiveSqlTxnState<'_>) -> Result<()> { + self.restore_runtime_from_storage(&mut state.runtime) } - fn maybe_demote_wal_after_large_explicit_commit(&self) { - let threshold = self.inner.config.wal_checkpoint_threshold_bytes; - if threshold == 0 { - return; - } - if self.inner.wal.latest_snapshot() < threshold { - return; + fn persist_runtime_if_latest( + &self, + runtime: EngineRuntime, + expected_latest: Option<(u64, u64)>, + rebuild_stale_indexes: bool, + ) -> Result { + let mut runtime = runtime; + let runtime_schema_cookie = runtime.catalog.schema_cookie; + if rebuild_stale_indexes { + runtime.rebuild_stale_indexes(self.inner.config.page_size)?; } - let target_bytes = threshold / 2; - if target_bytes == 0 { - return; + let compacted_bytes = runtime.compact_dirty_resident_storage_after_transaction_commit(); + let reactive_pending = self.take_reactive_pending_commit(&mut runtime); + self.begin_write()?; + if let Err(error) = runtime.persist_to_db(self) { + let _ = self.rollback(); + return Err(error); } - if matches!( + let committed_lsn = match expected_latest { + Some((lsn, epoch)) => self.commit_if_latest(lsn, epoch), + None => self.commit(), + }; + let committed_lsn = match committed_lsn { + Ok(lsn) => lsn, + Err(error) => { + let _ = self.rollback(); + return Err(error); + } + }; + self.sync_post_commit(&mut runtime, committed_lsn)?; + if self.inner.catalog.schema_cookie()? != runtime_schema_cookie { self.inner - .wal - .demote_resident_versions_if_reader_free(usize::try_from(target_bytes).unwrap_or(usize::MAX)), - Ok(demoted) if demoted > 0 - ) { - self.release_freed_heap_if_configured(); + .catalog + .replace(runtime.catalog.as_ref().clone())?; + } + self.sync_temp_state_from_runtime(&runtime)?; + let mut guard = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + *guard = runtime; + if self.should_redefer_paged_row_sources_after_write() { + let freed_bytes = guard.redefer_all_persisted_paged_tables(); + self.release_freed_heap_after_paged_row_source_drop(freed_bytes); } + self.release_freed_heap_after_runtime_compaction(compacted_bytes); + self.inner + .last_runtime_lsn + .store(committed_lsn, Ordering::Release); + self.inner + .writer_last_commit_lsn + .store(committed_lsn, Ordering::Release); + drop(guard); + self.maybe_demote_wal_after_large_explicit_commit(); + self.publish_reactive_commit(reactive_pending, committed_lsn); + + Ok(committed_lsn) } - fn redefer_statement_tables(&self, statement: &SqlStatement) -> Result<()> { - if !self.should_redefer_paged_row_sources_after_write() { - return Ok(()); + fn runtime_for_targeted_row_source_inspection(&self) -> Result<(EngineRuntime, Option)> { + if let Some((runtime, snapshot_lsn)) = self.transaction_runtime_snapshot_with_lsn()? { + return Ok((runtime, Some(snapshot_lsn))); } - let names = { - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let Some(base_tables) = - self.safe_referenced_base_tables_in_runtime(&runtime, statement) - else { - return Ok(()); - }; - base_tables - }; - let name_refs: Vec<&str> = names.iter().map(String::as_str).collect(); - self.redefer_persisted_tables_after_write(&name_refs) + if !self.inner.config.defer_table_materialization { + self.refresh_engine_from_storage()?; + return Ok((self.engine_snapshot()?, None)); + } + let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + self.refresh_engine_from_snapshot(snapshot_lsn)?; + drop(reader); + Ok((self.engine_snapshot()?, Some(snapshot_lsn))) } - fn finalize_row_source_autocommit_statement( - &self, - statement: &SqlStatement, - result: Result, - ) -> Result { - let redefer_result = if statement_is_read_only(statement) { - self.redefer_read_only_row_sources(statement, false) - } else { - self.redefer_statement_tables(statement) - }; - match (result, redefer_result) { - (Ok(result), Ok(())) => Ok(result), - (Err(error), Ok(())) => Err(error), - (Ok(_), Err(error)) => Err(error), - (Err(error), Err(_)) => Err(error), + fn validate_watch_tables(&self, tables: &[String]) -> Result> { + if tables.is_empty() { + return Err(DbError::sql("watch table list must not be empty")); + } + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let mut canonical = BTreeSet::new(); + for table in tables { + let schema = runtime + .catalog + .table(table) + .ok_or_else(|| DbError::sql(format!("unknown watch table {table}")))?; + if schema.temporary || crate::sync::is_internal_table_name(&schema.name) { + return Err(DbError::sql(format!( + "table {} is not watchable", + schema.name + ))); + } + canonical.insert(schema.name.clone()); } + Ok(canonical) } - fn finalize_row_source_autocommit_statement_with_full_redefer( - &self, - statement: &SqlStatement, - result: Result, - ) -> Result { - let redefer_result = if statement_is_read_only(statement) { - self.redefer_read_only_row_sources(statement, true) - } else if self.should_redefer_paged_row_sources_after_write() { - self.redefer_all_persisted_paged_tables() - } else { - Ok(()) - }; - match (result, redefer_result) { - (Ok(result), Ok(())) => Ok(result), - (Err(error), Ok(())) => Err(error), - (Ok(_), Err(error)) => Err(error), - (Err(error), Err(_)) => Err(error), + fn validate_watch_range_table(&self, table: &str) -> Result { + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let schema = runtime + .catalog + .table(table) + .ok_or_else(|| DbError::sql(format!("unknown watch table {table}")))?; + if schema.temporary || crate::sync::is_internal_table_name(&schema.name) { + return Err(DbError::sql(format!( + "table {} is not watchable", + schema.name + ))); + } + if schema.primary_key_columns.is_empty() { + return Err(DbError::sql(format!( + "range watch requires a primary key on table {}", + schema.name + ))); } + Ok(schema.name.clone()) } - fn begin_sql_snapshot(&self) -> Result<(ReaderGuard, u64, u64)> { - #[cfg(feature = "bench-internals")] - READ_PATH_WAL_READER_BEGIN_COUNT.fetch_add(1, Ordering::Relaxed); - let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - self.refresh_engine_from_snapshot(snapshot_lsn)?; - let checkpoint_epoch = self.inner.wal.checkpoint_epoch(); - Ok((reader, snapshot_lsn, checkpoint_epoch)) + fn query_watch_dependencies( + &self, + statement: &crate::sql::ast::Statement, + ) -> Result> { + let referenced = crate::sql::ast::safe_referenced_tables(statement) + .ok_or_else(|| DbError::sql("query dependencies are not watchable for this SELECT"))?; + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let mut dependencies = BTreeSet::new(); + for name in referenced { + if let Some(table) = runtime.catalog.table(&name) { + if table.temporary || crate::sync::is_internal_table_name(&table.name) { + return Err(DbError::sql(format!( + "table {} is not watchable", + table.name + ))); + } + dependencies.insert(table.name.clone()); + } else if let Some(view) = runtime.catalog.view(&name) { + if view.temporary || crate::sync::is_internal_table_name(&view.name) { + return Err(DbError::sql(format!("view {} is not watchable", view.name))); + } + for dependency in &view.dependencies { + let table = runtime.catalog.table(dependency).ok_or_else(|| { + DbError::sql(format!( + "view {} depends on unknown table {}", + view.name, dependency + )) + })?; + if !table.temporary && !crate::sync::is_internal_table_name(&table.name) { + dependencies.insert(table.name.clone()); + } + } + } else { + return Err(DbError::sql(format!( + "query dependency {name} is not a watchable table or view" + ))); + } + } + if dependencies.is_empty() { + return Err(DbError::sql( + "query subscription has no watchable table dependencies", + )); + } + Ok(dependencies) } - fn load_runtime_table_row_sources_at_snapshot( + fn ensure_inspection_table_row_source( &self, runtime: &mut EngineRuntime, - names: &[&str], - snapshot_lsn: u64, + table_name: &str, + snapshot_lsn: Option, ) -> Result<()> { - if names.is_empty() || !runtime.has_deferred_tables() { + if runtime.table_row_source(table_name).is_some() + || runtime.temp_table_schema(table_name).is_some() + { return Ok(()); } - let has_match = names.iter().any(|name| { - runtime - .deferred_table_names() - .any(|deferred| deferred.eq_ignore_ascii_case(name)) - }); - if !has_match { + let Some(snapshot_lsn) = snapshot_lsn else { return Ok(()); - } - let filter: BTreeSet = names.iter().map(|name| (*name).to_string()).collect(); - runtime.load_deferred_table_row_sources_filtered_at_snapshot( - &self.inner.pager, - &self.inner.wal, - self.inner.config.page_size, - &filter, - snapshot_lsn, - ) + }; + self.load_runtime_table_row_sources_at_snapshot(runtime, &[table_name], snapshot_lsn) } - fn load_runtime_table_row_sources_and_child_indexes_at_snapshot( + fn insert_dependency_table_names( &self, - runtime: &mut EngineRuntime, - names: &[&str], - child_index_targets: &[(&str, &str)], - snapshot_lsn: u64, - ) -> Result<()> { - self.load_runtime_table_row_sources_at_snapshot(runtime, names, snapshot_lsn)?; - for (_, index_name) in child_index_targets { - if runtime.index(index_name).is_none() { - runtime.rebuild_index(index_name, self.inner.config.page_size)?; + runtime: &EngineRuntime, + table_name: &str, + ) -> Result> { + let table = runtime + .table_schema(table_name) + .ok_or_else(|| DbError::sql(format!("unknown table {table_name}")))?; + let mut names = vec![table_name.to_string()]; + for foreign_key in &table.foreign_keys { + if !names + .iter() + .any(|name| identifiers_equal(name, &foreign_key.referenced_table)) + { + names.push(foreign_key.referenced_table.clone()); } } - Ok(()) + Ok(names) } - fn load_all_runtime_row_sources_at_snapshot( + fn redefer_inspection_table_row_source( &self, runtime: &mut EngineRuntime, - snapshot_lsn: u64, - ) -> Result<()> { - let table_names = runtime.deferred_table_names().cloned().collect::>(); - let table_refs = table_names.iter().map(String::as_str).collect::>(); - self.load_runtime_table_row_sources_at_snapshot(runtime, &table_refs, snapshot_lsn) + table_name: &str, + snapshot_lsn: Option, + ) { + if snapshot_lsn.is_some() && runtime.persisted_table_state(table_name).is_some() { + let _ = runtime.redefer_persisted_tables(&[table_name]); + } } - fn try_execute_query_with_row_sources_at_snapshot( - &self, - runtime: &EngineRuntime, - statement: &SqlStatement, - params: &[Value], - snapshot_lsn: u64, - rebuild_stale_indexes: bool, - ) -> Result> { - let SqlStatement::Query(_) = statement else { - return Ok(None); - }; - let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(runtime, statement) - else { - return Ok(None); - }; - let mut working = runtime.clone(); - let base_refs: Vec<&str> = base_tables.iter().map(String::as_str).collect(); - self.load_runtime_table_row_sources_at_snapshot(&mut working, &base_refs, snapshot_lsn)?; - if rebuild_stale_indexes { - working.rebuild_stale_indexes(self.inner.config.page_size)?; + fn runtime_for_metadata_inspection(&self) -> Result { + if let Some(runtime) = self.transaction_runtime_snapshot()? { + return Ok(runtime); } - let result = working.execute_read_statement(statement, params, self.inner.config.page_size); - drop(working); - Ok(Some(result?)) + self.refresh_engine_from_storage()?; + self.engine_snapshot() } - fn safe_referenced_base_tables_in_runtime( + fn runtime_table_row_count( &self, runtime: &EngineRuntime, - statement: &SqlStatement, - ) -> Option> { - let mut visited_triggers = BTreeSet::new(); - self.collect_safe_referenced_base_tables_in_runtime( - runtime, - statement, - &mut visited_triggers, - ) - } + table_name: &str, + snapshot_lsn: Option, + ) -> Result { + if let Some(table) = runtime.temp_table_schema(table_name) { + return Ok(runtime + .temp_table_data(&table.name) + .map_or(0, |data| data.rows.len())); + } - fn collect_safe_referenced_base_tables_in_runtime( - &self, - runtime: &EngineRuntime, - statement: &SqlStatement, - visited_triggers: &mut BTreeSet, - ) -> Option> { - use crate::sql::ast::safe_referenced_tables; + if let Some(source) = runtime.table_row_source(table_name) { + return Ok(source.row_count()); + } - let tables = safe_referenced_tables(statement)?; - let mut base_tables = Vec::new(); - for name in tables { - let is_base = runtime - .catalog - .tables - .keys() - .any(|entry| entry.eq_ignore_ascii_case(&name)); - let is_temp = runtime - .temp_tables - .keys() - .any(|entry| entry.eq_ignore_ascii_case(&name)); - if !is_base && !is_temp { - return None; - } - if is_base { - base_tables.push(name); + let state = runtime.persisted_table_state(table_name); + if let Some(state) = state { + // The persisted state belongs to the runtime snapshot selected by + // the caller. Writes keep its non-zero live-row count exact even + // when a paged row source is re-deferred, so do not fault the + // manifest back in merely to recount its chunks. + if state.row_count != 0 { + return Ok(state.row_count); } - } - if let SqlStatement::Delete(delete) = statement { - for child in runtime.delete_row_source_dependency_tables(delete)? { - if base_tables - .iter() - .any(|entry| entry.eq_ignore_ascii_case(&child)) - { - continue; - } - base_tables.push(child); + // A missing payload is an unambiguously empty table. A zero count + // paired with a non-empty legacy/paged pointer remains ambiguous: + // older catalogs did not persist the count unless ANALYZE stats + // were present, so that case must fall through to storage. + if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { + return Ok(0); } } - if let SqlStatement::Insert(insert) = statement { - for child in runtime.insert_row_source_dependency_tables(insert)? { - if base_tables - .iter() - .any(|entry| entry.eq_ignore_ascii_case(&child)) - { - continue; - } - base_tables.push(child); + + if let Some(table) = runtime.catalog.table(table_name) { + if let Some(stats) = runtime.catalog.table_stats.get(&table.name) { + // Presence, rather than a non-zero value, distinguishes an + // analyzed empty table from a legacy catalog whose row count + // is unknown. Mutations invalidate these stats before the + // runtime is persisted. + return Ok(usize::try_from(stats.row_count.max(0)).unwrap_or(usize::MAX)); } } - if let SqlStatement::Update(update) = statement { - for child in runtime.update_row_source_dependency_tables(update)? { - if base_tables - .iter() - .any(|entry| entry.eq_ignore_ascii_case(&child)) - { - continue; - } - base_tables.push(child); - } + + let Some(state) = state else { + return Ok(0); + }; + + let store = if let Some(lsn) = snapshot_lsn { + PagerReadStore::with_snapshot_lsn(self, lsn) + } else { + PagerReadStore::new(self)? + }; + if state.pointer.is_table_paged_manifest() { + return read_persisted_table_row_count(&store, state); } - self.append_trigger_dependency_tables( - runtime, - statement, - &mut base_tables, - visited_triggers, - )?; - Some(base_tables) + + let payload = read_overflow(&store, state.pointer)?; + read_table_payload_live_row_count_from_bytes(&payload) } - fn append_trigger_dependency_tables( + fn runtime_table_row_count_without_storage( &self, runtime: &EngineRuntime, - statement: &SqlStatement, - base_tables: &mut Vec, - visited_triggers: &mut BTreeSet, - ) -> Option<()> { - let (target_name, event) = match statement { - SqlStatement::Insert(insert) => (insert.table_name.as_str(), TriggerEvent::Insert), - SqlStatement::Update(update) => (update.table_name.as_str(), TriggerEvent::Update), - SqlStatement::Delete(delete) => (delete.table_name.as_str(), TriggerEvent::Delete), - _ => return Some(()), - }; - for trigger in runtime.catalog.triggers.values() { - if trigger.on_view - || trigger.event != event - || !identifiers_equal(&trigger.target_name, target_name) - || !visited_triggers.insert(trigger.name.clone()) - { - continue; + table_name: &str, + ) -> Result> { + if runtime.temp_table_schema(table_name).is_some() { + return Ok(None); + } + + if let Some(source) = runtime.table_row_source(table_name) { + return Ok(Some(source.row_count())); + } + + let state = runtime.persisted_table_state(table_name); + if let Some(state) = state { + if state.row_count != 0 { + return Ok(Some(state.row_count)); } - let trigger_statement = parse_sql_statement(&trigger.action_sql).ok()?; - for table in self.collect_safe_referenced_base_tables_in_runtime( - runtime, - &trigger_statement, - visited_triggers, - )? { - if base_tables - .iter() - .any(|entry| entry.eq_ignore_ascii_case(&table)) - { - continue; - } - base_tables.push(table); + if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { + return Ok(Some(0)); } } - Some(()) + + if let Some(table) = runtime.catalog.table(table_name) { + if let Some(stats) = runtime.catalog.table_stats.get(&table.name) { + return Ok(Some( + usize::try_from(stats.row_count.max(0)).unwrap_or(usize::MAX), + )); + } + } + + if state.is_none() { + return Ok(Some(0)); + } + + Ok(None) } - fn try_execute_indexed_join_grouped_count_query_at_snapshot( - &self, - runtime: &EngineRuntime, - query: &crate::sql::ast::Query, - params: &[Value], - snapshot_lsn: u64, - ) -> Result> { - if !runtime.has_deferred_tables() { - return Ok(None); + fn runtime_for_prepare(&self) -> Result { + if let Some(runtime) = self.transaction_runtime_snapshot_for_prepare()? { + return Ok(runtime); } - let Some(parent_table_name) = - runtime.indexed_join_grouped_count_parent_table_name(query, params)? - else { + self.refresh_engine_from_storage()?; + // ADR 0143 Phase B: prepare() only needs catalog/schema metadata + // to plan a statement. Skip the eager all-tables materialization + // so applications that prepare a large number of statements at + // startup don't fault every persisted table into memory just to + // get a `PreparedStatement` handle. Row data is loaded on first + // execution by the read/write paths. + self.engine_snapshot_without_index_rebuild() + } + + fn transaction_runtime_snapshot_for_prepare(&self) -> Result> { + if !self.inner.sql_txn_active.load(Ordering::Acquire) { return Ok(None); + } + let txn = self + .inner + .sql_txn + .lock() + .map_err(|_| DbError::internal("SQL transaction lock poisoned"))?; + let state = match &*txn { + SqlTxnSlot::Shared(state) => state, + SqlTxnSlot::Exclusive => return Err(self.exclusive_sql_txn_error()), + SqlTxnSlot::None => return Ok(None), }; + Ok(Some(state.runtime.clone())) + } - let parent_table_name = parent_table_name.to_string(); - let mut join_runtime = runtime.clone(); - self.load_runtime_table_row_sources_at_snapshot( - &mut join_runtime, - &[parent_table_name.as_str()], - snapshot_lsn, - )?; - let result = join_runtime.try_execute_indexed_join_grouped_count_query(query, params); - drop(join_runtime); - result + fn transaction_runtime_snapshot(&self) -> Result> { + self.transaction_runtime_snapshot_with_lsn() + .map(|maybe| maybe.map(|(runtime, _)| runtime)) } - fn try_execute_simple_indexed_join_projection_query_at_snapshot( - &self, - runtime: &EngineRuntime, - statement: &SqlStatement, - query: &crate::sql::ast::Query, - params: &[Value], - snapshot_lsn: u64, - ) -> Result> { - if !runtime.has_deferred_tables() { + fn transaction_runtime_snapshot_with_lsn(&self) -> Result> { + if !self.inner.sql_txn_active.load(Ordering::Acquire) { return Ok(None); } - let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(runtime, statement) - else { - return Ok(None); + let txn = self + .inner + .sql_txn + .lock() + .map_err(|_| DbError::internal("SQL transaction lock poisoned"))?; + let state = match &*txn { + SqlTxnSlot::Shared(state) => state, + SqlTxnSlot::Exclusive => return Err(self.exclusive_sql_txn_error()), + SqlTxnSlot::None => return Ok(None), }; - if base_tables.is_empty() { - return Ok(None); + + let mut snapshot = state.runtime.clone(); + if state.indexes_maybe_stale { + snapshot.rebuild_stale_indexes(self.inner.config.page_size)?; } - let mut join_runtime = runtime.clone(); - let base_refs: Vec<&str> = base_tables.iter().map(String::as_str).collect(); - self.load_runtime_table_row_sources_at_snapshot( - &mut join_runtime, - &base_refs, - snapshot_lsn, + Ok(Some((snapshot, state.snapshot_lsn()))) + } + + fn restore_runtime_from_storage(&self, runtime: &mut EngineRuntime) -> Result<()> { + let schema_cookie = self.current_schema_cookie()?; + let (mut restored, restored_lsn) = EngineRuntime::load_from_storage( + &self.inner.pager, + &self.inner.wal, + schema_cookie, + &self.inner.config, )?; - let result = join_runtime.try_execute_simple_indexed_join_projection_query(query, params); - drop(join_runtime); - result + restored.set_audit_context_handle(Arc::clone(&self.inner.audit_context)); + self.apply_temp_state_to_runtime(&mut restored)?; + self.inner + .catalog + .replace(restored.catalog.as_ref().clone())?; + *runtime = restored; + self.inner + .last_runtime_lsn + .store(restored_lsn, Ordering::Release); + Ok(()) } - fn ensure_runtime_tables_loaded_at_snapshot( - &self, - runtime: &mut EngineRuntime, - names: &[&str], - snapshot_lsn: u64, - ) -> Result<()> { - if names.is_empty() || !runtime.has_deferred_tables() { + fn refresh_engine_from_snapshot(&self, snapshot_lsn: u64) -> Result<()> { + let latest_checkpoint_epoch = self.inner.wal.checkpoint_epoch(); + let mut last_seen_checkpoint_epoch = self + .inner + .last_seen_checkpoint_epoch + .load(Ordering::Acquire); + let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); + let writer_last_commit_lsn = self.inner.writer_last_commit_lsn.load(Ordering::Acquire); + let last_explicit_checkpoint_epoch = self + .inner + .last_explicit_checkpoint_epoch + .load(Ordering::Acquire); + let mut checkpoint_lsn_after_refresh = None; + if latest_checkpoint_epoch != last_seen_checkpoint_epoch { + let cached_header = self.inner.pager.header_snapshot()?; + let on_disk_header = self.inner.pager.header_from_disk()?; + checkpoint_lsn_after_refresh = Some(on_disk_header.last_checkpoint_lsn); + if on_disk_header.last_checkpoint_lsn != cached_header.last_checkpoint_lsn { + self.inner.pager.refresh_from_disk(on_disk_header)?; + } + self.inner + .last_seen_checkpoint_epoch + .store(latest_checkpoint_epoch, Ordering::Release); + last_seen_checkpoint_epoch = latest_checkpoint_epoch; + } + if snapshot_lsn == last_runtime_lsn && latest_checkpoint_epoch == last_seen_checkpoint_epoch + { return Ok(()); } - let has_match = names.iter().any(|name| { - runtime - .deferred_table_names() - .any(|deferred| deferred.eq_ignore_ascii_case(name)) - }); - if !has_match { + + if last_runtime_lsn > 0 + && writer_last_commit_lsn > 0 + && last_runtime_lsn >= writer_last_commit_lsn + && snapshot_lsn == 0 + && last_explicit_checkpoint_epoch == latest_checkpoint_epoch + && checkpoint_lsn_after_refresh.is_some_and(|checkpoint_lsn| { + checkpoint_lsn == last_runtime_lsn && checkpoint_lsn >= writer_last_commit_lsn + }) + { + // An explicit checkpoint from this handle can fold exactly the + // current runtime into the database file and reset the live WAL + // end to 0. Only preserve the hot runtime before any post- + // checkpoint WAL frames exist; otherwise the runtime would no + // longer match the pinned snapshot. + self.inner + .last_runtime_lsn + .store(snapshot_lsn, Ordering::Release); return Ok(()); } - let filter: BTreeSet = names.iter().map(|name| (*name).to_string()).collect(); - runtime.load_deferred_tables_filtered_at_snapshot( + + let schema_cookie = self.current_schema_cookie_at_snapshot(snapshot_lsn)?; + let mut runtime = EngineRuntime::load_from_storage_at_snapshot( &self.inner.pager, &self.inner.wal, - self.inner.config.page_size, - &filter, + schema_cookie, + &self.inner.config, snapshot_lsn, - ) + )?; + self.apply_temp_state_to_runtime(&mut runtime)?; + self.inner + .catalog + .replace(runtime.catalog.as_ref().clone())?; + let mut guard = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + *guard = runtime; + self.inner + .last_runtime_lsn + .store(snapshot_lsn, Ordering::Release); + self.inner + .last_seen_checkpoint_epoch + .store(latest_checkpoint_epoch, Ordering::Release); + Ok(()) } - fn ensure_runtime_tables_loaded_for_statement_at_snapshot( - &self, - runtime: &mut EngineRuntime, - statement: &SqlStatement, - snapshot_lsn: u64, - ) -> Result { - let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(runtime, statement) - else { - return Ok(false); + fn observed_current_resident_snapshot_lsn(&self) -> Result> { + let Some(snapshot_lsn) = self.inner.wal.observed_current_snapshot_lsn()? else { + return Ok(None); }; - if base_tables.is_empty() { - return Ok(true); + if self.observed_current_runtime_is_current(snapshot_lsn) { + return Ok(Some(snapshot_lsn)); } - let base_tables: Vec<&str> = base_tables.iter().map(String::as_str).collect(); - self.ensure_runtime_tables_loaded_at_snapshot(runtime, &base_tables, snapshot_lsn)?; - Ok(true) + self.try_preserve_observed_current_runtime_after_explicit_checkpoint(snapshot_lsn)?; + if self.observed_current_runtime_is_current(snapshot_lsn) { + return Ok(Some(snapshot_lsn)); + } + Ok(None) } - fn ensure_runtime_all_tables_loaded_at_snapshot( - &self, - runtime: &mut EngineRuntime, - snapshot_lsn: u64, - ) -> Result<()> { - if !runtime.has_deferred_tables() { - return Ok(()); - } - if self.inner.config.paged_row_storage { - runtime.load_deferred_table_row_sources_at_snapshot( - &self.inner.pager, - &self.inner.wal, - self.inner.config.page_size, - snapshot_lsn, - ) - } else { - runtime.load_deferred_tables_at_snapshot( - &self.inner.pager, - &self.inner.wal, - self.inner.config.page_size, - snapshot_lsn, - ) - } + fn observed_current_resident_snapshot_still_valid(&self, snapshot_lsn: u64) -> Result { + let Some(current_snapshot_lsn) = self.inner.wal.observed_current_snapshot_lsn()? else { + return Ok(false); + }; + Ok(current_snapshot_lsn == snapshot_lsn + && self.observed_current_runtime_is_current(snapshot_lsn)) } - fn execute_read_in_runtime_state( - &self, - statement: &SqlStatement, - params: &[Value], - runtime: &mut EngineRuntime, - snapshot_lsn: u64, - indexes_maybe_stale: &mut bool, - ) -> Result { - let security_active = - self.load_security_tables_for_runtime_at_snapshot(runtime, snapshot_lsn)?; - if self.statement_is_temp_only(runtime, statement) { - return runtime.execute_read_statement(statement, params, self.inner.config.page_size); - } - if !security_active && !*indexes_maybe_stale { - if let SqlStatement::Query(query) = statement { - if let Some(result) = self - .try_execute_indexed_join_grouped_count_query_at_snapshot( - runtime, - query, - params, - snapshot_lsn, - )? - { - return Ok(result); - } - if let Some(result) = self - .try_execute_simple_indexed_join_projection_query_at_snapshot( - runtime, - statement, - query, - params, - snapshot_lsn, - )? - { - return Ok(result); - } - } - } - if !security_active { - if let Some(result) = self.try_execute_query_with_row_sources_at_snapshot( - runtime, - statement, - params, - snapshot_lsn, - *indexes_maybe_stale, - )? { - return Ok(result); - } - } - let targeted_ok = self.ensure_runtime_tables_loaded_for_statement_at_snapshot( - runtime, - statement, - snapshot_lsn, - )?; - if !targeted_ok { - // Intentionally unsupported for row-source execution: the - // statement analyzer could not determine a conservative set of - // referenced base tables (CTEs, recursive queries, VALUES, - // subqueries, etc.). Fall back to broad-load so the generic - // executor has every table available. - self.ensure_runtime_all_tables_loaded_at_snapshot(runtime, snapshot_lsn)?; - } - if *indexes_maybe_stale { - runtime.rebuild_stale_indexes(self.inner.config.page_size)?; - *indexes_maybe_stale = false; - } - runtime.execute_read_statement(statement, params, self.inner.config.page_size) + fn observed_current_runtime_is_current(&self, snapshot_lsn: u64) -> bool { + let latest_checkpoint_epoch = self.inner.wal.checkpoint_epoch(); + let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); + let last_seen_checkpoint_epoch = self + .inner + .last_seen_checkpoint_epoch + .load(Ordering::Acquire); + snapshot_lsn == last_runtime_lsn && latest_checkpoint_epoch == last_seen_checkpoint_epoch } - fn execute_write_in_runtime_state( + fn try_preserve_observed_current_runtime_after_explicit_checkpoint( &self, - statement: &SqlStatement, - params: &[Value], - runtime: &mut EngineRuntime, snapshot_lsn: u64, - persistent_changed: &mut bool, - indexes_maybe_stale: &mut bool, - ) -> Result { - if matches!(statement, SqlStatement::Analyze { .. }) { - return Err(DbError::transaction( - "ANALYZE is not supported inside an explicit SQL transaction", - )); + ) -> Result<()> { + let latest_checkpoint_epoch = self.inner.wal.checkpoint_epoch(); + let last_seen_checkpoint_epoch = self + .inner + .last_seen_checkpoint_epoch + .load(Ordering::Acquire); + if latest_checkpoint_epoch == last_seen_checkpoint_epoch { + return Ok(()); } - if *indexes_maybe_stale { - runtime.rebuild_stale_indexes(self.inner.config.page_size)?; - *indexes_maybe_stale = false; + let cached_header = self.inner.pager.header_snapshot()?; + let on_disk_header = self.inner.pager.header_from_disk()?; + if on_disk_header.last_checkpoint_lsn != cached_header.last_checkpoint_lsn { + self.inner.pager.refresh_from_disk(on_disk_header.clone())?; } + self.inner + .last_seen_checkpoint_epoch + .store(latest_checkpoint_epoch, Ordering::Release); - let temp_only = self.statement_is_temp_only(runtime, statement); - match statement { - SqlStatement::Insert(insert) => { - let table_names = - self.insert_dependency_table_names(runtime, &insert.table_name)?; - let table_refs = table_names.iter().map(String::as_str).collect::>(); - self.load_runtime_table_row_sources_at_snapshot( - runtime, - &table_refs, - snapshot_lsn, - )?; - if let Some(prepared_insert) = runtime.prepare_simple_insert(insert)? { - let result = runtime.execute_prepared_simple_insert( - &prepared_insert, - params, - self.inner.config.page_size, - )?; - *persistent_changed |= !temp_only; - return Ok(result); - } - if runtime.can_execute_insert_in_place(insert) { - let result = runtime.execute_statement( - statement, - params, - self.inner.config.page_size, - )?; - *persistent_changed |= !temp_only; - return Ok(result); - } - } - SqlStatement::Update(update) => { - self.load_runtime_table_row_sources_at_snapshot( - runtime, - &[update.table_name.as_str()], - snapshot_lsn, - )?; - if let Some(prepared_update) = runtime.prepare_simple_update(update)? { - let result = runtime.execute_prepared_simple_update( - &prepared_update, - params, - self.inner.config.page_size, - )?; - *persistent_changed |= !temp_only; - return Ok(result); - } - } - SqlStatement::Delete(delete) => { - if let Some(prepared_delete) = runtime.prepare_simple_delete(delete)? { - let table_names = prepared_delete.required_row_source_table_names(); - let child_index_targets = prepared_delete.child_index_hydration_targets(); - self.load_runtime_table_row_sources_and_child_indexes_at_snapshot( - runtime, - &table_names, - &child_index_targets, - snapshot_lsn, - )?; - if runtime.can_reuse_prepared_simple_delete(&prepared_delete) { - let result = runtime.execute_prepared_simple_delete( - &prepared_delete, - params, - self.inner.config.page_size, - )?; - *persistent_changed |= !temp_only; - return Ok(result); - } - if let Some(prepared_delete) = runtime.prepare_simple_delete(delete)? { - let result = runtime.execute_prepared_simple_delete( - &prepared_delete, - params, - self.inner.config.page_size, - )?; - *persistent_changed |= !temp_only; - return Ok(result); - } - } + let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); + let writer_last_commit_lsn = self.inner.writer_last_commit_lsn.load(Ordering::Acquire); + let last_explicit_checkpoint_epoch = self + .inner + .last_explicit_checkpoint_epoch + .load(Ordering::Acquire); + if last_runtime_lsn > 0 + && writer_last_commit_lsn > 0 + && last_runtime_lsn >= writer_last_commit_lsn + && snapshot_lsn == 0 + && last_explicit_checkpoint_epoch == latest_checkpoint_epoch + && on_disk_header.last_checkpoint_lsn == last_runtime_lsn + && on_disk_header.last_checkpoint_lsn >= writer_last_commit_lsn + { + self.inner + .last_runtime_lsn + .store(snapshot_lsn, Ordering::Release); + } + Ok(()) + } + + fn refresh_engine_from_storage(&self) -> Result<()> { + self.inner + .wal + .refresh_from_coordination(&self.inner.pager)?; + let latest_lsn = self.inner.wal.latest_snapshot(); + let latest_checkpoint_epoch = self.inner.wal.checkpoint_epoch(); + let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); + let last_seen_checkpoint_epoch = self + .inner + .last_seen_checkpoint_epoch + .load(Ordering::Acquire); + let writer_last_commit_lsn = self.inner.writer_last_commit_lsn.load(Ordering::Acquire); + + if latest_lsn == last_runtime_lsn && latest_checkpoint_epoch == last_seen_checkpoint_epoch { + return Ok(()); + } + + let mut checkpoint_lsn_after_refresh = None; + if latest_checkpoint_epoch != last_seen_checkpoint_epoch { + let cached_header = self.inner.pager.header_snapshot()?; + let on_disk_header = self.inner.pager.header_from_disk()?; + checkpoint_lsn_after_refresh = Some(on_disk_header.last_checkpoint_lsn); + if on_disk_header.last_checkpoint_lsn != cached_header.last_checkpoint_lsn { + self.inner.pager.refresh_from_disk(on_disk_header)?; } - _ => {} + self.inner + .last_seen_checkpoint_epoch + .store(latest_checkpoint_epoch, Ordering::Release); } - if runtime.can_execute_statement_in_state_without_clone(statement) { - let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(runtime, statement) - else { - // Intentionally unsupported for targeted loading: the - // statement analyzer could not determine a conservative set - // of referenced base tables. Fall back to broad-load. - self.ensure_runtime_all_tables_loaded_at_snapshot(runtime, snapshot_lsn)?; - let result = - runtime.execute_statement(statement, params, self.inner.config.page_size)?; - *persistent_changed |= !temp_only; - return Ok(result); - }; - let base_refs: Vec<&str> = base_tables.iter().map(String::as_str).collect(); - self.load_runtime_table_row_sources_at_snapshot(runtime, &base_refs, snapshot_lsn)?; - let result = - runtime.execute_statement(statement, params, self.inner.config.page_size)?; - *persistent_changed |= !temp_only; - return Ok(result); + let last_explicit_checkpoint_epoch = self + .inner + .last_explicit_checkpoint_epoch + .load(Ordering::Acquire); + if last_runtime_lsn > 0 + && writer_last_commit_lsn > 0 + && last_runtime_lsn >= writer_last_commit_lsn + && latest_lsn == 0 + && last_explicit_checkpoint_epoch == latest_checkpoint_epoch + && checkpoint_lsn_after_refresh.is_some_and(|checkpoint_lsn| { + checkpoint_lsn == last_runtime_lsn && checkpoint_lsn >= writer_last_commit_lsn + }) + { + // An explicit checkpoint from this handle can fold exactly the + // current runtime into the database file and reset the live WAL + // end to 0. Only preserve the runtime before any post-checkpoint + // WAL frames exist; lower nonzero LSNs after WAL reuse must reload. + self.inner + .last_runtime_lsn + .store(latest_lsn, Ordering::Release); + return Ok(()); } - let mut working = runtime.clone(); - let targeted_ok = self.ensure_runtime_tables_loaded_for_statement_at_snapshot( - &mut working, - statement, - snapshot_lsn, + let schema_cookie = self.current_schema_cookie()?; + let (mut runtime, runtime_lsn) = EngineRuntime::load_from_storage( + &self.inner.pager, + &self.inner.wal, + schema_cookie, + &self.inner.config, )?; - if !targeted_ok { - // Intentionally unsupported for row-source execution: the - // statement analyzer could not determine a conservative set of - // referenced base tables (CTEs, recursive queries, VALUES, - // subqueries, etc.). Fall back to broad-load so the generic - // executor has every table available. - self.ensure_runtime_all_tables_loaded_at_snapshot(&mut working, snapshot_lsn)?; + runtime.set_audit_context_handle(Arc::clone(&self.inner.audit_context)); + self.apply_temp_state_to_runtime(&mut runtime)?; + self.inner + .catalog + .replace(runtime.catalog.as_ref().clone())?; + let mut guard = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + *guard = runtime; + self.inner + .last_runtime_lsn + .store(runtime_lsn, Ordering::Release); + Ok(()) + } + + fn refresh_and_ensure_all_tables_loaded(&self) -> Result<()> { + if !self.inner.config.defer_table_materialization { + self.refresh_engine_from_storage()?; + self.ensure_all_tables_loaded()?; + return Ok(()); } - working.rebuild_stale_indexes(self.inner.config.page_size)?; - let result = working.execute_statement(statement, params, self.inner.config.page_size)?; - *runtime = working; - *persistent_changed |= !temp_only; - *indexes_maybe_stale = true; - Ok(result) + + let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + self.refresh_engine_from_snapshot(snapshot_lsn)?; + self.ensure_all_tables_loaded_at_snapshot(Some(snapshot_lsn))?; + drop(reader); + Ok(()) } - /// Attempts to materialize *only* the tables referenced by `statement`. - /// - /// Returns `Ok(true)` when statement analysis was conservatively - /// exhaustive and the targeted load succeeded — the caller can then - /// safely skip `ensure_all_tables_loaded()`. Returns `Ok(false)` when - /// the statement contains shapes the analyzer can't fully resolve - /// (CTEs, subqueries, VALUES queries, many DDL shapes, …); the - /// caller must fall back to loading all tables. - /// - /// Per ADR 0143 Phase B + the rubber-duck plan critique on - /// 2026-04-22: only a strict whitelist is treated as targeted-safe. - fn ensure_tables_loaded_for_statement_at_snapshot( + fn refresh_and_load_tables_for_statement_at_latest_snapshot( &self, statement: &SqlStatement, - snapshot_lsn: Option, - ) -> Result { - let names = { - let runtime = self - .inner - .engine - .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - let Some(base_tables) = - self.safe_referenced_base_tables_in_runtime(&runtime, statement) - else { - return Ok(false); - }; - base_tables - }; - let should_load_extension_catalog = self.inner.config.extension_unsigned_development_mode - || !self.inner.config.extension_trust_anchors.is_empty(); - if names.is_empty() && !should_load_extension_catalog { - return Ok(true); + ) -> Result<()> { + if !self.inner.config.defer_table_materialization { + self.refresh_engine_from_storage()?; + self.ensure_all_tables_loaded()?; + return Ok(()); } - let mut names_refs: Vec<&str> = names.iter().map(|s: &String| s.as_str()).collect(); - if should_load_extension_catalog { - names_refs.extend(crate::extensions::extension_catalog_table_names()); + + let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + self.refresh_engine_from_snapshot(snapshot_lsn)?; + let targeted_ok = + self.ensure_tables_loaded_for_statement_at_snapshot(statement, Some(snapshot_lsn))?; + if !targeted_ok { + self.ensure_all_tables_loaded_at_snapshot(Some(snapshot_lsn))?; } - self.ensure_tables_loaded_at_snapshot(&names_refs, snapshot_lsn)?; - Ok(true) + drop(reader); + Ok(()) } - /// Materializes all tables that were deferred during `Db::open`. + /// Materializes deferred tables specified by name. /// - /// Fast path (no deferred tables): one read-lock check on the engine. - /// Slow path: drops the read lock, takes a write lock, loads all deferred - /// tables and rebuilds indexes, then releases. - fn ensure_all_tables_loaded(&self) -> Result<()> { - self.ensure_all_tables_loaded_at_snapshot(None) - } - - fn ensure_all_tables_loaded_at_snapshot(&self, snapshot_lsn: Option) -> Result<()> { + /// Fast path (no matching deferred tables): one read-lock check. + /// Slow path: drops the read lock, takes a write lock, loads only the + /// specified tables and rebuilds their indexes, then releases. + /// + /// This enables per-table on-demand loading for ADR 0143 Phase B. + fn ensure_tables_loaded_at_snapshot( + &self, + names: &[&str], + snapshot_lsn: Option, + ) -> Result<()> { { let runtime = self .inner .engine .read() .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; - if !runtime.has_deferred_tables() { + let has_deferred = runtime.has_deferred_tables(); + let has_match = names.iter().any(|name| { + runtime + .deferred_table_names() + .any(|dt| dt.eq_ignore_ascii_case(name)) + }); + if !has_deferred || !has_match { return Ok(()); } } + let filter: BTreeSet = names.iter().map(|s| s.to_string()).collect(); let mut runtime = self .inner .engine @@ -11539,3399 +7233,2017 @@ impl Db { .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; if let Some(snapshot_lsn) = snapshot_lsn { if self.inner.config.paged_row_storage { - runtime.load_deferred_table_row_sources_at_snapshot( + runtime.load_deferred_table_row_sources_filtered_at_snapshot( &self.inner.pager, &self.inner.wal, self.inner.config.page_size, + &filter, snapshot_lsn, ) } else { - runtime.load_deferred_tables_at_snapshot( + runtime.load_deferred_tables_filtered_at_snapshot( &self.inner.pager, &self.inner.wal, self.inner.config.page_size, + &filter, snapshot_lsn, ) } } else if self.inner.config.paged_row_storage { - runtime.load_deferred_table_row_sources( + runtime.load_deferred_table_row_sources_filtered( &self.inner.pager, &self.inner.wal, self.inner.config.page_size, + &filter, ) } else { - runtime.load_deferred_tables( + runtime.load_deferred_tables_filtered( &self.inner.pager, &self.inner.wal, self.inner.config.page_size, - ) - } - } - - fn current_schema_cookie(&self) -> Result { - let page = self.read_page(page::HEADER_PAGE_ID)?; - let mut bytes = [0_u8; storage::header::DB_HEADER_SIZE]; - bytes.copy_from_slice(&page[..storage::header::DB_HEADER_SIZE]); - Ok(DatabaseHeader::decode(&bytes)?.schema_cookie) - } - - fn current_schema_cookie_at_snapshot(&self, snapshot_lsn: u64) -> Result { - Self::schema_cookie_at_storage_snapshot(&self.inner.pager, &self.inner.wal, snapshot_lsn) - } - - fn schema_cookie_at_storage_snapshot( - pager: &PagerHandle, - wal: &WalHandle, - snapshot_lsn: u64, - ) -> Result { - let mut bytes = [0_u8; storage::header::DB_HEADER_SIZE]; - if let Some(wal_page) = - wal.read_page_at_snapshot(pager, page::HEADER_PAGE_ID, snapshot_lsn)? - { - bytes.copy_from_slice(&wal_page[..storage::header::DB_HEADER_SIZE]); - } else { - let page = pager.read_page(page::HEADER_PAGE_ID)?; - bytes.copy_from_slice(&page[..storage::header::DB_HEADER_SIZE]); + &filter, + ) } - Ok(DatabaseHeader::decode(&bytes)?.schema_cookie) } - fn validate_prepared_schema_cookie( + fn security_catalog_table_names() -> [&'static str; 2] { + [ + crate::security::POLICIES_TABLE, + crate::security::MASKS_TABLE, + ] + } + + fn runtime_has_deferred_security_tables(runtime: &EngineRuntime) -> bool { + runtime.has_deferred_tables() + && Self::security_catalog_table_names().iter().any(|name| { + runtime + .deferred_table_names() + .any(|candidate| candidate.eq_ignore_ascii_case(name)) + }) + } + + fn runtime_read_for_fast_read_at_snapshot( &self, - prepared: &PreparedStatement, - schema_cookie: u32, - temp_schema_cookie: u32, - ) -> Result<()> { - if schema_cookie == prepared.schema_cookie - && temp_schema_cookie == prepared.temp_schema_cookie - { - return Ok(()); + snapshot_lsn: u64, + ) -> Result>> { + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + if Self::runtime_has_deferred_security_tables(&runtime) { + drop(runtime); + self.ensure_tables_loaded_at_snapshot( + &Self::security_catalog_table_names(), + Some(snapshot_lsn), + )?; + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + if runtime.security_rules_active()? { + return Ok(None); + } + return Ok(Some(runtime)); } - Err(DbError::sql( - "prepared statement is no longer valid because the schema changed", - )) + if runtime.security_rules_active()? { + return Ok(None); + } + Ok(Some(runtime)) } - fn validate_prepared_against_connection_state( + fn runtime_read_for_observed_current_resident_fast_read( &self, - prepared: &PreparedStatement, - ) -> Result<()> { - let temp_schema_cookie = self + snapshot_lsn: u64, + ) -> Result>> { + if !self.observed_current_runtime_is_current(snapshot_lsn) { + return Ok(None); + } + let runtime = self .inner - .temp_state - .lock() - .map_err(|_| DbError::internal("temp schema lock poisoned"))? - .schema_cookie; - self.validate_prepared_schema_cookie( - prepared, - self.inner.catalog.schema_cookie()?, - temp_schema_cookie, - ) + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + if Self::runtime_has_deferred_security_tables(&runtime) + || runtime.security_rules_active()? + { + return Ok(None); + } + if !self.observed_current_runtime_is_current(snapshot_lsn) { + return Ok(None); + } + Ok(Some(runtime)) } - fn build_sql_txn_state(&self) -> Result { - let (snapshot_reader, current_lsn, current_epoch) = self.begin_sql_snapshot()?; - - let mut runtime = self + fn ensure_security_tables_loaded_at_snapshot(&self, snapshot_lsn: u64) -> Result { + self.ensure_tables_loaded_at_snapshot( + &Self::security_catalog_table_names(), + Some(snapshot_lsn), + )?; + let runtime = self .inner .engine .read() - .map_err(|_| DbError::internal("engine runtime lock poisoned"))? - .clone(); - self.apply_temp_state_to_runtime(&mut runtime)?; - self.configure_runtime_sync_capture(&mut runtime)?; - Ok(SqlTxnState { - runtime, - snapshot_reader, - base_lsn: current_lsn, - base_checkpoint_epoch: current_epoch, - persistent_changed: false, - indexes_maybe_stale: false, - prepared_insert_runtime_cache: HashMap::new(), - savepoints: Vec::new(), - }) + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + runtime.security_rules_active() } - fn execute_prepared_in_state( + fn load_security_tables_for_runtime_at_snapshot( &self, - prepared: &PreparedStatement, - params: &[Value], - state: &mut SqlTxnState, - ) -> Result { - if !Arc::ptr_eq(&self.inner, &prepared.db.inner) { - return Err(DbError::transaction( - "prepared statement belongs to a different database handle", - )); - } - self.validate_prepared_schema_cookie( - prepared, - state.runtime.catalog.schema_cookie, - state.runtime.temp_schema_cookie, - )?; - if prepared.read_only { - if let Some(result) = self.try_execute_prepared_inspection_query(prepared, params)? { - return Ok(result); - } - let snapshot_lsn = state.snapshot_lsn(); - return self.execute_read_in_runtime_state( - prepared.statement.as_ref(), - params, - &mut state.runtime, - snapshot_lsn, - &mut state.indexes_maybe_stale, - ); - } - let snapshot_lsn = state.snapshot_lsn(); - if let Some(result) = self.try_execute_prepared_insert_in_runtime_state( - prepared, - params, - &mut state.runtime, - snapshot_lsn, - &mut state.persistent_changed, - &mut state.indexes_maybe_stale, - &mut state.prepared_insert_runtime_cache, - )? { - return Ok(result); - } - if let Some(prepared_update) = prepared.prepared_update.as_deref() { - if let Some(result) = self.try_execute_prepared_update_in_runtime_state( - prepared, - prepared_update, - params, - &mut state.runtime, - snapshot_lsn, - &mut state.persistent_changed, - &mut state.indexes_maybe_stale, - )? { - return Ok(result); - } - } - if let Some(prepared_delete) = prepared.prepared_delete.as_deref() { - if let Some(result) = self.try_execute_prepared_delete_in_runtime_state( - prepared, - prepared_delete, - params, - &mut state.runtime, - snapshot_lsn, - &mut state.persistent_changed, - &mut state.indexes_maybe_stale, - )? { - return Ok(result); - } - } - self.execute_write_in_runtime_state( - prepared.statement.as_ref(), - params, - &mut state.runtime, + runtime: &mut EngineRuntime, + snapshot_lsn: u64, + ) -> Result { + self.load_runtime_table_row_sources_at_snapshot( + runtime, + &Self::security_catalog_table_names(), snapshot_lsn, - &mut state.persistent_changed, - &mut state.indexes_maybe_stale, - ) + )?; + runtime.security_rules_active() } - fn execute_prepared_in_exclusive_state( + fn ensure_table_row_sources_loaded_at_snapshot( &self, - prepared: &PreparedStatement, - params: &[Value], - state: &mut ExclusiveSqlTxnState<'_>, - ) -> Result { - if !Arc::ptr_eq(&self.inner, &prepared.db.inner) { - return Err(DbError::transaction( - "prepared statement belongs to a different database handle", - )); - } - Self::flush_exclusive_prepared_insert_next_row_id(state)?; - self.validate_prepared_schema_cookie( - prepared, - state.runtime.catalog.schema_cookie, - state.runtime.temp_schema_cookie, - )?; - if prepared.read_only { - if let Some(result) = self.try_execute_prepared_inspection_query(prepared, params)? { - return Ok(result); + names: &[&str], + snapshot_lsn: u64, + ) -> Result<()> { + let filter: BTreeSet = names.iter().map(|s| s.to_string()).collect(); + { + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let has_deferred = runtime.has_deferred_tables(); + let has_match = names.iter().any(|name| { + runtime + .deferred_table_names() + .any(|dt| dt.eq_ignore_ascii_case(name)) + }); + if !has_deferred || !has_match { + return Ok(()); } - let snapshot_lsn = state.snapshot_lsn(); - return self.execute_read_in_runtime_state( - prepared.statement.as_ref(), - params, - &mut state.runtime, - snapshot_lsn, - &mut state.indexes_maybe_stale, - ); } - let snapshot_lsn = state.snapshot_lsn(); - if let Some(result) = self.try_execute_prepared_insert_in_runtime_state( - prepared, - params, - &mut state.runtime, + let mut runtime = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + runtime.load_deferred_table_row_sources_filtered_at_snapshot( + &self.inner.pager, + &self.inner.wal, + self.inner.config.page_size, + &filter, snapshot_lsn, - &mut state.persistent_changed, - &mut state.indexes_maybe_stale, - &mut state.prepared_insert_runtime_cache, - )? { - return Ok(result); + ) + } + + fn hydrate_deferred_runtime_index_at_snapshot( + &self, + table_name: &str, + index_name: &str, + snapshot_lsn: u64, + ) -> Result<()> { + { + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let has_deferred = runtime.has_deferred_tables(); + let has_match = runtime + .deferred_table_names() + .any(|deferred| identifiers_equal(deferred, table_name)); + if !has_deferred || !has_match { + return Ok(()); + } } - self.execute_write_in_runtime_state( - prepared.statement.as_ref(), - params, - &mut state.runtime, + let mut runtime = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + runtime.hydrate_deferred_runtime_index_at_snapshot( + &self.inner.pager, + &self.inner.wal, + self.inner.config.page_size, + table_name, + index_name, snapshot_lsn, - &mut state.persistent_changed, - &mut state.indexes_maybe_stale, ) } - fn execute_prepared_in_exclusive_state_mut( + fn try_load_prepared_read_row_sources_at_snapshot( &self, - prepared: &PreparedStatement, - params: &mut [Value], - state: &mut ExclusiveSqlTxnState<'_>, - ) -> Result { - if !prepared.read_only { - if let Some(result) = self - .try_execute_last_prepared_insert_in_exclusive_state_mut(prepared, params, state)? - { - return Ok(result); - } + names: &[&str], + snapshot_lsn: u64, + ) -> Result<()> { + if names.is_empty() + || !self.inner.config.defer_table_materialization + || !self.inner.config.paged_row_storage + { + return Ok(()); } - Self::flush_exclusive_prepared_insert_next_row_id(state)?; - if !Arc::ptr_eq(&self.inner, &prepared.db.inner) { - return Err(DbError::transaction( - "prepared statement belongs to a different database handle", - )); + + let row_limit = self.prepared_read_row_source_row_limit(); + if row_limit == 0 { + return Ok(()); } - self.validate_prepared_schema_cookie( - prepared, - state.runtime.catalog.schema_cookie, - state.runtime.temp_schema_cookie, - )?; - if prepared.read_only { - if let Some(result) = self.try_execute_prepared_inspection_query(prepared, params)? { - return Ok(result); + + let mut to_load = BTreeSet::new(); + { + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + for name in names { + let Some(table_name) = runtime.canonical_catalog_table_name(name) else { + continue; + }; + if runtime.table_row_source(&table_name).is_some() { + continue; + } + let Some(state) = runtime.persisted_tables.get(&table_name).copied() else { + continue; + }; + if !runtime.deferred_tables.contains(&table_name) { + continue; + } + if !state.pointer.is_table_paged_manifest() + || state.row_count < PREPARED_READ_ROW_SOURCE_MIN_ROWS + || state.row_count > row_limit + { + return Ok(()); + } + to_load.insert(table_name); } - let snapshot_lsn = state.snapshot_lsn(); - return self.execute_read_in_runtime_state( - prepared.statement.as_ref(), - params, - &mut state.runtime, - snapshot_lsn, - &mut state.indexes_maybe_stale, - ); - } - let snapshot_lsn = state.snapshot_lsn(); - if let Some(result) = self.try_execute_prepared_insert_in_runtime_state_mut( - prepared, - params, - &mut state.runtime, - snapshot_lsn, - &mut state.persistent_changed, - &mut state.indexes_maybe_stale, - &mut state.prepared_insert_runtime_cache, - &mut state.prepared_insert_last_cache_key, - &mut state.prepared_insert_last_plan, - &mut state.prepared_insert_last_next_row_id, - &mut state.prepared_insert_candidate, - )? { - return Ok(result); } - if let Some(prepared_update) = prepared.prepared_update.as_deref() { - if let Some(result) = self.try_execute_prepared_update_in_runtime_state( - prepared, - prepared_update, - params, - &mut state.runtime, - snapshot_lsn, - &mut state.persistent_changed, - &mut state.indexes_maybe_stale, - )? { - return Ok(result); - } + + if to_load.is_empty() { + return Ok(()); } - if let Some(prepared_delete) = prepared.prepared_delete.as_deref() { - if let Some(result) = self.try_execute_prepared_delete_in_runtime_state( - prepared, - prepared_delete, - params, - &mut state.runtime, + + { + let mut runtime = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + runtime.load_deferred_table_row_sources_filtered_at_snapshot( + &self.inner.pager, + &self.inner.wal, + self.inner.config.page_size, + &to_load, snapshot_lsn, - &mut state.persistent_changed, - &mut state.indexes_maybe_stale, - )? { - return Ok(result); - } + )?; } - self.execute_write_in_runtime_state( - prepared.statement.as_ref(), - params, - &mut state.runtime, - snapshot_lsn, - &mut state.persistent_changed, - &mut state.indexes_maybe_stale, - ) + + let loaded_refs = to_load.iter().map(String::as_str).collect::>(); + self.touch_read_only_paged_row_sources_by_name(&loaded_refs) } - fn prepared_insert_current_next_row_id( - runtime: &EngineRuntime, - prepared_insert: &PreparedSimpleInsert, - ) -> Result> { - let Some(table_name) = prepared_insert.catalog_table_name.as_deref() else { - return Ok(None); + fn touch_read_only_paged_row_sources_by_name(&self, names: &[&str]) -> Result<()> { + if names.is_empty() + || !self.inner.config.defer_table_materialization + || !self.inner.config.paged_row_storage + { + return Ok(()); + } + + let (touched_tables, all_paged_tables) = { + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let mut touched_tables = BTreeSet::new(); + let mut all_paged_tables = BTreeSet::new(); + for (name, state) in runtime.persisted_tables.iter() { + if state.pointer.is_table_paged_manifest() { + all_paged_tables.insert(name.clone()); + } + } + for name in names { + let Some(table_name) = runtime.canonical_catalog_table_name(name) else { + continue; + }; + if all_paged_tables.contains(&table_name) + && runtime.table_row_source(&table_name).is_some() + { + touched_tables.insert(table_name); + } + } + (touched_tables, all_paged_tables) }; - runtime - .catalog - .tables - .get(table_name) - .map(|table| Some(table.next_row_id)) - .ok_or_else(|| DbError::sql(format!("unknown table {}", prepared_insert.table_name))) - } - fn flush_exclusive_prepared_insert_next_row_id( - state: &mut ExclusiveSqlTxnState<'_>, - ) -> Result<()> { - let Some(next_row_id) = state.prepared_insert_last_next_row_id.take() else { - return Ok(()); - }; - let Some(prepared_insert) = state.prepared_insert_last_plan.as_ref() else { - state.prepared_insert_last_cache_key = None; - return Ok(()); - }; - let Some(table_name) = prepared_insert.catalog_table_name.as_deref() else { - return Ok(()); - }; - let catalog = Arc::make_mut(&mut state.runtime.catalog); - let table = catalog - .tables - .get_mut(table_name) - .ok_or_else(|| DbError::sql(format!("unknown table {}", prepared_insert.table_name)))?; - table.next_row_id = next_row_id; - Ok(()) + if touched_tables.is_empty() { + return Ok(()); + } + + let mut to_redefer: Vec = Vec::new(); + { + let mut residency = self + .inner + .read_only_paged_row_source_residency + .lock() + .map_err(|_| { + DbError::internal("read-only paged row source residency lock poisoned") + })?; + residency + .table_touch_generation + .retain(|name, _| all_paged_tables.contains(name)); + let touch_gen = residency.next_touch_gen; + residency.next_touch_gen = residency.next_touch_gen.saturating_add(1); + for table_name in touched_tables { + residency + .table_touch_generation + .insert(table_name, touch_gen); + } + if residency.table_touch_generation.len() > AUTOCOMMIT_PAGED_ROW_SOURCE_MAX_RESIDENT { + let mut ordered_touch = residency + .table_touch_generation + .iter() + .map(|(name, generation)| (name, *generation)) + .collect::>(); + ordered_touch.sort_by_key(|(_, generation)| *generation); + let overflow = ordered_touch.len() - AUTOCOMMIT_PAGED_ROW_SOURCE_MAX_RESIDENT; + to_redefer.reserve(overflow); + for (name, _) in ordered_touch.iter().take(overflow) { + to_redefer.push((*name).clone()); + } + for name in &to_redefer { + residency.table_touch_generation.remove(name); + } + } + } + + if to_redefer.is_empty() { + Ok(()) + } else { + let redefer_refs = to_redefer.iter().map(String::as_str).collect::>(); + self.redefer_persisted_tables(&redefer_refs) + } } - #[inline(always)] - fn try_execute_last_prepared_insert_in_exclusive_state_mut( + /// Fast path for non-transactional reads when deferred materialization is + /// enabled but the statement's base tables are already resident at the + /// pinned reader snapshot. + /// + /// Returns a read guard over the resident runtime when the statement can + /// be executed without reloading row sources. + /// Returns `Ok(None)` when any referenced base table is not resident, the + /// runtime LSN is stale, a checkpoint has advanced, or the statement's + /// base-table set cannot be resolved (callers fall back to the deferred + /// load path in that case). + fn try_resident_read_for_statement_at_snapshot( &self, - prepared: &PreparedStatement, - params: &mut [Value], - state: &mut ExclusiveSqlTxnState<'_>, - ) -> Result> { - if state.indexes_maybe_stale { + statement: &SqlStatement, + prepared: Option<&PreparedStatement>, + snapshot_lsn: u64, + ) -> Result>> { + if !self.inner.config.defer_table_materialization { return Ok(None); } - let Some(cache_key) = state.prepared_insert_last_cache_key else { + let checkpoint_epoch = self.inner.wal.checkpoint_epoch(); + let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); + let last_seen_checkpoint_epoch = self + .inner + .last_seen_checkpoint_epoch + .load(Ordering::Acquire); + if last_runtime_lsn != snapshot_lsn || last_seen_checkpoint_epoch != checkpoint_epoch { return Ok(None); - }; - if cache_key != Self::prepared_statement_cache_key(prepared) { + } + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let current_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); + let current_seen_checkpoint_epoch = self + .inner + .last_seen_checkpoint_epoch + .load(Ordering::Acquire); + if current_runtime_lsn != snapshot_lsn || current_seen_checkpoint_epoch != checkpoint_epoch + { return Ok(None); } - let Some(insert_plan) = state.prepared_insert_last_plan.as_ref() else { + self.validate_prepared_against_runtime(prepared, &runtime)?; + if Self::runtime_has_deferred_security_tables(&runtime) + || runtime.security_rules_active()? + { return Ok(None); - }; - let Some(cached_next_row_id) = state.prepared_insert_last_next_row_id.as_mut() else { + } + let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(&runtime, statement) + else { return Ok(None); }; - - let affected = state - .runtime - .execute_prepared_simple_insert_positional_params_in_place_with_cached_next_row_id( - insert_plan.as_ref(), - params, - &mut state.prepared_insert_candidate, - cached_next_row_id, - self.inner.config.page_size, - )?; - if !state.persistent_changed { - state.persistent_changed |= Self::prepared_insert_changes_persistent_table( - &state.runtime, - insert_plan.as_ref(), - ); - } - Ok(Some(QueryResult::with_affected_rows(affected))) - } - - fn prepare_batch_in_exclusive_state<'txn, 'db>( - &'db self, - prepared: &'txn PreparedStatement, - param_count: usize, - state: &'txn mut ExclusiveSqlTxnState<'db>, - ) -> Result> { - if !Arc::ptr_eq(&self.inner, &prepared.db.inner) { - return Err(DbError::transaction( - "prepared statement belongs to a different database handle", - )); + if base_tables.is_empty() { + return Ok(Some(runtime)); } - Self::flush_exclusive_prepared_insert_next_row_id(state)?; - self.validate_prepared_schema_cookie( - prepared, - state.runtime.catalog.schema_cookie, - state.runtime.temp_schema_cookie, - )?; - - let mut prepared_insert = None; - let mut direct_positional = false; - if !prepared.read_only && matches!(prepared.statement.as_ref(), SqlStatement::Insert(_)) { - let snapshot_lsn = state.snapshot_lsn(); - prepared_insert = self.prepared_insert_plan_for_runtime_state( - prepared, - &mut state.runtime, - snapshot_lsn, - &mut state.indexes_maybe_stale, - &mut state.prepared_insert_runtime_cache, - )?; - direct_positional = prepared_insert.as_deref().is_some_and(|insert| { - Self::prepared_insert_uses_direct_positional_params(insert, param_count) - }); + let all_resident = base_tables.iter().all(|name| { + runtime + .canonical_catalog_table_name(name) + .is_some_and(|table_name| runtime.table_row_source(&table_name).is_some()) + }); + if all_resident { + Ok(Some(runtime)) + } else { + Ok(None) } - Ok(PreparedStatementBatch { - db: self, - state, - prepared, - prepared_insert, - direct_positional, - prepared_insert_candidate: Vec::new(), - prepared_insert_encoded_values: Vec::new(), - }) } - fn execute_statement_in_state( - &self, - _sql: &str, - statement: &crate::sql::ast::Statement, - params: &[Value], - state: &mut SqlTxnState, - ) -> Result { - let snapshot_lsn = state.snapshot_lsn(); - self.execute_write_in_runtime_state( - statement, - params, - &mut state.runtime, - snapshot_lsn, - &mut state.persistent_changed, - &mut state.indexes_maybe_stale, - ) - } - - #[allow(clippy::too_many_arguments)] - fn try_execute_prepared_insert_in_runtime_state( + fn try_resident_read_for_single_process_statement( &self, - prepared: &PreparedStatement, - params: &[Value], - runtime: &mut EngineRuntime, - snapshot_lsn: u64, - persistent_changed: &mut bool, - indexes_maybe_stale: &mut bool, - prepared_insert_runtime_cache: &mut HashMap>, - ) -> Result> { - let Some(insert_plan) = self.prepared_insert_plan_for_runtime_state( - prepared, - runtime, - snapshot_lsn, - indexes_maybe_stale, - prepared_insert_runtime_cache, - )? + statement: &SqlStatement, + prepared: Option<&PreparedStatement>, + ) -> Result>> { + if !self.inner.config.defer_table_materialization { + return Ok(None); + } + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + self.validate_prepared_against_runtime(prepared, &runtime)?; + if Self::runtime_has_deferred_security_tables(&runtime) + || runtime.security_rules_active()? + { + return Ok(None); + } + let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(&runtime, statement) else { return Ok(None); }; - - let result = runtime.execute_prepared_simple_insert( - insert_plan.as_ref(), - params, - self.inner.config.page_size, - )?; - *persistent_changed |= - Self::prepared_insert_changes_persistent_table(runtime, insert_plan.as_ref()); - Ok(Some(result)) + if base_tables.is_empty() { + return Ok(Some(runtime)); + } + let all_resident = base_tables.iter().all(|name| { + runtime + .canonical_catalog_table_name(name) + .is_some_and(|table_name| runtime.table_row_source(&table_name).is_some()) + }); + if all_resident { + Ok(Some(runtime)) + } else { + Ok(None) + } } - #[allow(clippy::too_many_arguments)] - fn try_execute_prepared_insert_in_runtime_state_mut( + fn runtime_read_for_prepared_row_sources_at_snapshot( &self, - prepared: &PreparedStatement, - params: &mut [Value], - runtime: &mut EngineRuntime, + names: &[&str], snapshot_lsn: u64, - persistent_changed: &mut bool, - indexes_maybe_stale: &mut bool, - prepared_insert_runtime_cache: &mut HashMap>, - prepared_insert_last_cache_key: &mut Option, - prepared_insert_last_plan: &mut Option>, - prepared_insert_last_next_row_id: &mut Option, - prepared_insert_candidate: &mut Vec, - ) -> Result> { - let Some(insert_plan) = self.prepared_insert_plan_for_runtime_state( - prepared, - runtime, - snapshot_lsn, - indexes_maybe_stale, - prepared_insert_runtime_cache, - )? - else { + ) -> Result>> { + if names.is_empty() { return Ok(None); - }; - - if !Self::prepared_insert_uses_direct_positional_params(insert_plan.as_ref(), params.len()) + } + let latest_checkpoint_epoch = self.inner.wal.checkpoint_epoch(); + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); + let last_seen_checkpoint_epoch = self + .inner + .last_seen_checkpoint_epoch + .load(Ordering::Acquire); + if last_runtime_lsn != snapshot_lsn + || last_seen_checkpoint_epoch != latest_checkpoint_epoch + || names.iter().any(|name| { + runtime + .canonical_catalog_table_name(name) + .is_none_or(|table_name| runtime.table_row_source(&table_name).is_none()) + }) { return Ok(None); } - - let cache_key = Self::prepared_statement_cache_key(prepared); - *prepared_insert_last_cache_key = Some(cache_key); - *prepared_insert_last_plan = Some(Arc::clone(&insert_plan)); - let result = runtime - .execute_prepared_simple_insert_positional_params_in_place_with_candidate( - insert_plan.as_ref(), - params, - prepared_insert_candidate, - self.inner.config.page_size, - )?; - *prepared_insert_last_next_row_id = - Self::prepared_insert_current_next_row_id(runtime, insert_plan.as_ref())?; - if !*persistent_changed { - *persistent_changed |= - Self::prepared_insert_changes_persistent_table(runtime, insert_plan.as_ref()); - } - Ok(Some(QueryResult::with_affected_rows(result))) + Ok(Some(runtime)) } - #[allow(clippy::too_many_arguments)] - fn try_execute_prepared_update_in_runtime_state( + fn try_resident_read_for_prepared_table_statement( &self, - prepared_statement: &PreparedStatement, - prepared_update: &PreparedSimpleUpdate, - params: &[Value], - runtime: &mut EngineRuntime, - snapshot_lsn: u64, - persistent_changed: &mut bool, - indexes_maybe_stale: &mut bool, - ) -> Result> { - self.load_runtime_table_row_sources_at_snapshot( - runtime, - &[prepared_update.table_name.as_str()], - snapshot_lsn, - )?; - self.validate_prepared_schema_cookie( - prepared_statement, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - if !runtime.can_reuse_prepared_simple_update(prepared_update) { + prepared: &PreparedStatement, + table_name: &str, + ) -> Result>> { + if !self.inner.config.defer_table_materialization { return Ok(None); } - let temp_only = self.statement_is_temp_only(runtime, prepared_statement.statement.as_ref()); - let result = runtime.execute_prepared_simple_update( - prepared_update, - params, - self.inner.config.page_size, - )?; - *persistent_changed |= !temp_only; - *indexes_maybe_stale |= Self::runtime_has_stale_indexes(runtime); - Ok(Some(result)) - } - - #[allow(clippy::too_many_arguments)] - fn try_execute_prepared_delete_in_runtime_state( - &self, - prepared_statement: &PreparedStatement, - prepared_delete: &PreparedSimpleDelete, - params: &[Value], - runtime: &mut EngineRuntime, - snapshot_lsn: u64, - persistent_changed: &mut bool, - indexes_maybe_stale: &mut bool, - ) -> Result> { - let row_source_table_names = prepared_delete.required_row_source_table_names(); - let child_index_targets = prepared_delete.child_index_hydration_targets(); - self.load_runtime_table_row_sources_and_child_indexes_at_snapshot( - runtime, - &row_source_table_names, - &child_index_targets, - snapshot_lsn, - )?; - self.validate_prepared_schema_cookie( - prepared_statement, - runtime.catalog.schema_cookie, - runtime.temp_schema_cookie, - )?; - if !runtime.can_reuse_prepared_simple_delete(prepared_delete) { + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + self.validate_prepared_against_runtime(Some(prepared), &runtime)?; + if Self::runtime_has_deferred_security_tables(&runtime) + || runtime.security_rules_active()? + { return Ok(None); } - let temp_only = self.statement_is_temp_only(runtime, prepared_statement.statement.as_ref()); - let result = runtime.execute_prepared_simple_delete( - prepared_delete, - params, - self.inner.config.page_size, - )?; - *persistent_changed |= !temp_only; - *indexes_maybe_stale |= Self::runtime_has_stale_indexes(runtime); - Ok(Some(result)) + if runtime + .canonical_catalog_table_name(table_name) + .is_some_and(|canonical| runtime.table_row_source(&canonical).is_some()) + { + Ok(Some(runtime)) + } else { + Ok(None) + } } - fn exclusive_sql_txn_error(&self) -> DbError { - DbError::transaction( - "a SQL transaction handle is active on this database handle; use it until commit or rollback", - ) - } + fn load_simple_write_row_sources_at_latest_snapshot(&self, names: &[&str]) -> Result<()> { + if !self.inner.config.defer_table_materialization { + self.refresh_engine_from_storage()?; + self.ensure_tables_loaded_at_snapshot(names, None)?; + return Ok(()); + } - pub fn sync_init_replica(&self, replica_id: &str) -> Result<()> { - self.ensure_sync_tables()?; - self.sync_upsert_metadata("replica_id", replica_id)?; - self.sync_upsert_metadata("enabled", "true")?; - self.sync_upsert_metadata("next_sequence", "1")?; - self.inner.sync_ctx.set_replica_id(replica_id); - self.inner.sync_ctx.set_enabled(true); - self.inner.sync_ctx.set_next_sequence(1); - self.inner.sync_ctx.ensure_journal_open(&self.inner.vfs)?; + if self.simple_write_row_sources_loaded_for_current_runtime(names)? { + return Ok(()); + } + + let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + self.refresh_engine_from_snapshot(snapshot_lsn)?; + self.ensure_table_row_sources_loaded_at_snapshot(names, snapshot_lsn)?; + drop(reader); Ok(()) } - pub fn sync_create_scope( + fn load_simple_write_row_sources_and_child_indexes_at_latest_snapshot( &self, - name: &str, - include_tables: &[&str], - row_filter: Option<&str>, + names: &[&str], + child_index_targets: &[(&str, &str)], ) -> Result<()> { - self.ensure_sync_tables()?; - let runtime = self.runtime_for_metadata_inspection()?; - let validation = - validate_sync_scope_definition(&runtime, name, include_tables, row_filter)?; - let created_at_micros = self - .sync_scope(name)? - .map(|scope| scope.created_at_micros) - .unwrap_or_else(current_time_micros); - let updated_at_micros = current_time_micros(); - let sql = format!( - "INSERT INTO {table} (name, include_tables_json, row_filter, filter_columns_json, created_at_micros, updated_at_micros) VALUES ({name}, {include_tables_json}, {row_filter}, {filter_columns_json}, {created_at_micros}, {updated_at_micros}) ON CONFLICT (name) DO UPDATE SET include_tables_json = {include_tables_json}, row_filter = {row_filter}, filter_columns_json = {filter_columns_json}, updated_at_micros = {updated_at_micros}", - table = crate::sync::SCOPES_TABLE, - name = sql_text_literal(&validation.name), - include_tables_json = sql_text_literal( - &serde_json::to_string(&validation.include_tables) - .map_err(|error| DbError::internal(format!("failed to encode scope tables: {error}")))?, - ), - row_filter = sql_nullable_text_literal(validation.row_filter.as_deref()), - filter_columns_json = sql_text_literal( - &serde_json::to_string(&validation.filter_columns) - .map_err(|error| DbError::internal(format!("failed to encode scope columns: {error}")))?, - ), - created_at_micros = created_at_micros, - updated_at_micros = updated_at_micros, - ); - let _ = self.execute(&sql)?; + if !self.inner.config.defer_table_materialization { + self.refresh_engine_from_storage()?; + self.ensure_tables_loaded_at_snapshot(names, None)?; + return Ok(()); + } + + let indexes_loaded = { + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + child_index_targets + .iter() + .all(|(_, index_name)| runtime.index(index_name).is_some()) + }; + if self.simple_write_row_sources_loaded_for_current_runtime(names)? && indexes_loaded { + return Ok(()); + } + + let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + self.refresh_engine_from_snapshot(snapshot_lsn)?; + for (table_name, index_name) in child_index_targets { + self.hydrate_deferred_runtime_index_at_snapshot(table_name, index_name, snapshot_lsn)?; + } + self.ensure_table_row_sources_loaded_at_snapshot(names, snapshot_lsn)?; + drop(reader); Ok(()) } - pub fn sync_drop_scope(&self, name: &str) -> Result { - self.ensure_sync_tables()?; - let scope_name = name.trim(); - if scope_name.is_empty() { - return Err(DbError::sql("sync scope name must not be empty")); - } - if self - .sync_peer_scope_bindings()? - .iter() - .any(|binding| binding.scope_name.eq_ignore_ascii_case(scope_name)) - { - return Err(DbError::sql(format!( - "cannot drop sync scope '{scope_name}' while peer bindings exist" - ))); + fn simple_write_row_sources_loaded_for_current_runtime(&self, names: &[&str]) -> Result { + let latest_lsn = self.inner.wal.latest_snapshot(); + let latest_checkpoint_epoch = self.inner.wal.checkpoint_epoch(); + let last_runtime_lsn = self.inner.last_runtime_lsn.load(Ordering::Acquire); + let last_seen_checkpoint_epoch = self + .inner + .last_seen_checkpoint_epoch + .load(Ordering::Acquire); + + if latest_lsn > last_runtime_lsn || latest_checkpoint_epoch != last_seen_checkpoint_epoch { + return Ok(false); } - let sql = format!( - "DELETE FROM {} WHERE name = {}", - crate::sync::SCOPES_TABLE, - sql_text_literal(scope_name) - ); - let result = self.execute(&sql)?; - Ok(result.affected_rows() > 0) + + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let has_deferred_match = names.iter().any(|name| { + runtime + .deferred_table_names() + .any(|deferred| identifiers_equal(deferred, name)) + }); + Ok(!has_deferred_match) } - pub fn sync_scope(&self, name: &str) -> Result> { - self.ensure_sync_tables()?; - let scope_name = name.trim(); - if scope_name.is_empty() { - return Err(DbError::sql("sync scope name must not be empty")); + fn load_statement_row_sources_at_latest_snapshot( + &self, + statement: &SqlStatement, + ) -> Result { + if !self.inner.config.defer_table_materialization { + return self.ensure_tables_loaded_for_statement_at_snapshot(statement, None); } - let sql = format!( - "SELECT name, include_tables_json, row_filter, filter_columns_json, created_at_micros, updated_at_micros FROM {} WHERE name = {}", - crate::sync::SCOPES_TABLE, - sql_text_literal(scope_name) - ); - match self.execute(&sql) { - Ok(result) => Ok(result.rows().first().map(sync_scope_from_row).transpose()?), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(None) - } else { - Err(error) - } - } + + let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + self.refresh_engine_from_snapshot(snapshot_lsn)?; + let mut runtime = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(&runtime, statement) + else { + drop(reader); + return Ok(false); + }; + if base_tables.is_empty() { + drop(reader); + return Ok(true); } + let base_refs: Vec<&str> = base_tables.iter().map(String::as_str).collect(); + self.load_runtime_table_row_sources_at_snapshot(&mut runtime, &base_refs, snapshot_lsn)?; + drop(reader); + Ok(true) } - fn try_execute_prepared_inspection_query( + fn can_execute_statement_with_row_sources_at_latest_snapshot( &self, - prepared: &PreparedStatement, - params: &[Value], - ) -> Result> { - if let Some(result) = - self.try_execute_sync_inspection_query(&prepared.prepared_sql, params)? - { - return Ok(Some(result)); - } - if let Some(result) = crate::extensions::try_execute_extension_inspection_query( - self, - &prepared.prepared_sql, - params, - )? { - return Ok(Some(result)); + statement: &SqlStatement, + ) -> Result { + if !self.inner.config.defer_table_materialization { + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + return Ok(runtime.can_execute_statement_in_state_without_clone(statement)); } - Ok(None) + + let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + self.refresh_engine_from_snapshot(snapshot_lsn)?; + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(&runtime, statement) + else { + drop(reader); + return Ok(false); + }; + let mut working = runtime.clone(); + drop(runtime); + let base_refs: Vec<&str> = base_tables.iter().map(String::as_str).collect(); + self.load_runtime_table_row_sources_at_snapshot(&mut working, &base_refs, snapshot_lsn)?; + drop(reader); + Ok(working.can_execute_statement_in_state_without_clone(statement)) } - pub fn sync_scopes(&self) -> Result> { - self.ensure_sync_tables()?; - let sql = format!( - "SELECT name, include_tables_json, row_filter, filter_columns_json, created_at_micros, updated_at_micros FROM {} ORDER BY name", - crate::sync::SCOPES_TABLE - ); - match self.execute(&sql) { - Ok(result) => result.rows().iter().map(sync_scope_from_row).collect(), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(Vec::new()) - } else { - Err(error) - } - } - } + fn redefer_persisted_tables(&self, names: &[&str]) -> Result<()> { + self.redefer_persisted_tables_inner(names, true) } - pub fn sync_bind_peer_scope(&self, peer_name: &str, scope_name: &str) -> Result<()> { - self.ensure_sync_tables()?; - let peer_name = peer_name.trim(); - if peer_name.is_empty() { - return Err(DbError::sql("sync peer name must not be empty")); + fn redefer_persisted_tables_inner( + &self, + names: &[&str], + release_heap_after_drop: bool, + ) -> Result<()> { + if !self.inner.config.defer_table_materialization || names.is_empty() { + return Ok(()); } - if self.sync_peer(peer_name)?.is_none() { - return Err(DbError::sql(format!("sync peer '{peer_name}' not found"))); + let mut runtime = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let freed_bytes = runtime.redefer_persisted_tables(names); + drop(runtime); + if release_heap_after_drop { + self.release_freed_heap_after_paged_row_source_drop(freed_bytes); } - let scope = self - .sync_scope(scope_name)? - .ok_or_else(|| DbError::sql(format!("sync scope '{scope_name}' not found")))?; - let existing = self.sync_peer_scope_binding_row(peer_name)?; - let created_at_micros = existing - .as_ref() - .map(|binding| binding.created_at_micros) - .unwrap_or_else(current_time_micros); - let updated_at_micros = current_time_micros(); - let sql = format!( - "INSERT INTO {table} (peer_name, scope_name, created_at_micros, updated_at_micros) VALUES ({peer_name}, {scope_name}, {created_at_micros}, {updated_at_micros}) ON CONFLICT (peer_name) DO UPDATE SET scope_name = {scope_name}, updated_at_micros = {updated_at_micros}", - table = crate::sync::PEER_SCOPES_TABLE, - peer_name = sql_text_literal(peer_name), - scope_name = sql_text_literal(&scope.name), - created_at_micros = created_at_micros, - updated_at_micros = updated_at_micros, - ); - let _ = self.execute(&sql)?; Ok(()) } - pub fn sync_unbind_peer_scope(&self, peer_name: &str) -> Result { - self.ensure_sync_tables()?; - let peer_name = peer_name.trim(); - if peer_name.is_empty() { - return Err(DbError::sql("sync peer name must not be empty")); - } - let sql = format!( - "DELETE FROM {} WHERE peer_name = {}", - crate::sync::PEER_SCOPES_TABLE, - sql_text_literal(peer_name) - ); - let result = self.execute(&sql)?; - Ok(result.affected_rows() > 0) + fn should_redefer_paged_row_sources_after_write(&self) -> bool { + self.inner.config.defer_table_materialization + && self.inner.config.paged_row_storage + && !self.inner.config.retain_paged_row_sources_after_commit } - pub fn sync_peer_scope(&self, peer_name: &str) -> Result> { - self.ensure_sync_tables()?; - let peer_name = peer_name.trim(); - if peer_name.is_empty() { - return Err(DbError::sql("sync peer name must not be empty")); - } - self.sync_peer_scope_binding_row(peer_name) + fn runtime_should_redefer_persisted_tables_after_write( + &self, + runtime: &EngineRuntime, + names: &[&str], + ) -> bool { + self.should_redefer_paged_row_sources_after_write() + && runtime.has_redeferable_persisted_tables(names) } - pub fn sync_peer_scope_definition(&self, peer_name: &str) -> Result> { - let binding = match self.sync_peer_scope(peer_name)? { - Some(binding) => binding, - None => return Ok(None), - }; - match self.sync_scope(&binding.scope_name)? { - Some(scope) => Ok(Some(scope)), - None => Err(DbError::sql(format!( - "sync scope '{}' bound to peer '{}' was not found", - binding.scope_name, binding.peer_name - ))), + fn redefer_persisted_tables_after_write(&self, names: &[&str]) -> Result<()> { + if self.should_redefer_paged_row_sources_after_write() { + self.redefer_persisted_tables_inner(names, false) + } else { + Ok(()) } } - pub fn sync_peer_scope_bindings(&self) -> Result> { - self.ensure_sync_tables()?; - let sql = format!( - "SELECT peer_name, scope_name, created_at_micros, updated_at_micros FROM {} ORDER BY peer_name", - crate::sync::PEER_SCOPES_TABLE - ); - match self.execute(&sql) { - Ok(result) => result - .rows() - .iter() - .map(sync_peer_scope_binding_from_row) - .collect(), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(Vec::new()) - } else { - Err(error) - } - } + fn redefer_all_persisted_paged_tables(&self) -> Result<()> { + if !self.inner.config.defer_table_materialization || !self.inner.config.paged_row_storage { + return Ok(()); } + let mut runtime = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let freed_bytes = runtime.redefer_all_persisted_paged_tables(); + drop(runtime); + self.release_freed_heap_after_paged_row_source_drop(freed_bytes); + Ok(()) } - pub fn sync_export_batch_for_scope( - &self, - scope_name: &str, - since_seq: u64, - limit: usize, - ) -> Result { - let scope = self - .sync_scope(scope_name)? - .ok_or_else(|| DbError::sql(format!("sync scope '{scope_name}' not found")))?; - let records = self.sync_pending_changes(since_seq, limit)?; - let source_replica_id = records.first().map(|record| record.replica_id.clone()); - let source_high_watermark = records.last().map(|record| record.sequence); - let filtered = self.sync_filter_records_for_scope(&scope, records)?; - SyncChangeBatch::scoped_from_records(filtered, source_replica_id, source_high_watermark) - } - - pub fn sync_import_batch_for_scope( - &self, - scope_name: &str, - batch: &SyncChangeBatch, - ) -> Result { - batch.validate()?; - let scope = self - .sync_scope(scope_name)? - .ok_or_else(|| DbError::sql(format!("sync scope '{scope_name}' not found")))?; - self.sync_validate_batch_for_scope(&scope, batch)?; - self.sync_import_batch(batch) - } - - pub fn sync_import_batch_for_scope_with_policy( + fn redefer_read_only_row_sources( &self, - scope_name: &str, - batch: &SyncChangeBatch, - policy: SyncConflictPolicy, - ) -> Result { - batch.validate()?; - let scope = self - .sync_scope(scope_name)? - .ok_or_else(|| DbError::sql(format!("sync scope '{scope_name}' not found")))?; - self.sync_validate_batch_for_scope(&scope, batch)?; - self.sync_import_batch_with_policy(batch, policy) - } - - pub fn sync_add_peer(&self, name: &str, endpoint: &str, token_env: Option<&str>) -> Result<()> { - let name = name.trim(); - if name.is_empty() { - return Err(DbError::sql("sync peer name must not be empty")); - } - if !(endpoint.starts_with("http://") || endpoint.starts_with("https://")) { - return Err(DbError::sql( - "sync peer endpoint must start with http:// or https://", - )); + statement: &SqlStatement, + allow_redefer_all_on_unknown: bool, + ) -> Result<()> { + if !self.inner.config.defer_table_materialization || !self.inner.config.paged_row_storage { + return Ok(()); } - if token_env.is_some_and(|value| value.trim().is_empty()) { - return Err(DbError::sql("sync peer token_env must not be empty")); + let (touched_tables, all_paged_tables) = { + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let base_tables = match self.safe_referenced_base_tables_in_runtime(&runtime, statement) + { + Some(base_tables) => base_tables, + None => { + drop(runtime); + return if allow_redefer_all_on_unknown { + self.redefer_all_persisted_paged_tables() + } else { + Ok(()) + }; + } + }; + if base_tables.is_empty() { + return Ok(()); + } + let mut touched_tables = BTreeSet::new(); + let mut all_paged_tables = BTreeSet::new(); + for (name, state) in runtime.persisted_tables.iter() { + if state.pointer.is_table_paged_manifest() { + all_paged_tables.insert(name.clone()); + } + } + for base_table in base_tables { + if let Some(table_name) = runtime.canonical_catalog_table_name(&base_table) { + if runtime + .persisted_tables + .get(&table_name) + .is_some_and(|state| state.pointer.is_table_paged_manifest()) + { + touched_tables.insert(table_name); + } + } + } + (touched_tables, all_paged_tables) + }; + if touched_tables.is_empty() { + if allow_redefer_all_on_unknown { + self.redefer_all_persisted_paged_tables() + } else { + Ok(()) + } + } else { + let mut to_redefer: Vec = Vec::new(); + { + let mut residency = self + .inner + .read_only_paged_row_source_residency + .lock() + .map_err(|_| { + DbError::internal("read-only paged row source residency lock poisoned") + })?; + residency + .table_touch_generation + .retain(|name, _| all_paged_tables.contains(name)); + let touch_gen = residency.next_touch_gen; + residency.next_touch_gen = residency.next_touch_gen.saturating_add(1); + for table_name in touched_tables { + residency + .table_touch_generation + .insert(table_name, touch_gen); + } + if residency.table_touch_generation.len() > AUTOCOMMIT_PAGED_ROW_SOURCE_MAX_RESIDENT + { + let mut ordered_touch = residency + .table_touch_generation + .iter() + .map(|(name, generation)| (name, *generation)) + .collect::>(); + ordered_touch.sort_by_key(|(_, generation)| *generation); + let overflow = ordered_touch.len() - AUTOCOMMIT_PAGED_ROW_SOURCE_MAX_RESIDENT; + to_redefer.reserve(overflow); + for (name, _) in ordered_touch.iter().take(overflow) { + to_redefer.push((*name).clone()); + } + for name in &to_redefer { + residency.table_touch_generation.remove(name); + } + } + } + if to_redefer.is_empty() { + Ok(()) + } else { + let redefer_refs = to_redefer.iter().map(String::as_str).collect::>(); + self.redefer_persisted_tables(&redefer_refs) + } } - - self.ensure_sync_tables()?; - let now = current_time_micros(); - let token_sql = token_env - .map(sql_text_literal) - .unwrap_or_else(|| "NULL".to_string()); - let sql = format!( - "INSERT INTO {table} (name, endpoint, token_env, created_at_micros, updated_at_micros) VALUES ({name}, {endpoint}, {token_env}, {now}, {now}) ON CONFLICT (name) DO UPDATE SET endpoint = {endpoint}, token_env = {token_env}, updated_at_micros = {now}", - table = crate::sync::PEERS_TABLE, - name = sql_text_literal(name), - endpoint = sql_text_literal(endpoint), - token_env = token_sql, - now = now, - ); - let _ = self.execute(&sql)?; - Ok(()) } - pub fn sync_remove_peer(&self, name: &str) -> Result { - self.ensure_sync_tables()?; - let sql = format!( - "DELETE FROM {} WHERE name = {}", - crate::sync::PEERS_TABLE, - sql_text_literal(name) - ); - let result = self.execute(&sql)?; - Ok(result.affected_rows() > 0) + fn release_freed_heap_after_paged_row_source_drop(&self, freed_bytes: usize) { + if self.inner.config.paged_row_storage + && should_release_freed_paged_row_source_heap(freed_bytes) + { + self.release_freed_heap_if_configured(); + } } - pub fn sync_peers(&self) -> Result> { - self.ensure_sync_tables()?; - let sql = format!( - "SELECT name, endpoint, token_env, created_at_micros, updated_at_micros FROM {} ORDER BY name", - crate::sync::PEERS_TABLE - ); - match self.execute(&sql) { - Ok(result) => result.rows().iter().map(sync_peer_from_row).collect(), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(Vec::new()) - } else { - Err(error) - } - } + fn release_freed_heap_after_runtime_compaction(&self, freed_bytes: usize) { + if freed_bytes >= RESIDENT_COMMIT_HEAP_RELEASE_THRESHOLD { + self.release_freed_heap_if_configured(); } } - pub fn sync_peer(&self, name: &str) -> Result> { - self.ensure_sync_tables()?; - let sql = format!( - "SELECT name, endpoint, token_env, created_at_micros, updated_at_micros FROM {} WHERE name = {}", - crate::sync::PEERS_TABLE, - sql_text_literal(name) - ); - match self.execute(&sql) { - Ok(result) => Ok(result.rows().first().map(sync_peer_from_row).transpose()?), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(None) - } else { - Err(error) - } - } + fn release_freed_heap_if_configured(&self) { + if !self.inner.config.release_freed_memory_after_checkpoint { + return; } + #[cfg(test)] + PAGED_ROW_SOURCE_HEAP_RELEASE_COUNT.with(|count| count.set(count.get().saturating_add(1))); + crate::wal::platform::release_freed_heap(); } - fn sync_peer_scope_binding_row(&self, peer_name: &str) -> Result> { - let sql = format!( - "SELECT peer_name, scope_name, created_at_micros, updated_at_micros FROM {} WHERE peer_name = {}", - crate::sync::PEER_SCOPES_TABLE, - sql_text_literal(peer_name) - ); - match self.execute(&sql) { - Ok(result) => Ok(result - .rows() - .first() - .map(sync_peer_scope_binding_from_row) - .transpose()?), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(None) - } else { - Err(error) - } - } + fn maybe_demote_wal_after_large_explicit_commit(&self) { + let threshold = self.inner.config.wal_checkpoint_threshold_bytes; + if threshold == 0 { + return; + } + if self.inner.wal.latest_snapshot() < threshold { + return; + } + let target_bytes = threshold / 2; + if target_bytes == 0 { + return; + } + if matches!( + self.inner + .wal + .demote_resident_versions_if_reader_free(usize::try_from(target_bytes).unwrap_or(usize::MAX)), + Ok(demoted) if demoted > 0 + ) { + self.release_freed_heap_if_configured(); } } - pub fn sync_sessions(&self) -> Result> { - self.ensure_sync_tables()?; - let sql = format!( - "SELECT session_id, peer_name, direction, remote_replica_id, started_at_micros, ended_at_micros, status, error, pushed_batch_id, pulled_batch_id, pushed_seen, pushed_applied, pushed_skipped, pushed_conflicted, pulled_seen, pulled_applied, pulled_skipped, pulled_conflicted, retry_count FROM {} ORDER BY session_id", - crate::sync::SESSIONS_TABLE - ); - match self.execute(&sql) { - Ok(result) => result.rows().iter().map(sync_session_from_row).collect(), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(Vec::new()) - } else { - Err(error) - } - } + fn redefer_statement_tables(&self, statement: &SqlStatement) -> Result<()> { + if !self.should_redefer_paged_row_sources_after_write() { + return Ok(()); } + let names = { + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let Some(base_tables) = + self.safe_referenced_base_tables_in_runtime(&runtime, statement) + else { + return Ok(()); + }; + base_tables + }; + let name_refs: Vec<&str> = names.iter().map(String::as_str).collect(); + self.redefer_persisted_tables_after_write(&name_refs) } - pub fn sync_start_session( + fn finalize_row_source_autocommit_statement( &self, - peer_name: &str, - direction: SyncRunDirection, - remote_replica_id: Option<&str>, - ) -> Result { - self.ensure_sync_tables()?; - let session_id = self.next_sync_session_id()?; - let started_at_micros = current_time_micros(); - let sql = format!( - "INSERT INTO {table} (session_id, peer_name, direction, remote_replica_id, started_at_micros, ended_at_micros, status, error, pushed_batch_id, pulled_batch_id, pushed_seen, pushed_applied, pushed_skipped, pushed_conflicted, pulled_seen, pulled_applied, pulled_skipped, pulled_conflicted, retry_count) VALUES ({session_id}, {peer_name}, {direction}, {remote_replica_id}, {started_at_micros}, NULL, 'started', NULL, NULL, NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0)", - table = crate::sync::SESSIONS_TABLE, - session_id = session_id, - peer_name = sql_text_literal(peer_name), - direction = sql_text_literal(direction.as_str()), - remote_replica_id = remote_replica_id - .map(sql_text_literal) - .unwrap_or_else(|| "NULL".to_string()), - started_at_micros = started_at_micros, - ); - let _ = self.execute(&sql)?; - Ok(session_id) + statement: &SqlStatement, + result: Result, + ) -> Result { + let redefer_result = if statement_is_read_only(statement) { + self.redefer_read_only_row_sources(statement, false) + } else { + self.redefer_statement_tables(statement) + }; + match (result, redefer_result) { + (Ok(result), Ok(())) => Ok(result), + (Err(error), Ok(())) => Err(error), + (Ok(_), Err(error)) => Err(error), + (Err(error), Err(_)) => Err(error), + } } - pub fn sync_finish_session_success( + fn finalize_row_source_autocommit_statement_with_full_redefer( &self, - session_id: i64, - summary: &SyncRunSummary, - ) -> Result<()> { - self.sync_update_session(session_id, summary, "success", None, current_time_micros()) + statement: &SqlStatement, + result: Result, + ) -> Result { + let redefer_result = if statement_is_read_only(statement) { + self.redefer_read_only_row_sources(statement, true) + } else if self.should_redefer_paged_row_sources_after_write() { + self.redefer_all_persisted_paged_tables() + } else { + Ok(()) + }; + match (result, redefer_result) { + (Ok(result), Ok(())) => Ok(result), + (Err(error), Ok(())) => Err(error), + (Ok(_), Err(error)) => Err(error), + (Err(error), Err(_)) => Err(error), + } } - pub fn sync_finish_session_failed( - &self, - session_id: i64, - summary: &SyncRunSummary, - error: &str, - ) -> Result<()> { - self.sync_update_session( - session_id, - summary, - "failed", - Some(error), - current_time_micros(), - ) + fn begin_sql_snapshot(&self) -> Result<(ReaderGuard, u64, u64)> { + #[cfg(feature = "bench-internals")] + READ_PATH_WAL_READER_BEGIN_COUNT.fetch_add(1, Ordering::Relaxed); + let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + self.refresh_engine_from_snapshot(snapshot_lsn)?; + let checkpoint_epoch = self.inner.wal.checkpoint_epoch(); + Ok((reader, snapshot_lsn, checkpoint_epoch)) } - pub fn sync_integrity_report(&self) -> Result { - let local_replica_id = self.sync_read_metadata("replica_id").ok().flatten(); - crate::sync::inspect_journal_integrity( - self.inner.sync_ctx.journal_path(), - &self.inner.vfs, - local_replica_id.as_deref(), + fn load_runtime_table_row_sources_at_snapshot( + &self, + runtime: &mut EngineRuntime, + names: &[&str], + snapshot_lsn: u64, + ) -> Result<()> { + if names.is_empty() || !runtime.has_deferred_tables() { + return Ok(()); + } + let has_match = names.iter().any(|name| { + runtime + .deferred_table_names() + .any(|deferred| deferred.eq_ignore_ascii_case(name)) + }); + if !has_match { + return Ok(()); + } + let filter: BTreeSet = names.iter().map(|name| (*name).to_string()).collect(); + runtime.load_deferred_table_row_sources_filtered_at_snapshot( + &self.inner.pager, + &self.inner.wal, + self.inner.config.page_size, + &filter, + snapshot_lsn, ) } - pub fn sync_peer_lag_report(&self) -> Result> { - self.ensure_sync_tables()?; - let local_high_watermark = self.sync_integrity_report()?.last_sequence; - let peers = self.sync_peers()?; - let sessions = self.sync_sessions()?; - let mut latest_successful_remote_replica_ids: HashMap> = - HashMap::new(); - for session in sessions.iter().rev() { - if session.status == "success" { - latest_successful_remote_replica_ids - .entry(session.peer_name.clone()) - .or_insert_with(|| session.remote_replica_id.clone()); + fn load_runtime_table_row_sources_and_child_indexes_at_snapshot( + &self, + runtime: &mut EngineRuntime, + names: &[&str], + child_index_targets: &[(&str, &str)], + snapshot_lsn: u64, + ) -> Result<()> { + self.load_runtime_table_row_sources_at_snapshot(runtime, names, snapshot_lsn)?; + for (_, index_name) in child_index_targets { + if runtime.index(index_name).is_none() { + runtime.rebuild_index(index_name, self.inner.config.page_size)?; } } + Ok(()) + } - peers - .into_iter() - .map(|peer| { - let remote_replica_id = latest_successful_remote_replica_ids - .get(&peer.name) - .cloned() - .flatten(); - let in_watermark = match remote_replica_id.as_deref() { - Some(replica_id) => self.sync_peer_watermark(replica_id)?, - None => None, - }; - let out_watermark = self.sync_peer_out_watermark(&peer.name)?; - let in_lag = match (local_high_watermark, in_watermark) { - (Some(local_high), Some(in_watermark)) if local_high >= in_watermark => { - Some(local_high - in_watermark) - } - _ => None, - }; - let out_lag = match (local_high_watermark, out_watermark) { - (Some(local_high), Some(out_watermark)) if local_high >= out_watermark => { - Some(local_high - out_watermark) - } - _ => None, - }; - Ok(SyncPeerLag { - peer_name: peer.name, - remote_replica_id, - in_watermark, - out_watermark, - local_high_watermark, - in_lag, - out_lag, - }) - }) - .collect() + fn load_all_runtime_row_sources_at_snapshot( + &self, + runtime: &mut EngineRuntime, + snapshot_lsn: u64, + ) -> Result<()> { + let table_names = runtime.deferred_table_names().cloned().collect::>(); + let table_refs = table_names.iter().map(String::as_str).collect::>(); + self.load_runtime_table_row_sources_at_snapshot(runtime, &table_refs, snapshot_lsn) } - pub fn sync_retention_report(&self) -> Result { - let integrity = self.sync_integrity_report()?; - let peer_lag = self.sync_peer_lag_report()?; - let journal_size_bytes = self.sync_status()?.journal_size_bytes; - let mut watermark_entries = peer_lag - .iter() - .flat_map(|peer| { - let inbound = peer.remote_replica_id.as_ref().zip(peer.in_watermark).map( - |(remote_replica_id, watermark)| { - (format!("remote:{remote_replica_id}"), watermark) - }, - ); - let outbound = peer - .out_watermark - .map(|watermark| (peer.peer_name.clone(), watermark)); - inbound.into_iter().chain(outbound) - }) - .collect::>(); - watermark_entries.extend(self.sync_peer_watermark_entries()?); - watermark_entries.extend( - self.sync_shape_clients()? - .into_iter() - .filter(|client| client.retention_blocking) - .map(|client| { - ( - format!( - "shape:{}:client:{}", - client.shape_id, client.client_replica_id - ), - client.last_ack_watermark, - ) - }), - ); - watermark_entries.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1))); - watermark_entries.dedup(); - let lowest_watermark = watermark_entries - .iter() - .map(|(_, watermark)| *watermark) - .min(); - let safe_prune_through = if integrity.total_records == 0 { - None - } else { - lowest_watermark.and_then(|watermark| watermark.checked_sub(1)) - }; - let blocked_by = if integrity.total_records == 0 { - Vec::new() - } else if let Some(lowest_watermark) = lowest_watermark { - if integrity - .last_sequence - .is_some_and(|local_high| lowest_watermark <= local_high) - { - watermark_entries - .iter() - .filter(|(_, watermark)| *watermark == lowest_watermark) - .map(|(label, _)| label.clone()) - .collect::>() - } else { - Vec::new() - } - } else { - Vec::new() + fn try_execute_query_with_row_sources_at_snapshot( + &self, + runtime: &EngineRuntime, + statement: &SqlStatement, + params: &[Value], + snapshot_lsn: u64, + rebuild_stale_indexes: bool, + ) -> Result> { + let SqlStatement::Query(_) = statement else { + return Ok(None); }; - let prunable_records = match safe_prune_through { - Some(safe_through) => { - match crate::sync::read_journal_records( - self.inner.sync_ctx.journal_path(), - &self.inner.vfs, - 0, - usize::MAX, - ) { - Ok(records) => records - .into_iter() - .filter(|record| record.sequence <= safe_through) - .count(), - Err(_) => 0, - } - } - None => 0, + let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(runtime, statement) + else { + return Ok(None); }; - - Ok(SyncRetentionReport { - journal_records: integrity.total_records, - first_sequence: integrity.first_sequence, - last_sequence: integrity.last_sequence, - safe_prune_through, - prunable_records, - blocked_by, - journal_size_bytes, - }) - } - - pub fn sync_operational_doctor_report(&self) -> Result { - let status = self.sync_status()?; - let integrity = self.sync_integrity_report()?; - let retention = self.sync_retention_report()?; - let peer_lag = self.sync_peer_lag_report()?; - let unresolved_conflicts = self.sync_conflicts()?.len(); - let mut recent_sessions = self.sync_sessions()?; - if recent_sessions.len() > 5 { - recent_sessions = recent_sessions.split_off(recent_sessions.len() - 5); - } - let mut issues = integrity.issues.clone(); - let mut guidance = Vec::new(); - let mut highest_severity = integrity.highest_severity; - - if !status.enabled { - highest_severity = highest_severity.max(SyncDoctorSeverity::Warning); - guidance.push( - "sync is disabled; enable it before expecting journal growth or peer watermarks" - .to_string(), - ); - } - - if unresolved_conflicts > 0 { - highest_severity = highest_severity.max(SyncDoctorSeverity::Warning); - let message = format!("{unresolved_conflicts} unresolved conflict(s) need attention"); - issues.push(SyncJournalIssue { - line_number: 0, - sequence: None, - severity: SyncDoctorSeverity::Warning, - code: "unresolved_conflicts".to_string(), - message: message.clone(), - }); - guidance.push(message); - } - - if retention.journal_records > 0 && retention.safe_prune_through.is_none() { - highest_severity = highest_severity.max(SyncDoctorSeverity::Warning); - let message = if retention.blocked_by.is_empty() { - "safe prune is unavailable because no peer watermarks are known".to_string() - } else { - format!( - "safe prune is blocked by {}", - retention.blocked_by.join(", ") - ) - }; - issues.push(SyncJournalIssue { - line_number: 0, - sequence: None, - severity: SyncDoctorSeverity::Warning, - code: "retention_blocked".to_string(), - message: message.clone(), - }); - guidance.push(message); - } else if let Some(safe_through) = retention.safe_prune_through { - guidance.push(format!( - "safe prune is available through sequence {safe_through}" - )); - } - - if peer_lag.iter().any(|peer| { - peer.in_lag.is_some_and(|lag| lag > 0) || peer.out_lag.is_some_and(|lag| lag > 0) - }) { - highest_severity = highest_severity.max(SyncDoctorSeverity::Warning); - guidance.push("peer lag exists; inspect sys_sync_peer_lag before pruning".to_string()); - } - - if integrity.highest_severity == SyncDoctorSeverity::Error { - highest_severity = SyncDoctorSeverity::Error; - } - - if issues.is_empty() { - guidance.push("journal integrity is clean".to_string()); + let mut working = runtime.clone(); + let base_refs: Vec<&str> = base_tables.iter().map(String::as_str).collect(); + self.load_runtime_table_row_sources_at_snapshot(&mut working, &base_refs, snapshot_lsn)?; + if rebuild_stale_indexes { + working.rebuild_stale_indexes(self.inner.config.page_size)?; } - - Ok(SyncOperationalDoctorReport { - status, - integrity, - retention, - peer_lag, - unresolved_conflicts, - recent_sessions, - highest_severity, - issues, - guidance, - }) + let result = working.execute_read_statement(statement, params, self.inner.config.page_size); + drop(working); + Ok(Some(result?)) } - pub fn sync_status(&self) -> Result { - if self.inner.sync_ctx.is_enabled() { - return Ok(SyncStatus { - enabled: true, - replica_id: self.inner.sync_ctx.replica_id(), - next_sequence: self.inner.sync_ctx.next_sequence(), - journal_path: Some( - self.inner - .sync_ctx - .journal_path() - .to_string_lossy() - .to_string(), - ), - journal_size_bytes: self.inner.sync_ctx.journal_size_bytes(), - }); - } - self.load_sync_status_from_db() + fn safe_referenced_base_tables_in_runtime( + &self, + runtime: &EngineRuntime, + statement: &SqlStatement, + ) -> Option> { + let mut visited_triggers = BTreeSet::new(); + self.collect_safe_referenced_base_tables_in_runtime( + runtime, + statement, + &mut visited_triggers, + ) } - pub fn sync_pending_changes( + fn collect_safe_referenced_base_tables_in_runtime( &self, - since_seq: u64, - limit: usize, - ) -> Result> { - if !self.inner.sync_ctx.is_enabled() { - let loaded = self.load_sync_status_from_db()?; - if !loaded.enabled { - return Ok(Vec::new()); + runtime: &EngineRuntime, + statement: &SqlStatement, + visited_triggers: &mut BTreeSet, + ) -> Option> { + use crate::sql::ast::safe_referenced_tables; + + let tables = safe_referenced_tables(statement)?; + let mut base_tables = Vec::new(); + for name in tables { + let is_base = runtime + .catalog + .tables + .keys() + .any(|entry| entry.eq_ignore_ascii_case(&name)); + let is_temp = runtime + .temp_tables + .keys() + .any(|entry| entry.eq_ignore_ascii_case(&name)); + if !is_base && !is_temp { + return None; + } + if is_base { + base_tables.push(name); } } - crate::sync::read_journal_records( - self.inner.sync_ctx.journal_path(), - &self.inner.vfs, - since_seq, - limit, - ) - } - - fn sync_scope_row_filter_expr(scope: &SyncScope) -> Result> { - match scope.row_filter.as_deref() { - Some(filter_sql) => Ok(Some(parse_expression_sql(filter_sql).map_err(|error| { - DbError::sql(format!( - "invalid row filter for sync scope '{}': {error}", - scope.name - )) - })?)), - None => Ok(None), - } - } - - fn sync_scope_record_matches( - &self, - scope: &SyncScope, - record: &SyncJournalRecord, - ) -> Result { - if !scope - .include_tables - .iter() - .any(|table_name| table_name.eq_ignore_ascii_case(&record.table)) - { - return Ok(false); + if let SqlStatement::Delete(delete) = statement { + for child in runtime.delete_row_source_dependency_tables(delete)? { + if base_tables + .iter() + .any(|entry| entry.eq_ignore_ascii_case(&child)) + { + continue; + } + base_tables.push(child); + } } - let Some(expr) = Self::sync_scope_row_filter_expr(scope)? else { - return Ok(true); - }; - let runtime = self.runtime_for_metadata_inspection()?; - let table = runtime - .catalog - .table(&record.table) - .ok_or_else(|| DbError::sql(format!("unknown table '{}'", record.table)))?; - let payload = match record.operation.as_str() { - "delete" => &record.primary_key, - "insert" | "update" => record - .after - .as_ref() - .ok_or_else(|| DbError::sql("sync record missing after payload"))?, - other => { - return Err(DbError::sql(format!("unsupported operation '{other}'"))); + if let SqlStatement::Insert(insert) = statement { + for child in runtime.insert_row_source_dependency_tables(insert)? { + if base_tables + .iter() + .any(|entry| entry.eq_ignore_ascii_case(&child)) + { + continue; + } + base_tables.push(child); } - }; - let payload = payload.as_object().ok_or_else(|| { - DbError::sql(format!( - "sync record for table '{}' must use an object payload", - record.table - )) - })?; - - let mut values = Vec::with_capacity(scope.filter_columns.len()); - for column_name in &scope.filter_columns { - let column = table - .columns - .iter() - .find(|candidate| candidate.name.eq_ignore_ascii_case(column_name)) - .ok_or_else(|| { - DbError::sql(format!( - "sync scope column '{column_name}' is missing from table '{}'", - table.name - )) - })?; - let json_value = payload.get(&column.name).ok_or_else(|| { - DbError::sql(format!( - "sync record for table '{}' is missing scoped column '{}'", - table.name, column.name - )) - })?; - values.push(json_to_column_value(&table.name, column, json_value)?); } - - row_satisfies_expression(&runtime, &table.name, &scope.filter_columns, &values, &expr) - } - - fn sync_filter_records_for_scope( - &self, - scope: &SyncScope, - records: Vec, - ) -> Result> { - let mut filtered = Vec::new(); - for record in records { - if self.sync_scope_record_matches(scope, &record)? { - filtered.push(record); + if let SqlStatement::Update(update) = statement { + for child in runtime.update_row_source_dependency_tables(update)? { + if base_tables + .iter() + .any(|entry| entry.eq_ignore_ascii_case(&child)) + { + continue; + } + base_tables.push(child); } } - Ok(filtered) + self.append_trigger_dependency_tables( + runtime, + statement, + &mut base_tables, + visited_triggers, + )?; + Some(base_tables) } - fn sync_validate_batch_for_scope( + fn append_trigger_dependency_tables( &self, - scope: &SyncScope, - batch: &SyncChangeBatch, - ) -> Result<()> { - let runtime = self.runtime_for_metadata_inspection()?; - for record in &batch.records { - if !scope - .include_tables - .iter() - .any(|table_name| table_name.eq_ignore_ascii_case(&record.table)) + runtime: &EngineRuntime, + statement: &SqlStatement, + base_tables: &mut Vec, + visited_triggers: &mut BTreeSet, + ) -> Option<()> { + let (target_name, event) = match statement { + SqlStatement::Insert(insert) => (insert.table_name.as_str(), TriggerEvent::Insert), + SqlStatement::Update(update) => (update.table_name.as_str(), TriggerEvent::Update), + SqlStatement::Delete(delete) => (delete.table_name.as_str(), TriggerEvent::Delete), + _ => return Some(()), + }; + for trigger in runtime.catalog.triggers.values() { + if trigger.on_view + || trigger.event != event + || !identifiers_equal(&trigger.target_name, target_name) + || !visited_triggers.insert(trigger.name.clone()) { - return Err(DbError::sql(format!( - "sync batch contains table '{}' which is outside scope '{}'", - record.table, scope.name - ))); + continue; } - if scope.row_filter.is_some() { - let table = runtime - .catalog - .table(&record.table) - .ok_or_else(|| DbError::sql(format!("unknown table '{}'", record.table)))?; - let payload = match record.operation.as_str() { - "delete" => &record.primary_key, - "insert" | "update" => record - .after - .as_ref() - .ok_or_else(|| DbError::sql("sync record missing after payload"))?, - other => { - return Err(DbError::sql(format!("unsupported operation '{other}'"))); - } - }; - let payload = payload.as_object().ok_or_else(|| { - DbError::sql(format!( - "sync record for table '{}' must use an object payload", - record.table - )) - })?; - let mut values = Vec::with_capacity(scope.filter_columns.len()); - for column_name in &scope.filter_columns { - let column = table - .columns - .iter() - .find(|candidate| candidate.name.eq_ignore_ascii_case(column_name)) - .ok_or_else(|| { - DbError::sql(format!( - "sync scope column '{column_name}' is missing from table '{}'", - table.name - )) - })?; - let json_value = payload.get(&column.name).ok_or_else(|| { - DbError::sql(format!( - "sync record for table '{}' is missing scoped column '{}'", - table.name, column.name - )) - })?; - values.push(json_to_column_value(&table.name, column, json_value)?); - } - if !row_satisfies_expression( - &runtime, - &table.name, - &scope.filter_columns, - &values, - &Self::sync_scope_row_filter_expr(scope)? - .ok_or_else(|| DbError::internal("scope row filter expression missing"))?, - )? { - return Err(DbError::sql(format!( - "sync batch contains record for table '{}' that does not match scope '{}'", - record.table, scope.name - ))); + let trigger_statement = parse_sql_statement(&trigger.action_sql).ok()?; + for table in self.collect_safe_referenced_base_tables_in_runtime( + runtime, + &trigger_statement, + visited_triggers, + )? { + if base_tables + .iter() + .any(|entry| entry.eq_ignore_ascii_case(&table)) + { + continue; } + base_tables.push(table); } } - Ok(()) - } - - pub fn sync_export_batch(&self, since_seq: u64, limit: usize) -> Result { - let records = self.sync_pending_changes(since_seq, limit)?; - SyncChangeBatch::from_records(records) + Some(()) } - pub fn sync_create_changeset( + fn try_execute_indexed_join_grouped_count_query_at_snapshot( &self, - mut options: CreateChangesetOptions, - ) -> Result { - self.ensure_sync_tables()?; - let mut scoped_from_shape = false; - if let Some(principal) = options.principal.as_ref() { - principal.validate()?; - } - if let Some(shape_id) = options.shape_id.as_deref() { - let shape = self.sync_shape(shape_id)?.ok_or_else(|| { - DbError::sql(format!( - "SHAPE_NOT_FOUND: sync shape '{shape_id}' not found" - )) - })?; - self.sync_authorize_shape(options.principal.as_ref(), &shape)?; - options.scope_name = Some(shape.scope_name); - scoped_from_shape = true; - } - if let Some(scope_name) = options.scope_name.as_deref() { - if !scoped_from_shape { - self.sync_authorize_scope(options.principal.as_ref(), scope_name)?; - } + runtime: &EngineRuntime, + query: &crate::sql::ast::Query, + params: &[Value], + snapshot_lsn: u64, + ) -> Result> { + if !runtime.has_deferred_tables() { + return Ok(None); } + let Some(parent_table_name) = + runtime.indexed_join_grouped_count_parent_table_name(query, params)? + else { + return Ok(None); + }; - let max_records = options - .max_records - .map(usize::try_from) - .transpose() - .map_err(|_| DbError::sql("max_records is too large"))? - .unwrap_or(usize::MAX); - let created_at_micros = current_time_micros(); - let tooling = self.get_tooling_metadata()?; - let runtime = self.runtime_for_metadata_inspection()?; - let schema_cookie = runtime.catalog.schema_cookie; - let tenant_id = options - .principal - .as_ref() - .map(|principal| principal.tenant_id.clone()) - .or_else(|| { - options - .shape_id - .as_deref() - .and_then(|shape_id| self.sync_shape(shape_id).ok().flatten()) - .map(|shape| shape.tenant_id) - }); + let parent_table_name = parent_table_name.to_string(); + let mut join_runtime = runtime.clone(); + self.load_runtime_table_row_sources_at_snapshot( + &mut join_runtime, + &[parent_table_name.as_str()], + snapshot_lsn, + )?; + let result = join_runtime.try_execute_indexed_join_grouped_count_query(query, params); + drop(join_runtime); + result + } - let mut changeset = match &options.source { - SyncChangesetSource::Checkpoint { - peer, - since_sequence, - } => { - let batch = match options.scope_name.as_deref() { - Some(scope_name) => { - self.sync_export_batch_for_scope(scope_name, *since_sequence, max_records)? - } - None => self.sync_export_batch(*since_sequence, max_records)?, - }; - let source_replica_id = batch - .source_replica_id - .clone() - .or_else(|| self.sync_status().ok().and_then(|status| status.replica_id)) - .unwrap_or_else(|| "unknown".to_string()); - let records = batch - .records - .iter() - .map(sync_changeset_record_from_journal_record) - .collect::>(); - let start_checkpoint = batch.first_sequence; - let end_checkpoint = batch.last_sequence; - let source_high_watermark = batch - .source_high_watermark - .or(batch.last_sequence) - .or_else(|| { - self.sync_integrity_report() - .ok() - .and_then(|report| report.last_sequence) - }); - let changeset_id = sync_changeset_id( - "checkpoint", - &source_replica_id, - created_at_micros, - records.len(), - ); - SyncChangeset { - changeset_version: crate::sync::SYNC_CHANGESET_VERSION, - changeset_id, - source_replica_id, - source_kind: options.source.kind(), - tenant_id, - scope_name: options.scope_name.clone(), - shape_id: options.shape_id.clone(), - base_kind: "checkpoint".to_string(), - base_checkpoint: Some(SyncChangesetCheckpoint { - peer: peer.clone(), - sequence: *since_sequence, - }), - base_branch: None, - base_snapshot: None, - start_checkpoint, - end_checkpoint, - source_high_watermark, - schema_fingerprint: tooling.schema_fingerprint.clone(), - schema_cookie, - sync_contract_version: crate::sync::SYNC_CONTRACT_VERSION, - query_contract_fingerprint: None, - producer_capabilities: SyncChangesetCapabilities::default(), - limits: SyncChangesetLimits::default(), - records, - conflict_policy_hint: None, - created_at_micros, - integrity_hash: None, - } - } - SyncChangesetSource::Branch { from, to } => { - self.sync_create_diff_changeset(SyncDiffChangesetContext { - base_kind: "branch", - from_ref: from, - to_ref: to, - scope_name: options.scope_name.as_deref(), - shape_id: options.shape_id.as_deref(), - tenant_id: tenant_id.as_deref(), - schema_fingerprint: &tooling.schema_fingerprint, - schema_cookie, - created_at_micros, - max_records, - })? - } - SyncChangesetSource::Snapshot { from, to } => { - self.sync_create_diff_changeset(SyncDiffChangesetContext { - base_kind: "snapshot", - from_ref: from, - to_ref: to, - scope_name: options.scope_name.as_deref(), - shape_id: options.shape_id.as_deref(), - tenant_id: tenant_id.as_deref(), - schema_fingerprint: &tooling.schema_fingerprint, - schema_cookie, - created_at_micros, - max_records, - })? - } + fn try_execute_simple_indexed_join_projection_query_at_snapshot( + &self, + runtime: &EngineRuntime, + statement: &SqlStatement, + query: &crate::sql::ast::Query, + params: &[Value], + snapshot_lsn: u64, + ) -> Result> { + if !runtime.has_deferred_tables() { + return Ok(None); + } + let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(runtime, statement) + else { + return Ok(None); }; - self.sync_finalize_changeset(&mut changeset, options.max_bytes)?; - self.sync_record_changeset_history(&changeset, "created", None)?; - Ok(changeset) + if base_tables.is_empty() { + return Ok(None); + } + let mut join_runtime = runtime.clone(); + let base_refs: Vec<&str> = base_tables.iter().map(String::as_str).collect(); + self.load_runtime_table_row_sources_at_snapshot( + &mut join_runtime, + &base_refs, + snapshot_lsn, + )?; + let result = join_runtime.try_execute_simple_indexed_join_projection_query(query, params); + drop(join_runtime); + result } - fn sync_create_diff_changeset( + fn ensure_runtime_tables_loaded_at_snapshot( &self, - ctx: SyncDiffChangesetContext<'_>, - ) -> Result { - let diff = self.branch_diff(ctx.from_ref, ctx.to_ref)?; - let table_infos = self - .list_tables()? - .into_iter() - .map(|table| (table.name.clone(), table)) - .collect::>(); - let mut records = Vec::new(); - let source_replica_id = format!("{}:{}:{}", ctx.base_kind, ctx.from_ref, ctx.to_ref); - let mut sequence = 1u64; - for table_diff in &diff.tables { - if matches!( - table_diff.status, - crate::branch::BranchTableDiffStatus::Unsupported - ) { - return Err(DbError::sql(format!( - "CHANGESET_UNSUPPORTED: branch/snapshot diff for table '{}' is unsupported: {}", - table_diff.table, - table_diff - .message - .clone() - .unwrap_or_else(|| "unsupported row diff".to_string()) - ))); - } - if table_diff.schema_changed { - return Err(DbError::sql(format!( - "SCHEMA_INCOMPATIBLE: changeset diff for table '{}' changes schema", - table_diff.table - ))); - } - let Some(table) = table_infos.get(&table_diff.table) else { - continue; - }; - let pk_names = &table.primary_key_columns; - let column_names = table - .columns - .iter() - .map(|column| column.name.clone()) - .collect::>(); - let table_context = BranchChangesetTableContext { - source_replica_id: &source_replica_id, - table_name: &table.name, - primary_key_columns: pk_names, - column_names: &column_names, - schema_cookie: ctx.schema_cookie, - created_at_micros: ctx.created_at_micros, - }; - for row in &table_diff.added { - records.push(sync_changeset_record_from_branch_row( - &table_context, - sequence, - "insert", - row, - )?); - sequence += 1; - } - for row in &table_diff.updated { - records.push(sync_changeset_record_from_branch_row( - &table_context, - sequence, - "update", - row, - )?); - sequence += 1; - } - for row in &table_diff.deleted { - records.push(sync_changeset_record_from_branch_row( - &table_context, - sequence, - "delete", - row, - )?); - sequence += 1; - } - if records.len() > ctx.max_records { - return Err(DbError::sql( - "BATCH_TOO_LARGE: changeset exceeds max_records", - )); - } + runtime: &mut EngineRuntime, + names: &[&str], + snapshot_lsn: u64, + ) -> Result<()> { + if names.is_empty() || !runtime.has_deferred_tables() { + return Ok(()); + } + let has_match = names.iter().any(|name| { + runtime + .deferred_table_names() + .any(|deferred| deferred.eq_ignore_ascii_case(name)) + }); + if !has_match { + return Ok(()); } + let filter: BTreeSet = names.iter().map(|name| (*name).to_string()).collect(); + runtime.load_deferred_tables_filtered_at_snapshot( + &self.inner.pager, + &self.inner.wal, + self.inner.config.page_size, + &filter, + snapshot_lsn, + ) + } - let source_kind = if ctx.base_kind == "branch" { - crate::sync::SyncChangesetSourceKind::Branch - } else { - crate::sync::SyncChangesetSourceKind::Snapshot + fn ensure_runtime_tables_loaded_for_statement_at_snapshot( + &self, + runtime: &mut EngineRuntime, + statement: &SqlStatement, + snapshot_lsn: u64, + ) -> Result { + let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(runtime, statement) + else { + return Ok(false); }; - let changeset_id = sync_changeset_id( - ctx.base_kind, - &source_replica_id, - ctx.created_at_micros, - records.len(), - ); - Ok(SyncChangeset { - changeset_version: crate::sync::SYNC_CHANGESET_VERSION, - changeset_id, - source_replica_id, - source_kind, - tenant_id: ctx.tenant_id.map(str::to_string), - scope_name: ctx.scope_name.map(str::to_string), - shape_id: ctx.shape_id.map(str::to_string), - base_kind: ctx.base_kind.to_string(), - base_checkpoint: None, - base_branch: (ctx.base_kind == "branch").then(|| ctx.from_ref.to_string()), - base_snapshot: (ctx.base_kind == "snapshot").then(|| ctx.from_ref.to_string()), - start_checkpoint: records.first().map(|record| record.origin_sequence), - end_checkpoint: records.last().map(|record| record.origin_sequence), - source_high_watermark: records.last().map(|record| record.origin_sequence), - schema_fingerprint: ctx.schema_fingerprint.to_string(), - schema_cookie: ctx.schema_cookie, - sync_contract_version: crate::sync::SYNC_CONTRACT_VERSION, - query_contract_fingerprint: None, - producer_capabilities: SyncChangesetCapabilities { - before_images: true, - ..SyncChangesetCapabilities::default() - }, - limits: SyncChangesetLimits::default(), - records, - conflict_policy_hint: None, - created_at_micros: ctx.created_at_micros, - integrity_hash: None, - }) + if base_tables.is_empty() { + return Ok(true); + } + let base_tables: Vec<&str> = base_tables.iter().map(String::as_str).collect(); + self.ensure_runtime_tables_loaded_at_snapshot(runtime, &base_tables, snapshot_lsn)?; + Ok(true) } - pub fn sync_inspect_changeset( + fn ensure_runtime_all_tables_loaded_at_snapshot( &self, - changeset: &SyncChangeset, - options: InspectChangesetOptions, - ) -> Result { - self.sync_validate_changeset_envelope(changeset)?; - let bytes = serde_json::to_vec(changeset) - .map_err(|error| DbError::internal(format!("failed to serialize changeset: {error}")))? - .len() as u64; - let mut tables = BTreeSet::new(); - let mut operations = BTreeMap::new(); - let mut warnings = Vec::new(); - for record in &changeset.records { - tables.insert(record.table.clone()); - *operations.entry(record.operation.clone()).or_insert(0u64) += 1; - if record.operation == "delete" && record.before.is_none() { - warnings.push(format!( - "delete record for table '{}' cannot be inverted without before image", - record.table - )); - } + runtime: &mut EngineRuntime, + snapshot_lsn: u64, + ) -> Result<()> { + if !runtime.has_deferred_tables() { + return Ok(()); } - let compatibility = if options.check_local_compatibility { - match self.sync_check_changeset_compatibility(changeset) { - Ok(()) => SyncChangesetCompatibility { - checked_against_local_db: true, - status: "compatible".to_string(), - message: None, - }, - Err(error) => SyncChangesetCompatibility { - checked_against_local_db: true, - status: "incompatible".to_string(), - message: Some(error.to_string()), - }, - } + if self.inner.config.paged_row_storage { + runtime.load_deferred_table_row_sources_at_snapshot( + &self.inner.pager, + &self.inner.wal, + self.inner.config.page_size, + snapshot_lsn, + ) } else { - SyncChangesetCompatibility { - checked_against_local_db: false, - status: "not_checked".to_string(), - message: None, - } - }; - Ok(SyncChangesetInspection { - changeset_id: changeset.changeset_id.clone(), - valid_envelope: true, - source_kind: changeset.source_kind.clone(), - scope_name: changeset.scope_name.clone(), - shape_id: changeset.shape_id.clone(), - record_count: changeset.records.len() as u64, - bytes, - tables: tables.into_iter().collect(), - operations, - start_checkpoint: changeset.start_checkpoint, - end_checkpoint: changeset.end_checkpoint, - schema_fingerprint: changeset.schema_fingerprint.clone(), - compatibility, - warnings, - }) + runtime.load_deferred_tables_at_snapshot( + &self.inner.pager, + &self.inner.wal, + self.inner.config.page_size, + snapshot_lsn, + ) + } } - pub fn sync_apply_changeset( + fn execute_read_in_runtime_state( &self, - changeset: &SyncChangeset, - options: ApplyChangesetOptions, - ) -> Result { - self.ensure_sync_tables()?; - self.sync_validate_changeset_envelope(changeset)?; - if !options.atomic { - return Err(DbError::sql( - "CHANGESET_UNSUPPORTED: non-atomic changeset apply is not supported", - )); - } - if let Some(principal) = options.principal.as_ref() { - principal.validate()?; - } - let mut scope_authorized_via_shape = false; - if let Some(shape_id) = changeset.shape_id.as_deref() { - let shape = self.sync_shape(shape_id)?.ok_or_else(|| { - DbError::sql(format!( - "SHAPE_NOT_FOUND: sync shape '{shape_id}' not found" - )) - })?; - scope_authorized_via_shape = true; - self.sync_authorize_shape(options.principal.as_ref(), &shape)?; + statement: &SqlStatement, + params: &[Value], + runtime: &mut EngineRuntime, + snapshot_lsn: u64, + indexes_maybe_stale: &mut bool, + ) -> Result { + let security_active = + self.load_security_tables_for_runtime_at_snapshot(runtime, snapshot_lsn)?; + if self.statement_is_temp_only(runtime, statement) { + return runtime.execute_read_statement(statement, params, self.inner.config.page_size); } - if let Some(scope_name) = changeset.scope_name.as_deref() { - if !scope_authorized_via_shape { - self.sync_authorize_scope(options.principal.as_ref(), scope_name)?; + if !security_active && !*indexes_maybe_stale { + if let SqlStatement::Query(query) = statement { + if let Some(result) = self + .try_execute_indexed_join_grouped_count_query_at_snapshot( + runtime, + query, + params, + snapshot_lsn, + )? + { + return Ok(result); + } + if let Some(result) = self + .try_execute_simple_indexed_join_projection_query_at_snapshot( + runtime, + statement, + query, + params, + snapshot_lsn, + )? + { + return Ok(result); + } } } - if matches!( - options.compatibility_mode, - crate::sync::SyncCompatibilityMode::Strict - ) { - self.sync_check_changeset_compatibility(changeset)?; - } - let integrity_hash = self.sync_changeset_integrity_hash(changeset)?; - if let Some(existing) = - self.sync_read_metadata(&changeset_applied_key(&changeset.changeset_id))? - { - if existing != integrity_hash { - return Err(DbError::sql(format!( - "CHANGESET_ID_COLLISION: changeset '{}' was already applied with a different integrity hash", - changeset.changeset_id - ))); + if !security_active { + if let Some(result) = self.try_execute_query_with_row_sources_at_snapshot( + runtime, + statement, + params, + snapshot_lsn, + *indexes_maybe_stale, + )? { + return Ok(result); } - return Ok(SyncChangesetApplyResult { - outcome: "already_applied".to_string(), - changeset_id: changeset.changeset_id.clone(), - rows_seen: changeset.records.len() as u64, - rows_applied: 0, - rows_skipped: changeset.records.len() as u64, - rows_conflicted: 0, - checkpoint_after: changeset.source_high_watermark.or(changeset.end_checkpoint), - }); } - - let journal_records = changeset - .records - .iter() - .map(sync_journal_record_from_changeset_record) - .collect::>>()?; - let batch = SyncChangeBatch::scoped_from_records( - journal_records, - Some(changeset.source_replica_id.clone()), - changeset.source_high_watermark.or(changeset.end_checkpoint), - )?; - let summary = match (changeset.scope_name.as_deref(), options.conflict_policy) { - (Some(scope_name), Some(policy)) => { - self.sync_import_batch_for_scope_with_policy(scope_name, &batch, policy)? - } - (Some(scope_name), None) => self.sync_import_batch_for_scope(scope_name, &batch)?, - (None, Some(policy)) => self.sync_import_batch_with_policy(&batch, policy)?, - (None, None) => self.sync_import_batch(&batch)?, - }; - self.sync_upsert_metadata( - &changeset_applied_key(&changeset.changeset_id), - &integrity_hash, + let targeted_ok = self.ensure_runtime_tables_loaded_for_statement_at_snapshot( + runtime, + statement, + snapshot_lsn, )?; - self.sync_record_changeset_history(changeset, "applied", Some(current_time_micros()))?; - Ok(SyncChangesetApplyResult { - outcome: if summary.conflicted > 0 { - "conflict_recorded".to_string() - } else { - "applied".to_string() - }, - changeset_id: changeset.changeset_id.clone(), - rows_seen: summary.seen as u64, - rows_applied: summary.applied as u64, - rows_skipped: summary.skipped as u64, - rows_conflicted: summary.conflicted as u64, - checkpoint_after: changeset.source_high_watermark.or(changeset.end_checkpoint), - }) - } - - pub fn sync_invert_changeset( - &self, - changeset: &SyncChangeset, - _options: InvertChangesetOptions, - ) -> Result { - self.sync_validate_changeset_envelope(changeset)?; - let created_at_micros = current_time_micros(); - let mut inverse_records = Vec::with_capacity(changeset.records.len()); - for (index, record) in changeset.records.iter().enumerate() { - let operation = match record.operation.as_str() { - "insert" => "delete", - "delete" if record.before.is_some() => "insert", - "update" if record.before.is_some() => "update", - "delete" | "update" => { - return Err(DbError::sql(format!( - "CHANGESET_INVERSION_UNSUPPORTED: record {index} lacks before image" - ))); - } - other => { - return Err(DbError::sql(format!( - "CHANGESET_INVALID: unsupported record operation '{other}'" - ))); - } - }; - inverse_records.push(SyncChangesetRecord { - record_version: record.record_version, - table: record.table.clone(), - operation: operation.to_string(), - primary_key: record.primary_key.clone(), - origin_replica_id: format!("inverse:{}", changeset.changeset_id), - origin_sequence: (index as u64) + 1, - transaction_id: format!("txn:inverse:{}", changeset.changeset_id), - transaction_lsn: (index as u64) + 1, - schema_cookie: record.schema_cookie, - before_hash: None, - before: record.after.clone(), - after: if operation == "delete" { - None - } else { - record.before.clone() - }, - column_mask: record.column_mask.clone(), - tombstone: operation == "delete", - conflict_metadata: None, - }); + if !targeted_ok { + // Intentionally unsupported for row-source execution: the + // statement analyzer could not determine a conservative set of + // referenced base tables (CTEs, recursive queries, VALUES, + // subqueries, etc.). Fall back to broad-load so the generic + // executor has every table available. + self.ensure_runtime_all_tables_loaded_at_snapshot(runtime, snapshot_lsn)?; } - let source_replica_id = format!("inverse:{}", changeset.source_replica_id); - let mut inverse = SyncChangeset { - changeset_version: crate::sync::SYNC_CHANGESET_VERSION, - changeset_id: sync_changeset_id( - "inverse", - &source_replica_id, - created_at_micros, - inverse_records.len(), - ), - source_replica_id, - source_kind: changeset.source_kind.clone(), - tenant_id: changeset.tenant_id.clone(), - scope_name: changeset.scope_name.clone(), - shape_id: changeset.shape_id.clone(), - base_kind: format!("inverse:{}", changeset.base_kind), - base_checkpoint: changeset.base_checkpoint.clone(), - base_branch: changeset.base_branch.clone(), - base_snapshot: changeset.base_snapshot.clone(), - start_checkpoint: inverse_records.first().map(|record| record.origin_sequence), - end_checkpoint: inverse_records.last().map(|record| record.origin_sequence), - source_high_watermark: inverse_records.last().map(|record| record.origin_sequence), - schema_fingerprint: changeset.schema_fingerprint.clone(), - schema_cookie: changeset.schema_cookie, - sync_contract_version: changeset.sync_contract_version, - query_contract_fingerprint: changeset.query_contract_fingerprint.clone(), - producer_capabilities: SyncChangesetCapabilities { - before_images: true, - ..SyncChangesetCapabilities::default() - }, - limits: SyncChangesetLimits::default(), - records: inverse_records, - conflict_policy_hint: changeset.conflict_policy_hint.clone(), - created_at_micros, - integrity_hash: None, - }; - self.sync_finalize_changeset(&mut inverse, None)?; - Ok(inverse) + if *indexes_maybe_stale { + runtime.rebuild_stale_indexes(self.inner.config.page_size)?; + *indexes_maybe_stale = false; + } + runtime.execute_read_statement(statement, params, self.inner.config.page_size) } - pub fn sync_create_shape(&self, options: CreateShapeOptions) -> Result { - self.ensure_sync_tables()?; - let shape_id = options.shape_id.trim(); - let scope_name = options.scope_name.trim(); - let tenant_id = options.tenant_id.trim(); - if shape_id.is_empty() { - return Err(DbError::sql("sync shape_id must not be empty")); - } - if scope_name.is_empty() { - return Err(DbError::sql("sync shape scope_name must not be empty")); - } - if tenant_id.is_empty() { - return Err(DbError::sql( - "TENANT_REQUIRED: sync shape tenant_id is required", + fn execute_write_in_runtime_state( + &self, + statement: &SqlStatement, + params: &[Value], + runtime: &mut EngineRuntime, + snapshot_lsn: u64, + persistent_changed: &mut bool, + indexes_maybe_stale: &mut bool, + ) -> Result { + if matches!(statement, SqlStatement::Analyze { .. }) { + return Err(DbError::transaction( + "ANALYZE is not supported inside an explicit SQL transaction", )); } - let scope = self - .sync_scope(scope_name)? - .ok_or_else(|| DbError::sql(format!("sync scope '{scope_name}' not found")))?; - if scope.include_tables.is_empty() { - return Err(DbError::sql(format!( - "sync scope '{scope_name}' has no included tables" - ))); - } - let name = options.name.as_deref().unwrap_or(shape_id).trim(); - if name.is_empty() { - return Err(DbError::sql("sync shape name must not be empty")); - } - let now = current_time_micros(); - let existing = self.sync_shape(shape_id)?; - let created_at_micros = existing - .as_ref() - .map(|shape| shape.created_at_micros) - .unwrap_or(now); - let retention_ttl_micros = options - .retention_ttl_micros - .unwrap_or(30 * 24 * 60 * 60 * 1_000_000); - let max_records = options.max_records.unwrap_or(50_000); - let ack_deadline_micros = options.ack_deadline_micros.unwrap_or(30_000_000); - let heartbeat_micros = options.heartbeat_micros.unwrap_or(20_000_000); - let allowed_roles_json = - serde_json::to_string(&options.allowed_roles).map_err(|error| { - DbError::internal(format!("failed to encode shape allowed roles: {error}")) - })?; - let allowed_subjects_json = - serde_json::to_string(&options.allowed_subjects).map_err(|error| { - DbError::internal(format!("failed to encode shape allowed subjects: {error}")) - })?; - let sql = format!( - "INSERT INTO {table} (shape_id, name, scope_name, tenant_id, allowed_roles_json, allowed_subjects_json, created_at_micros, updated_at_micros, retention_ttl_micros, max_records, ack_deadline_micros, heartbeat_micros) VALUES ({shape_id}, {name}, {scope_name}, {tenant_id}, {allowed_roles_json}, {allowed_subjects_json}, {created_at_micros}, {updated_at_micros}, {retention_ttl_micros}, {max_records}, {ack_deadline_micros}, {heartbeat_micros}) ON CONFLICT (shape_id) DO UPDATE SET name = {name}, scope_name = {scope_name}, tenant_id = {tenant_id}, allowed_roles_json = {allowed_roles_json}, allowed_subjects_json = {allowed_subjects_json}, updated_at_micros = {updated_at_micros}, retention_ttl_micros = {retention_ttl_micros}, max_records = {max_records}, ack_deadline_micros = {ack_deadline_micros}, heartbeat_micros = {heartbeat_micros}", - table = crate::sync::SHAPES_TABLE, - shape_id = sql_text_literal(shape_id), - name = sql_text_literal(name), - scope_name = sql_text_literal(&scope.name), - tenant_id = sql_text_literal(tenant_id), - allowed_roles_json = sql_text_literal(&allowed_roles_json), - allowed_subjects_json = sql_text_literal(&allowed_subjects_json), - created_at_micros = created_at_micros, - updated_at_micros = now, - retention_ttl_micros = retention_ttl_micros, - max_records = max_records, - ack_deadline_micros = ack_deadline_micros, - heartbeat_micros = heartbeat_micros, - ); - let _ = self.execute(&sql)?; - self.sync_shape(shape_id)? - .ok_or_else(|| DbError::internal("sync shape missing after create/update")) - } - pub fn sync_drop_shape(&self, shape_id: &str) -> Result { - self.ensure_sync_tables()?; - let shape_id = shape_id.trim(); - if shape_id.is_empty() { - return Err(DbError::sql("sync shape_id must not be empty")); + if *indexes_maybe_stale { + runtime.rebuild_stale_indexes(self.inner.config.page_size)?; + *indexes_maybe_stale = false; } - let _ = self.execute(&format!( - "DELETE FROM {} WHERE shape_id = {}", - crate::sync::SHAPE_CLIENTS_TABLE, - sql_text_literal(shape_id) - ))?; - let result = self.execute(&format!( - "DELETE FROM {} WHERE shape_id = {}", - crate::sync::SHAPES_TABLE, - sql_text_literal(shape_id) - ))?; - Ok(result.affected_rows() > 0) - } - pub fn sync_shape(&self, shape_id: &str) -> Result> { - self.ensure_sync_tables()?; - let sql = format!( - "SELECT shape_id, name, scope_name, tenant_id, allowed_roles_json, allowed_subjects_json, created_at_micros, updated_at_micros, retention_ttl_micros, max_records, ack_deadline_micros, heartbeat_micros FROM {} WHERE shape_id = {}", - crate::sync::SHAPES_TABLE, - sql_text_literal(shape_id) - ); - match self.execute(&sql) { - Ok(result) => result.rows().first().map(sync_shape_from_row).transpose(), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(None) - } else { - Err(error) + let temp_only = self.statement_is_temp_only(runtime, statement); + match statement { + SqlStatement::Insert(insert) => { + let table_names = + self.insert_dependency_table_names(runtime, &insert.table_name)?; + let table_refs = table_names.iter().map(String::as_str).collect::>(); + self.load_runtime_table_row_sources_at_snapshot( + runtime, + &table_refs, + snapshot_lsn, + )?; + if let Some(prepared_insert) = runtime.prepare_simple_insert(insert)? { + let result = runtime.execute_prepared_simple_insert( + &prepared_insert, + params, + self.inner.config.page_size, + )?; + *persistent_changed |= !temp_only; + return Ok(result); + } + if runtime.can_execute_insert_in_place(insert) { + let result = runtime.execute_statement( + statement, + params, + self.inner.config.page_size, + )?; + *persistent_changed |= !temp_only; + return Ok(result); } } - } - } - - pub fn sync_shapes(&self) -> Result> { - self.ensure_sync_tables()?; - let sql = format!( - "SELECT shape_id, name, scope_name, tenant_id, allowed_roles_json, allowed_subjects_json, created_at_micros, updated_at_micros, retention_ttl_micros, max_records, ack_deadline_micros, heartbeat_micros FROM {} ORDER BY shape_id", - crate::sync::SHAPES_TABLE, - ); - match self.execute(&sql) { - Ok(result) => result.rows().iter().map(sync_shape_from_row).collect(), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(Vec::new()) - } else { - Err(error) + SqlStatement::Update(update) => { + self.load_runtime_table_row_sources_at_snapshot( + runtime, + &[update.table_name.as_str()], + snapshot_lsn, + )?; + if let Some(prepared_update) = runtime.prepare_simple_update(update)? { + let result = runtime.execute_prepared_simple_update( + &prepared_update, + params, + self.inner.config.page_size, + )?; + *persistent_changed |= !temp_only; + return Ok(result); } } - } - } - - pub fn sync_shape_clients(&self) -> Result> { - self.ensure_sync_tables()?; - let sql = format!( - "SELECT shape_id, tenant_id, client_replica_id, subject_id, session_id, last_ack_sequence, last_ack_watermark, last_changeset_id, last_seen_at_micros, retention_blocking, status FROM {} ORDER BY shape_id, client_replica_id", - crate::sync::SHAPE_CLIENTS_TABLE, - ); - match self.execute(&sql) { - Ok(result) => result - .rows() - .iter() - .map(sync_shape_client_from_row) - .collect(), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(Vec::new()) - } else { - Err(error) + SqlStatement::Delete(delete) => { + if let Some(prepared_delete) = runtime.prepare_simple_delete(delete)? { + let table_names = prepared_delete.required_row_source_table_names(); + let child_index_targets = prepared_delete.child_index_hydration_targets(); + self.load_runtime_table_row_sources_and_child_indexes_at_snapshot( + runtime, + &table_names, + &child_index_targets, + snapshot_lsn, + )?; + if runtime.can_reuse_prepared_simple_delete(&prepared_delete) { + let result = runtime.execute_prepared_simple_delete( + &prepared_delete, + params, + self.inner.config.page_size, + )?; + *persistent_changed |= !temp_only; + return Ok(result); + } + if let Some(prepared_delete) = runtime.prepare_simple_delete(delete)? { + let result = runtime.execute_prepared_simple_delete( + &prepared_delete, + params, + self.inner.config.page_size, + )?; + *persistent_changed |= !temp_only; + return Ok(result); + } } } + _ => {} } - } - - pub fn sync_shape_snapshot( - &self, - shape_id: &str, - _client_replica_id: &str, - principal: Option, - ) -> Result { - self.ensure_sync_tables()?; - let shape = self.sync_shape(shape_id)?.ok_or_else(|| { - DbError::sql(format!( - "SHAPE_NOT_FOUND: sync shape '{shape_id}' not found" - )) - })?; - self.sync_authorize_shape(principal.as_ref(), &shape)?; - let scope = self - .sync_scope(&shape.scope_name)? - .ok_or_else(|| DbError::sql(format!("sync scope '{}' not found", shape.scope_name)))?; - let changeset = - self.sync_create_shape_snapshot_changeset(&shape, &scope, principal.as_ref())?; - let shape_sequence = changeset - .source_high_watermark - .or(changeset.end_checkpoint) - .unwrap_or(0); - Ok(SyncShapeDelivery { - message_type: "snapshot".to_string(), - shape_id: shape.shape_id, - shape_sequence, - ack_deadline_micros: current_time_micros() + shape.ack_deadline_micros, - checkpoint: SyncShapeCheckpoint { - shape_sequence, - source_high_watermark: changeset.source_high_watermark.unwrap_or(shape_sequence), - }, - changeset, - }) - } - pub fn sync_shape_changes( - &self, - shape_id: &str, - since_watermark: u64, - principal: Option, - ) -> Result { - let shape = self.sync_shape(shape_id)?.ok_or_else(|| { - DbError::sql(format!( - "SHAPE_NOT_FOUND: sync shape '{shape_id}' not found" - )) - })?; - self.sync_authorize_shape(principal.as_ref(), &shape)?; - let retention = self.sync_retention_report()?; - if let Some(first_sequence) = retention.first_sequence { - if since_watermark > 0 && since_watermark < first_sequence { - return Err(DbError::sql(format!( - "SHAPE_RESYNC_REQUIRED: since checkpoint {since_watermark} is below retained first sequence {first_sequence}" - ))); - } + if runtime.can_execute_statement_in_state_without_clone(statement) { + let Some(base_tables) = self.safe_referenced_base_tables_in_runtime(runtime, statement) + else { + // Intentionally unsupported for targeted loading: the + // statement analyzer could not determine a conservative set + // of referenced base tables. Fall back to broad-load. + self.ensure_runtime_all_tables_loaded_at_snapshot(runtime, snapshot_lsn)?; + let result = + runtime.execute_statement(statement, params, self.inner.config.page_size)?; + *persistent_changed |= !temp_only; + return Ok(result); + }; + let base_refs: Vec<&str> = base_tables.iter().map(String::as_str).collect(); + self.load_runtime_table_row_sources_at_snapshot(runtime, &base_refs, snapshot_lsn)?; + let result = + runtime.execute_statement(statement, params, self.inner.config.page_size)?; + *persistent_changed |= !temp_only; + return Ok(result); } - let changeset = self.sync_create_changeset(CreateChangesetOptions { - source: SyncChangesetSource::Checkpoint { - peer: shape_id.to_string(), - since_sequence: since_watermark, - }, - scope_name: Some(shape.scope_name.clone()), - shape_id: Some(shape.shape_id.clone()), - max_records: Some(shape.max_records), - max_bytes: None, - principal, - })?; - let shape_sequence = changeset - .source_high_watermark - .or(changeset.end_checkpoint) - .unwrap_or(since_watermark); - Ok(SyncShapeDelivery { - message_type: "changeset".to_string(), - shape_id: shape.shape_id, - shape_sequence, - ack_deadline_micros: current_time_micros() + shape.ack_deadline_micros, - checkpoint: SyncShapeCheckpoint { - shape_sequence, - source_high_watermark: changeset.source_high_watermark.unwrap_or(shape_sequence), - }, - changeset, - }) - } - pub fn sync_ack_shape(&self, ack: ShapeAckOptions) -> Result { - self.sync_ack_shape_with_principal(ack, None) - } - - pub fn sync_ack_shape_with_principal( - &self, - ack: ShapeAckOptions, - principal: Option<&SyncPrincipal>, - ) -> Result { - self.ensure_sync_tables()?; - let shape = self.sync_shape(&ack.shape_id)?.ok_or_else(|| { - DbError::sql(format!( - "SHAPE_NOT_FOUND: sync shape '{}' not found", - ack.shape_id - )) - })?; - self.sync_authorize_shape(principal, &shape)?; - if !shape.tenant_id.eq_ignore_ascii_case(&ack.tenant_id) { - return Err(DbError::sql(format!( - "AUTH_FORBIDDEN: shape '{}' belongs to tenant '{}'", - shape.shape_id, shape.tenant_id - ))); + let mut working = runtime.clone(); + let targeted_ok = self.ensure_runtime_tables_loaded_for_statement_at_snapshot( + &mut working, + statement, + snapshot_lsn, + )?; + if !targeted_ok { + // Intentionally unsupported for row-source execution: the + // statement analyzer could not determine a conservative set of + // referenced base tables (CTEs, recursive queries, VALUES, + // subqueries, etc.). Fall back to broad-load so the generic + // executor has every table available. + self.ensure_runtime_all_tables_loaded_at_snapshot(&mut working, snapshot_lsn)?; } - let now = current_time_micros(); - let sql = format!( - "INSERT INTO {table} (shape_id, tenant_id, client_replica_id, subject_id, session_id, last_ack_sequence, last_ack_watermark, last_changeset_id, last_seen_at_micros, retention_blocking, status) VALUES ({shape_id}, {tenant_id}, {client_replica_id}, {subject_id}, {session_id}, {last_ack_sequence}, {last_ack_watermark}, {last_changeset_id}, {last_seen_at_micros}, 1, 'active') ON CONFLICT (shape_id, client_replica_id) DO UPDATE SET tenant_id = {tenant_id}, subject_id = {subject_id}, session_id = {session_id}, last_ack_sequence = {last_ack_sequence}, last_ack_watermark = {last_ack_watermark}, last_changeset_id = {last_changeset_id}, last_seen_at_micros = {last_seen_at_micros}, retention_blocking = 1, status = 'active'", - table = crate::sync::SHAPE_CLIENTS_TABLE, - shape_id = sql_text_literal(&shape.shape_id), - tenant_id = sql_text_literal(&ack.tenant_id), - client_replica_id = sql_text_literal(&ack.client_replica_id), - subject_id = sql_text_literal(&ack.subject_id), - session_id = sql_nullable_text_literal(ack.session_id.as_deref()), - last_ack_sequence = ack.shape_sequence, - last_ack_watermark = ack.source_high_watermark, - last_changeset_id = sql_nullable_text_literal(ack.changeset_id.as_deref()), - last_seen_at_micros = now, - ); - let _ = self.execute(&sql)?; - self.sync_shape_clients()? - .into_iter() - .find(|client| { - client.shape_id == shape.shape_id - && client.client_replica_id == ack.client_replica_id - }) - .ok_or_else(|| DbError::internal("sync shape client missing after ack")) + working.rebuild_stale_indexes(self.inner.config.page_size)?; + let result = working.execute_statement(statement, params, self.inner.config.page_size)?; + *runtime = working; + *persistent_changed |= !temp_only; + *indexes_maybe_stale = true; + Ok(result) } - pub fn sync_relay_status( + /// Attempts to materialize *only* the tables referenced by `statement`. + /// + /// Returns `Ok(true)` when statement analysis was conservatively + /// exhaustive and the targeted load succeeded — the caller can then + /// safely skip `ensure_all_tables_loaded()`. Returns `Ok(false)` when + /// the statement contains shapes the analyzer can't fully resolve + /// (CTEs, subqueries, VALUES queries, many DDL shapes, …); the + /// caller must fall back to loading all tables. + /// + /// Per ADR 0143 Phase B + the rubber-duck plan critique on + /// 2026-04-22: only a strict whitelist is treated as targeted-safe. + fn ensure_tables_loaded_for_statement_at_snapshot( &self, - relay_id: Option<&str>, - production_mode: bool, - secure_transport_required: bool, - insecure_override_enabled: bool, - started_at_micros: Option, - ) -> Result { - let status = self.sync_status()?; - let active_sessions = self - .sync_relay_sessions()? - .into_iter() - .filter(|session| session.ended_at_micros.is_none() && session.status == "started") - .count() as u64; - Ok(SyncRelayStatus { - relay_id: relay_id.unwrap_or("relay-local").to_string(), - protocol_version: crate::sync::SYNC_RELAY_PROTOCOL_VERSION, - database_replica_id: status.replica_id, - production_mode, - secure_transport_required, - insecure_override_enabled, - active_sessions, - active_streams: 0, - started_at_micros: started_at_micros.unwrap_or_else(current_time_micros), - }) - } - - pub fn sync_relay_sessions(&self) -> Result> { - self.ensure_sync_tables()?; - let sql = format!( - "SELECT session_id, tenant_id, subject_id, subject_kind, request_id, operation, scope_name, shape_id, started_at_micros, ended_at_micros, status, error, rows_seen, bytes_seen FROM {} ORDER BY started_at_micros, session_id", - crate::sync::RELAY_SESSIONS_TABLE, - ); - match self.execute(&sql) { - Ok(result) => result - .rows() - .iter() - .map(sync_relay_session_from_row) - .collect(), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(Vec::new()) - } else { - Err(error) - } - } + statement: &SqlStatement, + snapshot_lsn: Option, + ) -> Result { + let names = { + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + let Some(base_tables) = + self.safe_referenced_base_tables_in_runtime(&runtime, statement) + else { + return Ok(false); + }; + base_tables + }; + let should_load_extension_catalog = self.inner.config.extension_unsigned_development_mode + || !self.inner.config.extension_trust_anchors.is_empty(); + if names.is_empty() && !should_load_extension_catalog { + return Ok(true); } - } - - pub fn sync_start_relay_session( - &self, - principal: &SyncPrincipal, - operation: &str, - scope_name: Option<&str>, - shape_id: Option<&str>, - ) -> Result { - self.ensure_sync_tables()?; - principal.validate()?; - let started_at_micros = current_time_micros(); - let session_id = principal.session_id.clone(); - let sql = format!( - "INSERT INTO {table} (session_id, tenant_id, subject_id, subject_kind, request_id, operation, scope_name, shape_id, started_at_micros, ended_at_micros, status, error, rows_seen, bytes_seen) VALUES ({session_id}, {tenant_id}, {subject_id}, {subject_kind}, {request_id}, {operation}, {scope_name}, {shape_id}, {started_at_micros}, NULL, 'started', NULL, 0, 0) ON CONFLICT (session_id) DO UPDATE SET tenant_id = {tenant_id}, subject_id = {subject_id}, subject_kind = {subject_kind}, request_id = {request_id}, operation = {operation}, scope_name = {scope_name}, shape_id = {shape_id}, started_at_micros = {started_at_micros}, ended_at_micros = NULL, status = 'started', error = NULL", - table = crate::sync::RELAY_SESSIONS_TABLE, - session_id = sql_text_literal(&session_id), - tenant_id = sql_text_literal(&principal.tenant_id), - subject_id = sql_text_literal(&principal.subject_id), - subject_kind = sql_text_literal(principal.subject_kind.as_str()), - request_id = sql_text_literal(&principal.request_id), - operation = sql_text_literal(operation), - scope_name = sql_nullable_text_literal(scope_name), - shape_id = sql_nullable_text_literal(shape_id), - started_at_micros = started_at_micros, - ); - let _ = self.execute(&sql)?; - self.sync_relay_sessions()? - .into_iter() - .find(|session| session.session_id == session_id) - .ok_or_else(|| DbError::internal("sync relay session missing after start")) - } - - pub fn sync_finish_relay_session( - &self, - session_id: &str, - status: &str, - error: Option<&str>, - rows_seen: u64, - bytes_seen: u64, - ) -> Result<()> { - self.ensure_sync_tables()?; - let sql = format!( - "UPDATE {table} SET ended_at_micros = {ended_at_micros}, status = {status}, error = {error}, rows_seen = {rows_seen}, bytes_seen = {bytes_seen} WHERE session_id = {session_id}", - table = crate::sync::RELAY_SESSIONS_TABLE, - ended_at_micros = current_time_micros(), - status = sql_text_literal(status), - error = sql_nullable_text_literal(error), - rows_seen = rows_seen, - bytes_seen = bytes_seen, - session_id = sql_text_literal(session_id), - ); - let _ = self.execute(&sql)?; - Ok(()) - } - - pub fn sync_changeset_history(&self) -> Result> { - self.ensure_sync_tables()?; - let sql = format!( - "SELECT changeset_id, source_replica_id, source_kind, scope_name, shape_id, record_count, bytes, created_at_micros, applied_at_micros, outcome, integrity_hash FROM {} ORDER BY created_at_micros, changeset_id", - crate::sync::CHANGESET_HISTORY_TABLE, - ); - match self.execute(&sql) { - Ok(result) => result - .rows() - .iter() - .map(sync_changeset_history_from_row) - .collect(), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(Vec::new()) - } else { - Err(error) - } - } + let mut names_refs: Vec<&str> = names.iter().map(|s: &String| s.as_str()).collect(); + if should_load_extension_catalog { + names_refs.extend(crate::extensions::extension_catalog_table_names()); } + self.ensure_tables_loaded_at_snapshot(&names_refs, snapshot_lsn)?; + Ok(true) } - fn sync_authorize_scope( - &self, - principal: Option<&SyncPrincipal>, - scope_name: &str, - ) -> Result<()> { - if let Some(principal) = principal { - if !principal.allows_scope(scope_name) { - return Err(DbError::sql(format!( - "SCOPE_UNAUTHORIZED: principal '{}' cannot access scope '{}'", - principal.subject_id, scope_name - ))); - } - } - Ok(()) + /// Materializes all tables that were deferred during `Db::open`. + /// + /// Fast path (no deferred tables): one read-lock check on the engine. + /// Slow path: drops the read lock, takes a write lock, loads all deferred + /// tables and rebuilds indexes, then releases. + fn ensure_all_tables_loaded(&self) -> Result<()> { + self.ensure_all_tables_loaded_at_snapshot(None) } - fn sync_authorize_shape( - &self, - principal: Option<&SyncPrincipal>, - shape: &SyncShape, - ) -> Result<()> { - let Some(principal) = principal else { - return Ok(()); - }; - if !principal.tenant_id.eq_ignore_ascii_case(&shape.tenant_id) { - return Err(DbError::sql(format!( - "AUTH_FORBIDDEN: shape '{}' belongs to tenant '{}'", - shape.shape_id, shape.tenant_id - ))); - } - if !principal.allows_shape(&shape.shape_id) { - return Err(DbError::sql(format!( - "AUTH_FORBIDDEN: principal '{}' cannot access shape '{}'", - principal.subject_id, shape.shape_id - ))); - } - if !shape.allowed_subjects.is_empty() - && !shape - .allowed_subjects - .iter() - .any(|subject| subject == "*" || subject == &principal.subject_id) - { - return Err(DbError::sql(format!( - "AUTH_FORBIDDEN: subject '{}' is not allowed for shape '{}'", - principal.subject_id, shape.shape_id - ))); - } - if !shape.allowed_roles.is_empty() - && !principal.roles.iter().any(|role| { - shape - .allowed_roles - .iter() - .any(|allowed| allowed == "*" || allowed == role) - }) + fn ensure_all_tables_loaded_at_snapshot(&self, snapshot_lsn: Option) -> Result<()> { { - return Err(DbError::sql(format!( - "AUTH_FORBIDDEN: principal '{}' lacks a role for shape '{}'", - principal.subject_id, shape.shape_id - ))); - } - Ok(()) - } - - fn sync_create_shape_snapshot_changeset( - &self, - shape: &SyncShape, - scope: &SyncScope, - principal: Option<&SyncPrincipal>, - ) -> Result { - let created_at_micros = current_time_micros(); - let tooling = self.get_tooling_metadata()?; - let runtime = self.runtime_for_metadata_inspection()?; - let schema_cookie = runtime.catalog.schema_cookie; - let source_replica_id = self - .sync_status() - .ok() - .and_then(|status| status.replica_id) - .unwrap_or_else(|| "snapshot".to_string()); - let mut records = Vec::new(); - let mut origin_sequence = 1u64; - for table_name in &scope.include_tables { - let table = runtime.catalog.table(table_name).ok_or_else(|| { - DbError::sql(format!("sync scope table '{table_name}' does not exist")) - })?; - let column_sql = table - .columns - .iter() - .map(|column| sql_identifier(&column.name)) - .collect::>() - .join(", "); - let order_by = table - .primary_key_columns - .iter() - .map(|column| sql_identifier(column)) - .collect::>() - .join(", "); - let where_sql = scope - .row_filter - .as_ref() - .map(|filter| format!(" WHERE {filter}")) - .unwrap_or_default(); - let sql = format!( - "SELECT {column_sql} FROM {}{where_sql} ORDER BY {order_by}", - sql_identifier(&table.name) - ); - let result = self.execute(&sql)?; - for row in result.rows() { - let after = crate::sync::build_after_json(table, row.values()); - let primary_key = crate::sync::build_primary_key_json(table, row.values()); - records.push(SyncChangesetRecord { - record_version: 1, - table: table.name.clone(), - operation: "insert".to_string(), - primary_key, - origin_replica_id: source_replica_id.clone(), - origin_sequence, - transaction_id: format!("shape-snapshot:{}:{origin_sequence}", shape.shape_id), - transaction_lsn: origin_sequence, - schema_cookie, - before_hash: None, - before: None, - after: Some(after), - column_mask: table - .columns - .iter() - .map(|column| column.name.clone()) - .collect(), - tombstone: false, - conflict_metadata: None, - }); - origin_sequence += 1; - if records.len() as u64 > shape.max_records { - return Err(DbError::sql( - "BATCH_TOO_LARGE: shape snapshot exceeds max_records", - )); - } + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + if !runtime.has_deferred_tables() { + return Ok(()); } } - let high_watermark = self.sync_integrity_report()?.last_sequence.unwrap_or(0); - let mut changeset = SyncChangeset { - changeset_version: crate::sync::SYNC_CHANGESET_VERSION, - changeset_id: sync_changeset_id( - "shape_snapshot", - &source_replica_id, - created_at_micros, - records.len(), - ), - source_replica_id, - source_kind: crate::sync::SyncChangesetSourceKind::Snapshot, - tenant_id: Some( - principal - .map(|principal| principal.tenant_id.clone()) - .unwrap_or_else(|| shape.tenant_id.clone()), - ), - scope_name: Some(scope.name.clone()), - shape_id: Some(shape.shape_id.clone()), - base_kind: "snapshot".to_string(), - base_checkpoint: None, - base_branch: None, - base_snapshot: Some(format!("shape:{}", shape.shape_id)), - start_checkpoint: records.first().map(|record| record.origin_sequence), - end_checkpoint: records.last().map(|record| record.origin_sequence), - source_high_watermark: Some(high_watermark), - schema_fingerprint: tooling.schema_fingerprint, - schema_cookie, - sync_contract_version: crate::sync::SYNC_CONTRACT_VERSION, - query_contract_fingerprint: None, - producer_capabilities: SyncChangesetCapabilities::default(), - limits: SyncChangesetLimits::default(), - records, - conflict_policy_hint: None, - created_at_micros, - integrity_hash: None, - }; - self.sync_finalize_changeset(&mut changeset, None)?; - self.sync_record_changeset_history(&changeset, "created", None)?; - Ok(changeset) - } - - fn sync_finalize_changeset( - &self, - changeset: &mut SyncChangeset, - max_bytes: Option, - ) -> Result<()> { - changeset.limits.record_count = changeset.records.len() as u64; - changeset.limits.uncompressed_bytes = 0; - changeset.integrity_hash = None; - let bytes = serde_json::to_vec(changeset) - .map_err(|error| DbError::internal(format!("failed to serialize changeset: {error}")))? - .len() as u64; - if max_bytes.is_some_and(|limit| bytes > limit) { - return Err(DbError::sql(format!( - "BATCH_TOO_LARGE: changeset is {bytes} bytes" - ))); + let mut runtime = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + if let Some(snapshot_lsn) = snapshot_lsn { + if self.inner.config.paged_row_storage { + runtime.load_deferred_table_row_sources_at_snapshot( + &self.inner.pager, + &self.inner.wal, + self.inner.config.page_size, + snapshot_lsn, + ) + } else { + runtime.load_deferred_tables_at_snapshot( + &self.inner.pager, + &self.inner.wal, + self.inner.config.page_size, + snapshot_lsn, + ) + } + } else if self.inner.config.paged_row_storage { + runtime.load_deferred_table_row_sources( + &self.inner.pager, + &self.inner.wal, + self.inner.config.page_size, + ) + } else { + runtime.load_deferred_tables( + &self.inner.pager, + &self.inner.wal, + self.inner.config.page_size, + ) } - changeset.limits.uncompressed_bytes = bytes; - let hash = self.sync_changeset_integrity_hash(changeset)?; - changeset.integrity_hash = Some(hash); - Ok(()) } - fn sync_validate_changeset_envelope(&self, changeset: &SyncChangeset) -> Result<()> { - if changeset.changeset_version != crate::sync::SYNC_CHANGESET_VERSION { - return Err(DbError::sql(format!( - "CHANGESET_UNSUPPORTED: unsupported changeset version {}", - changeset.changeset_version - ))); - } - if changeset.sync_contract_version != crate::sync::SYNC_CONTRACT_VERSION { - return Err(DbError::sql(format!( - "CHANGESET_UNSUPPORTED: unsupported sync contract version {}", - changeset.sync_contract_version - ))); - } - if changeset.changeset_id.trim().is_empty() { - return Err(DbError::sql("CHANGESET_INVALID: changeset_id is required")); - } - let Some(expected_hash) = changeset.integrity_hash.as_deref() else { - return Err(DbError::sql( - "CHANGESET_INVALID: integrity_hash is required", - )); - }; - let actual_hash = self.sync_changeset_integrity_hash(changeset)?; - if expected_hash != actual_hash { - return Err(DbError::sql( - "CHANGESET_INVALID: integrity_hash does not match payload", - )); - } - for (index, record) in changeset.records.iter().enumerate() { - if record.record_version != 1 { - return Err(DbError::sql(format!( - "CHANGESET_UNSUPPORTED: record {index} uses version {}", - record.record_version - ))); - } - match record.operation.as_str() { - "insert" | "update" if record.after.is_none() => { - return Err(DbError::sql(format!( - "CHANGESET_INVALID: record {index} operation '{}' requires after image", - record.operation - ))); - } - "insert" | "update" | "delete" => {} - other => { - return Err(DbError::sql(format!( - "CHANGESET_INVALID: unsupported record operation '{other}'" - ))); - } - } - } - Ok(()) + fn current_schema_cookie(&self) -> Result { + let page = self.read_page(page::HEADER_PAGE_ID)?; + let mut bytes = [0_u8; storage::header::DB_HEADER_SIZE]; + bytes.copy_from_slice(&page[..storage::header::DB_HEADER_SIZE]); + Ok(DatabaseHeader::decode(&bytes)?.schema_cookie) } - fn sync_check_changeset_compatibility(&self, changeset: &SyncChangeset) -> Result<()> { - let tooling = self.get_tooling_metadata()?; - if tooling.schema_fingerprint != changeset.schema_fingerprint { - return Err(DbError::sql(format!( - "SCHEMA_INCOMPATIBLE: local schema fingerprint {} does not match changeset {}", - tooling.schema_fingerprint, changeset.schema_fingerprint - ))); - } - let runtime = self.runtime_for_metadata_inspection()?; - if runtime.catalog.schema_cookie != changeset.schema_cookie { - return Err(DbError::sql(format!( - "SCHEMA_INCOMPATIBLE: local schema_cookie {} does not match changeset {}", - runtime.catalog.schema_cookie, changeset.schema_cookie - ))); - } - Ok(()) + fn current_schema_cookie_at_snapshot(&self, snapshot_lsn: u64) -> Result { + Self::schema_cookie_at_storage_snapshot(&self.inner.pager, &self.inner.wal, snapshot_lsn) } - fn sync_changeset_integrity_hash(&self, changeset: &SyncChangeset) -> Result { - let mut clone = changeset.clone(); - clone.integrity_hash = None; - let bytes = serde_json::to_vec(&clone).map_err(|error| { - DbError::internal(format!("failed to serialize changeset: {error}")) - })?; - let digest = Sha256::digest(&bytes); - Ok(format!("sha256:{}", hex_encode(&digest))) + fn schema_cookie_at_storage_snapshot( + pager: &PagerHandle, + wal: &WalHandle, + snapshot_lsn: u64, + ) -> Result { + let mut bytes = [0_u8; storage::header::DB_HEADER_SIZE]; + if let Some(wal_page) = + wal.read_page_at_snapshot(pager, page::HEADER_PAGE_ID, snapshot_lsn)? + { + bytes.copy_from_slice(&wal_page[..storage::header::DB_HEADER_SIZE]); + } else { + let page = pager.read_page(page::HEADER_PAGE_ID)?; + bytes.copy_from_slice(&page[..storage::header::DB_HEADER_SIZE]); + } + Ok(DatabaseHeader::decode(&bytes)?.schema_cookie) } - fn sync_record_changeset_history( + fn validate_prepared_schema_cookie( &self, - changeset: &SyncChangeset, - outcome: &str, - applied_at_micros: Option, + prepared: &PreparedStatement, + schema_cookie: u32, + temp_schema_cookie: u32, ) -> Result<()> { - self.ensure_sync_tables()?; - let sql = format!( - "INSERT INTO {table} (changeset_id, source_replica_id, source_kind, scope_name, shape_id, record_count, bytes, created_at_micros, applied_at_micros, outcome, integrity_hash) VALUES ({changeset_id}, {source_replica_id}, {source_kind}, {scope_name}, {shape_id}, {record_count}, {bytes}, {created_at_micros}, {applied_at_micros}, {outcome}, {integrity_hash}) ON CONFLICT (changeset_id) DO UPDATE SET applied_at_micros = COALESCE({applied_at_micros}, applied_at_micros), outcome = {outcome}, integrity_hash = {integrity_hash}", - table = crate::sync::CHANGESET_HISTORY_TABLE, - changeset_id = sql_text_literal(&changeset.changeset_id), - source_replica_id = sql_text_literal(&changeset.source_replica_id), - source_kind = sql_text_literal(changeset.source_kind.as_str()), - scope_name = sql_nullable_text_literal(changeset.scope_name.as_deref()), - shape_id = sql_nullable_text_literal(changeset.shape_id.as_deref()), - record_count = changeset.records.len(), - bytes = changeset.limits.uncompressed_bytes, - created_at_micros = changeset.created_at_micros, - applied_at_micros = applied_at_micros - .map(|value| value.to_string()) - .unwrap_or_else(|| "NULL".to_string()), - outcome = sql_text_literal(outcome), - integrity_hash = sql_nullable_text_literal(changeset.integrity_hash.as_deref()), - ); - let _ = self.execute(&sql)?; - Ok(()) - } - - pub fn sync_conflict_policy(&self) -> Result { - let default_policy = self - .sync_read_metadata("conflict_policy")? - .map(|value| SyncConflictPolicy::from_str(&value)) - .transpose()? - .unwrap_or_default(); - let origin_priority = match self.sync_read_metadata("conflict_origin_priority")? { - Some(value) => serde_json::from_str::>(&value).map_err(|error| { - DbError::sql(format!( - "invalid sync conflict origin priority metadata: {error}" - )) - })?, - None => Vec::new(), - }; - Ok(SyncConflictPolicyConfig { - default_policy, - origin_priority, - }) + if schema_cookie == prepared.schema_cookie + && temp_schema_cookie == prepared.temp_schema_cookie + { + return Ok(()); + } + Err(DbError::sql( + "prepared statement is no longer valid because the schema changed", + )) } - pub fn sync_set_conflict_policy( + fn validate_prepared_against_connection_state( &self, - policy: SyncConflictPolicy, - origin_priority: &[&str], + prepared: &PreparedStatement, ) -> Result<()> { - self.ensure_sync_tables()?; - let origin_priority = origin_priority - .iter() - .map(|value| value.trim()) - .map(|value| { - if value.is_empty() { - Err(DbError::sql( - "sync conflict origin priority entries must not be empty", - )) - } else { - Ok(value.to_string()) - } - }) - .collect::>>()?; - self.sync_upsert_metadata("conflict_policy", policy.as_str())?; - self.sync_upsert_metadata( - "conflict_origin_priority", - &serde_json::to_string(&origin_priority).map_err(|error| { - DbError::internal(format!( - "failed to serialize sync conflict origin priority: {error}" - )) - })?, - )?; - Ok(()) - } - - pub fn sync_import_batch(&self, batch: &SyncChangeBatch) -> Result { - let policy = self.sync_conflict_policy()?.default_policy; - self.sync_import_batch_with_policy(batch, policy) + let temp_schema_cookie = self + .inner + .temp_state + .lock() + .map_err(|_| DbError::internal("temp schema lock poisoned"))? + .schema_cookie; + self.validate_prepared_schema_cookie( + prepared, + self.inner.catalog.schema_cookie()?, + temp_schema_cookie, + ) } - pub fn sync_import_batch_with_policy( - &self, - batch: &SyncChangeBatch, - policy: SyncConflictPolicy, - ) -> Result { - batch.validate()?; - self.ensure_sync_tables()?; + fn build_sql_txn_state(&self) -> Result { + let (snapshot_reader, current_lsn, current_epoch) = self.begin_sql_snapshot()?; - let runtime = self.runtime_for_metadata_inspection()?; - let schema_cookie = runtime.catalog.schema_cookie; - let local_replica_id = self + let mut runtime = self .inner - .sync_ctx - .replica_id() - .or_else(|| self.sync_read_metadata("replica_id").ok().flatten()); - let batch_source_replica_id = batch.source_replica_id.as_deref(); - let batch_watermark = batch.source_high_watermark.or(batch.last_sequence); - let current_peer_watermark = match batch_source_replica_id { - Some(replica_id) => self.sync_peer_watermark(replica_id)?, - None => None, - }; - - if let Some(local_replica_id) = local_replica_id.as_deref() { - if batch_source_replica_id == Some(local_replica_id) { - return Err(DbError::sql(format!( - "cannot import batch from same replica '{}'", - local_replica_id - ))); - } - } - - let _suppress_capture = self.inner.sync_ctx.suppress_capture(); - struct SyncImportTransaction<'a>(&'a Db, bool); - impl<'a> SyncImportTransaction<'a> { - fn new(db: &'a Db) -> Result { - db.begin_transaction()?; - Ok(Self(db, true)) - } - fn commit(mut self) -> Result<()> { - self.1 = false; - self.0.commit_transaction()?; - Ok(()) - } - } - impl Drop for SyncImportTransaction<'_> { - fn drop(&mut self) { - if self.1 { - let _ = self.0.rollback_transaction(); - } - } - } - - if let Some(batch_watermark) = batch_watermark { - if current_peer_watermark.is_some_and(|watermark| batch_watermark <= watermark) { - if let Some(replica_id) = batch_source_replica_id { - let watermark = current_peer_watermark - .map_or(batch_watermark, |current| current.max(batch_watermark)); - self.sync_upsert_metadata( - &peer_watermark_key(replica_id), - &watermark.to_string(), - )?; - } - return Ok(SyncImportSummary { - seen: batch.record_count, - applied: 0, - skipped: batch.record_count, - conflicted: 0, - }); - } - } - - let tx = SyncImportTransaction::new(self)?; - let mut applied = 0usize; - let mut skipped = 0usize; - let mut conflicted = 0usize; - let mut stop_conflict: Option<(SyncJournalRecord, SyncConflictRecordData)> = None; - - for record in &batch.records { - if let Some(local_replica_id) = local_replica_id.as_deref() { - if record.replica_id == local_replica_id { - return Err(DbError::sql(format!( - "cannot import record from same replica '{}'", - local_replica_id - ))); - } - } - - if let Some(watermark) = current_peer_watermark { - if record.sequence <= watermark { - skipped += 1; - continue; - } - } - - if record.schema_version != 1 { - return Err(DbError::sql(format!( - "unsupported sync record schema version {}", - record.schema_version - ))); - } - - if record.schema_cookie != schema_cookie { - return Err(DbError::sql(format!( - "schema mismatch for table '{}': record has schema_cookie {} but local schema is {}", - record.table, record.schema_cookie, schema_cookie - ))); - } - - let table = runtime - .catalog - .table(&record.table) - .ok_or_else(|| DbError::sql(format!("unknown table '{}'", record.table)))?; - if crate::sync::is_internal_table_name(&table.name) { - return Err(DbError::sql(format!( - "cannot import into internal table '{}'", - table.name - ))); - } - let marker_key = imported_record_key(&record.replica_id, record.sequence); - if self.sync_read_metadata(&marker_key)?.is_some() { - skipped += 1; - continue; - } - - let outcome = self.sync_apply_import_record(batch, record, table, &policy)?; - match outcome { - SyncImportRecordOutcome::Applied => { - self.sync_upsert_metadata(&marker_key, "applied")?; - applied += 1; - } - SyncImportRecordOutcome::Conflict(conflict) => { - if matches!(policy, SyncConflictPolicy::Stop) { - stop_conflict = Some((record.clone(), conflict)); - break; - } - self.record_sync_conflict_with_data(batch, record, &conflict)?; - conflicted += 1; - } - SyncImportRecordOutcome::Resolved(conflict) => { - self.sync_upsert_metadata(&marker_key, "applied")?; - self.record_sync_conflict_with_data(batch, record, &conflict)?; - applied += 1; - conflicted += 1; - } - } - } - - if let Some((record, conflict)) = stop_conflict { - drop(tx); - let conflict_id = self.record_sync_conflict_with_data(batch, &record, &conflict)?; - return Err(DbError::sql(format!( - "sync import stopped on conflict {}", - conflict_id - ))); - } - - if let (Some(replica_id), Some(batch_watermark)) = - (batch_source_replica_id, batch_watermark) - { - let watermark = current_peer_watermark - .map_or(batch_watermark, |current| current.max(batch_watermark)); - self.sync_upsert_metadata(&peer_watermark_key(replica_id), &watermark.to_string())?; - } - - crate::reactive::with_change_source(ChangeSource::SyncApply, || tx.commit())?; - Ok(SyncImportSummary { - seen: batch.record_count, - applied, - skipped, - conflicted, + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))? + .clone(); + self.apply_temp_state_to_runtime(&mut runtime)?; + self.configure_runtime_sync_capture(&mut runtime)?; + Ok(SqlTxnState { + runtime, + snapshot_reader, + base_lsn: current_lsn, + base_checkpoint_epoch: current_epoch, + persistent_changed: false, + indexes_maybe_stale: false, + prepared_insert_runtime_cache: HashMap::new(), + savepoints: Vec::new(), }) } - pub fn sync_import_records(&self, records: &[SyncJournalRecord]) -> Result { - let batch = SyncChangeBatch::from_records(records.to_vec())?; - self.sync_import_batch(&batch) - } - - pub fn sync_peer_watermark(&self, replica_id: &str) -> Result> { - match self.sync_read_metadata(&peer_watermark_key(replica_id))? { - Some(value) => value - .parse::() - .map(Some) - .map_err(|error| DbError::sql(format!("invalid peer watermark value: {error}"))), - None => Ok(None), + fn execute_prepared_in_state( + &self, + prepared: &PreparedStatement, + params: &[Value], + state: &mut SqlTxnState, + ) -> Result { + if !Arc::ptr_eq(&self.inner, &prepared.db.inner) { + return Err(DbError::transaction( + "prepared statement belongs to a different database handle", + )); } - } - - pub fn sync_peer_out_watermark(&self, peer_name: &str) -> Result> { - match self.sync_read_metadata(&peer_out_watermark_key(peer_name))? { - Some(value) => value.parse::().map(Some).map_err(|error| { - DbError::sql(format!("invalid peer outbound watermark value: {error}")) - }), - None => Ok(None), + self.validate_prepared_schema_cookie( + prepared, + state.runtime.catalog.schema_cookie, + state.runtime.temp_schema_cookie, + )?; + if prepared.read_only { + if let Some(result) = self.try_execute_prepared_inspection_query(prepared, params)? { + return Ok(result); + } + let snapshot_lsn = state.snapshot_lsn(); + return self.execute_read_in_runtime_state( + prepared.statement.as_ref(), + params, + &mut state.runtime, + snapshot_lsn, + &mut state.indexes_maybe_stale, + ); } - } - - pub fn sync_set_peer_out_watermark(&self, peer_name: &str, watermark: u64) -> Result<()> { - self.ensure_sync_tables()?; - self.sync_upsert_metadata(&peer_out_watermark_key(peer_name), &watermark.to_string()) - } - - pub fn sync_conflicts(&self) -> Result> { - self.ensure_sync_tables()?; - let sql = format!( - "SELECT * FROM {} WHERE resolved = 0 ORDER BY conflict_id", - crate::sync::CONFLICTS_TABLE - ); - match self.execute(&sql) { - Ok(result) => result.rows().iter().map(sync_conflict_from_row).collect(), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(Vec::new()) - } else { - Err(error) - } + let snapshot_lsn = state.snapshot_lsn(); + if let Some(result) = self.try_execute_prepared_insert_in_runtime_state( + prepared, + params, + &mut state.runtime, + snapshot_lsn, + &mut state.persistent_changed, + &mut state.indexes_maybe_stale, + &mut state.prepared_insert_runtime_cache, + )? { + return Ok(result); + } + if let Some(prepared_update) = prepared.prepared_update.as_deref() { + if let Some(result) = self.try_execute_prepared_update_in_runtime_state( + prepared, + prepared_update, + params, + &mut state.runtime, + snapshot_lsn, + &mut state.persistent_changed, + &mut state.indexes_maybe_stale, + )? { + return Ok(result); } } - } - - pub fn sync_conflicts_all(&self) -> Result> { - self.ensure_sync_tables()?; - let sql = format!( - "SELECT * FROM {} ORDER BY conflict_id", - crate::sync::CONFLICTS_TABLE - ); - match self.execute(&sql) { - Ok(result) => result.rows().iter().map(sync_conflict_from_row).collect(), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(Vec::new()) - } else { - Err(error) - } + if let Some(prepared_delete) = prepared.prepared_delete.as_deref() { + if let Some(result) = self.try_execute_prepared_delete_in_runtime_state( + prepared, + prepared_delete, + params, + &mut state.runtime, + snapshot_lsn, + &mut state.persistent_changed, + &mut state.indexes_maybe_stale, + )? { + return Ok(result); } } + self.execute_write_in_runtime_state( + prepared.statement.as_ref(), + params, + &mut state.runtime, + snapshot_lsn, + &mut state.persistent_changed, + &mut state.indexes_maybe_stale, + ) } - pub fn sync_conflict(&self, conflict_id: i64) -> Result> { - self.ensure_sync_tables()?; - let sql = format!( - "SELECT * FROM {} WHERE conflict_id = {}", - crate::sync::CONFLICTS_TABLE, - conflict_id - ); - match self.execute(&sql) { - Ok(result) => Ok(result - .rows() - .first() - .map(sync_conflict_from_row) - .transpose()?), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(None) - } else { - Err(error) - } + fn execute_prepared_in_exclusive_state( + &self, + prepared: &PreparedStatement, + params: &[Value], + state: &mut ExclusiveSqlTxnState<'_>, + ) -> Result { + if !Arc::ptr_eq(&self.inner, &prepared.db.inner) { + return Err(DbError::transaction( + "prepared statement belongs to a different database handle", + )); + } + Self::flush_exclusive_prepared_insert_next_row_id(state)?; + self.validate_prepared_schema_cookie( + prepared, + state.runtime.catalog.schema_cookie, + state.runtime.temp_schema_cookie, + )?; + if prepared.read_only { + if let Some(result) = self.try_execute_prepared_inspection_query(prepared, params)? { + return Ok(result); } + let snapshot_lsn = state.snapshot_lsn(); + return self.execute_read_in_runtime_state( + prepared.statement.as_ref(), + params, + &mut state.runtime, + snapshot_lsn, + &mut state.indexes_maybe_stale, + ); } - } - - pub fn sync_resolve_conflict_keep_local( - &self, - conflict_id: i64, - resolved_by: Option<&str>, - note: Option<&str>, - ) -> Result { - self.sync_update_conflict_resolution( - conflict_id, - Some("keep_local"), - resolved_by, - note, - Some(current_time_micros()), + let snapshot_lsn = state.snapshot_lsn(); + if let Some(result) = self.try_execute_prepared_insert_in_runtime_state( + prepared, + params, + &mut state.runtime, + snapshot_lsn, + &mut state.persistent_changed, + &mut state.indexes_maybe_stale, + &mut state.prepared_insert_runtime_cache, + )? { + return Ok(result); + } + self.execute_write_in_runtime_state( + prepared.statement.as_ref(), + params, + &mut state.runtime, + snapshot_lsn, + &mut state.persistent_changed, + &mut state.indexes_maybe_stale, ) } - pub fn sync_resolve_conflict_apply_remote( + fn execute_prepared_in_exclusive_state_mut( &self, - conflict_id: i64, - resolved_by: Option<&str>, - note: Option<&str>, - ) -> Result { - let Some(conflict) = self.sync_conflict(conflict_id)? else { - return Ok(false); - }; - let record: SyncJournalRecord = serde_json::from_value(conflict.remote_record_json.clone()) - .map_err(|error| { - DbError::corruption(format!( - "malformed sync conflict remote_record_json: {error}" - )) - })?; - let batch = SyncChangeBatch::from_records(vec![record.clone()])?; - let policy = SyncConflictPolicy::LastWriterWins; - let _suppress_capture = self.inner.sync_ctx.suppress_capture(); - let tx = { - self.begin_transaction()?; - struct Tx<'a>(&'a Db, bool); - impl<'a> Drop for Tx<'a> { - fn drop(&mut self) { - if self.1 { - let _ = self.0.rollback_transaction(); - } - } - } - impl<'a> Tx<'a> { - fn commit(mut self) -> Result<()> { - self.1 = false; - self.0.commit_transaction()?; - Ok(()) - } + prepared: &PreparedStatement, + params: &mut [Value], + state: &mut ExclusiveSqlTxnState<'_>, + ) -> Result { + if !prepared.read_only { + if let Some(result) = self + .try_execute_last_prepared_insert_in_exclusive_state_mut(prepared, params, state)? + { + return Ok(result); } - Tx(self, true) - }; - let runtime = self.runtime_for_metadata_inspection()?; - let table = runtime - .catalog - .table(&record.table) - .ok_or_else(|| DbError::sql(format!("unknown table '{}'", record.table)))?; - match self.sync_apply_import_record(&batch, &record, table, &policy)? { - SyncImportRecordOutcome::Applied => { - self.sync_upsert_metadata( - &imported_record_key(&record.replica_id, record.sequence), - "applied", - )?; - self.sync_update_conflict_resolution( - conflict_id, - Some("apply_remote"), - resolved_by, - note, - Some(current_time_micros()), - )?; - tx.commit()?; - Ok(true) + } + Self::flush_exclusive_prepared_insert_next_row_id(state)?; + if !Arc::ptr_eq(&self.inner, &prepared.db.inner) { + return Err(DbError::transaction( + "prepared statement belongs to a different database handle", + )); + } + self.validate_prepared_schema_cookie( + prepared, + state.runtime.catalog.schema_cookie, + state.runtime.temp_schema_cookie, + )?; + if prepared.read_only { + if let Some(result) = self.try_execute_prepared_inspection_query(prepared, params)? { + return Ok(result); } - SyncImportRecordOutcome::Resolved(_) => { - self.sync_upsert_metadata( - &imported_record_key(&record.replica_id, record.sequence), - "applied", - )?; - self.sync_update_conflict_resolution( - conflict_id, - Some("apply_remote"), - resolved_by, - note, - Some(current_time_micros()), - )?; - tx.commit()?; - Ok(true) + let snapshot_lsn = state.snapshot_lsn(); + return self.execute_read_in_runtime_state( + prepared.statement.as_ref(), + params, + &mut state.runtime, + snapshot_lsn, + &mut state.indexes_maybe_stale, + ); + } + let snapshot_lsn = state.snapshot_lsn(); + if let Some(result) = self.try_execute_prepared_insert_in_runtime_state_mut( + prepared, + params, + &mut state.runtime, + snapshot_lsn, + &mut state.persistent_changed, + &mut state.indexes_maybe_stale, + &mut state.prepared_insert_runtime_cache, + &mut state.prepared_insert_last_cache_key, + &mut state.prepared_insert_last_plan, + &mut state.prepared_insert_last_next_row_id, + &mut state.prepared_insert_candidate, + )? { + return Ok(result); + } + if let Some(prepared_update) = prepared.prepared_update.as_deref() { + if let Some(result) = self.try_execute_prepared_update_in_runtime_state( + prepared, + prepared_update, + params, + &mut state.runtime, + snapshot_lsn, + &mut state.persistent_changed, + &mut state.indexes_maybe_stale, + )? { + return Ok(result); } - SyncImportRecordOutcome::Conflict(conflict) => { - let _ = conflict; - Err(DbError::sql(format!( - "cannot apply remote conflict {} because replay now fails", - conflict_id - ))) + } + if let Some(prepared_delete) = prepared.prepared_delete.as_deref() { + if let Some(result) = self.try_execute_prepared_delete_in_runtime_state( + prepared, + prepared_delete, + params, + &mut state.runtime, + snapshot_lsn, + &mut state.persistent_changed, + &mut state.indexes_maybe_stale, + )? { + return Ok(result); } } + self.execute_write_in_runtime_state( + prepared.statement.as_ref(), + params, + &mut state.runtime, + snapshot_lsn, + &mut state.persistent_changed, + &mut state.indexes_maybe_stale, + ) } - pub fn sync_reopen_conflict(&self, conflict_id: i64) -> Result { - self.sync_update_conflict_resolution(conflict_id, None, None, None, None) - } - - pub fn sync_prune_journal_through(&self, sequence: u64) -> Result { - self.sync_prune_journal(sequence, false, false) - .map(|summary| summary.pruned) + fn flush_exclusive_prepared_insert_next_row_id( + state: &mut ExclusiveSqlTxnState<'_>, + ) -> Result<()> { + let Some(next_row_id) = state.prepared_insert_last_next_row_id.take() else { + return Ok(()); + }; + let Some(prepared_insert) = state.prepared_insert_last_plan.as_ref() else { + state.prepared_insert_last_cache_key = None; + return Ok(()); + }; + let Some(table_name) = prepared_insert.catalog_table_name.as_deref() else { + return Ok(()); + }; + let catalog = Arc::make_mut(&mut state.runtime.catalog); + let table = catalog + .tables + .get_mut(table_name) + .ok_or_else(|| DbError::sql(format!("unknown table {}", prepared_insert.table_name)))?; + table.next_row_id = next_row_id; + Ok(()) } - pub fn sync_prune_journal( + #[inline(always)] + fn try_execute_last_prepared_insert_in_exclusive_state_mut( &self, - through: u64, - dry_run: bool, - allow_data_loss: bool, - ) -> Result { - let retention = self.sync_retention_report()?; - let requested_through = through; - if !allow_data_loss && through > retention.safe_prune_through.unwrap_or(0) { - let message = if let Some(lowest_watermark) = - retention.safe_prune_through.map(|value| value + 1) - { - format!( - "cannot prune through {through}; lowest peer watermark is {lowest_watermark}" - ) - } else if retention.blocked_by.is_empty() { - format!("cannot prune through {through}; no peer watermarks are known") - } else { - format!("cannot prune through {through}; lowest peer watermark is 0") - }; - return Err(DbError::sql(message)); - } - - let records = crate::sync::read_journal_records( - self.inner.sync_ctx.journal_path(), - &self.inner.vfs, - 0, - usize::MAX, - )?; - if records.is_empty() { - return Ok(SyncPruneSummary { - requested_through, - effective_through: 0, - pruned: 0, - dry_run, - allow_data_loss, - blocked_by: retention.blocked_by, - }); - } - - let effective_through = records - .last() - .map(|record| record.sequence.min(through)) - .unwrap_or(0); - let total_records = records.len(); - let retained = records - .into_iter() - .filter(|record| record.sequence > through) - .collect::>(); - let pruned = total_records.saturating_sub(retained.len()); - - if dry_run || pruned == 0 { - return Ok(SyncPruneSummary { - requested_through, - effective_through, - pruned, - dry_run, - allow_data_loss, - blocked_by: retention.blocked_by, - }); - } - - let mut buffer = Vec::new(); - for record in &retained { - serde_json::to_writer(&mut buffer, record).map_err(|error| { - DbError::internal(format!("failed to serialize sync journal record: {error}")) - })?; - buffer.push(b'\n'); - } - - if self - .inner - .vfs - .file_exists(self.inner.sync_ctx.journal_path())? - { - let journal_file = self.inner.sync_ctx.journal_file_handle()?; - let journal_file = match journal_file { - Some(file) => file, - None => self.inner.vfs.open( - self.inner.sync_ctx.journal_path(), - OpenMode::OpenExisting, - FileKind::SyncJournal, - )?, - }; - - journal_file.set_len(0)?; - write_all_at(journal_file.as_ref(), 0, &buffer)?; - journal_file.sync_data()?; - self.inner - .sync_ctx - .set_journal_write_offset(buffer.len() as u64)?; + prepared: &PreparedStatement, + params: &mut [Value], + state: &mut ExclusiveSqlTxnState<'_>, + ) -> Result> { + if state.indexes_maybe_stale { + return Ok(None); } - - Ok(SyncPruneSummary { - requested_through, - effective_through, - pruned, - dry_run, - allow_data_loss, - blocked_by: retention.blocked_by, - }) - } - - pub fn sync_set_enabled(&self, enabled: bool) -> Result<()> { - self.ensure_sync_tables()?; - self.sync_upsert_metadata("enabled", if enabled { "true" } else { "false" })?; - self.inner.sync_ctx.set_enabled(enabled); - if enabled { - self.inner.sync_ctx.ensure_journal_open(&self.inner.vfs)?; + let Some(cache_key) = state.prepared_insert_last_cache_key else { + return Ok(None); + }; + if cache_key != Self::prepared_statement_cache_key(prepared) { + return Ok(None); } - Ok(()) - } + let Some(insert_plan) = state.prepared_insert_last_plan.as_ref() else { + return Ok(None); + }; + let Some(cached_next_row_id) = state.prepared_insert_last_next_row_id.as_mut() else { + return Ok(None); + }; - pub fn sync_is_enabled(&self) -> Result { - if self.inner.sync_ctx.is_enabled() { - return Ok(true); - } - let status = self.load_sync_status_from_db()?; - if status.enabled { - self.inner.sync_ctx.set_enabled(true); - self.inner - .sync_ctx - .set_replica_id(&status.replica_id.unwrap_or_default()); - self.inner.sync_ctx.set_next_sequence(status.next_sequence); + let affected = state + .runtime + .execute_prepared_simple_insert_positional_params_in_place_with_cached_next_row_id( + insert_plan.as_ref(), + params, + &mut state.prepared_insert_candidate, + cached_next_row_id, + self.inner.config.page_size, + )?; + if !state.persistent_changed { + state.persistent_changed |= Self::prepared_insert_changes_persistent_table( + &state.runtime, + insert_plan.as_ref(), + ); } - Ok(status.enabled) + Ok(Some(QueryResult::with_affected_rows(affected))) } - fn sync_upsert_metadata(&self, key: &str, value: &str) -> Result<()> { - let sql = format!( - "INSERT INTO {table} (key, value) VALUES ('{k}', '{v}') ON CONFLICT (key) DO UPDATE SET value = '{v}'", - table = crate::sync::METADATA_TABLE, - k = key.replace('\'', "''"), - v = value.replace('\'', "''"), - ); - let _ = self.execute(&sql)?; - Ok(()) - } + fn prepare_batch_in_exclusive_state<'txn, 'db>( + &'db self, + prepared: &'txn PreparedStatement, + param_count: usize, + state: &'txn mut ExclusiveSqlTxnState<'db>, + ) -> Result> { + if !Arc::ptr_eq(&self.inner, &prepared.db.inner) { + return Err(DbError::transaction( + "prepared statement belongs to a different database handle", + )); + } + Self::flush_exclusive_prepared_insert_next_row_id(state)?; + self.validate_prepared_schema_cookie( + prepared, + state.runtime.catalog.schema_cookie, + state.runtime.temp_schema_cookie, + )?; - fn sync_read_metadata(&self, key: &str) -> Result> { - let sql = format!( - "SELECT value FROM {} WHERE key = '{}'", - crate::sync::METADATA_TABLE, - key.replace('\'', "''"), - ); - match self.execute(&sql) { - Ok(result) => { - if let Some(row) = result.rows().first() { - if let Some(val) = row.values().first() { - match val { - Value::Text(s) => return Ok(Some(s.clone())), - _ => return Ok(None), - } - } - } - Ok(None) - } - Err(e) => { - let msg = e.to_string(); - if msg.contains("no such table") || msg.contains("unknown table") { - return Ok(None); - } - Err(e) - } + let mut prepared_insert = None; + let mut direct_positional = false; + if !prepared.read_only && matches!(prepared.statement.as_ref(), SqlStatement::Insert(_)) { + let snapshot_lsn = state.snapshot_lsn(); + prepared_insert = self.prepared_insert_plan_for_runtime_state( + prepared, + &mut state.runtime, + snapshot_lsn, + &mut state.indexes_maybe_stale, + &mut state.prepared_insert_runtime_cache, + )?; + direct_positional = prepared_insert.as_deref().is_some_and(|insert| { + Self::prepared_insert_uses_direct_positional_params(insert, param_count) + }); } + Ok(PreparedStatementBatch { + db: self, + state, + prepared, + prepared_insert, + direct_positional, + prepared_insert_candidate: Vec::new(), + prepared_insert_encoded_values: Vec::new(), + }) } - fn sync_metadata_entries(&self) -> Result> { - let sql = format!("SELECT key, value FROM {}", crate::sync::METADATA_TABLE); - match self.execute(&sql) { - Ok(result) => result - .rows() - .iter() - .map(|row| { - let key = row - .values() - .first() - .and_then(|value| match value { - Value::Text(text) => Some(text.clone()), - _ => None, - }) - .ok_or_else(|| DbError::corruption("malformed sync metadata row"))?; - let value = row - .values() - .get(1) - .and_then(|value| match value { - Value::Text(text) => Some(text.clone()), - _ => None, - }) - .ok_or_else(|| DbError::corruption("malformed sync metadata row"))?; - Ok((key, value)) - }) - .collect(), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(Vec::new()) - } else { - Err(error) - } - } - } + fn execute_statement_in_state( + &self, + _sql: &str, + statement: &crate::sql::ast::Statement, + params: &[Value], + state: &mut SqlTxnState, + ) -> Result { + let snapshot_lsn = state.snapshot_lsn(); + self.execute_write_in_runtime_state( + statement, + params, + &mut state.runtime, + snapshot_lsn, + &mut state.persistent_changed, + &mut state.indexes_maybe_stale, + ) } - fn sync_peer_watermark_entries(&self) -> Result> { - self.sync_metadata_entries()? - .into_iter() - .filter_map(|(key, value)| { - key.strip_prefix("peer_watermark:") - .map(|replica_id| (replica_id.to_string(), value)) - }) - .map(|(replica_id, value)| { - let watermark = value.parse::().map_err(|error| { - DbError::sql(format!( - "invalid peer watermark value for replica '{}': {}", - replica_id, error - )) - })?; - Ok((format!("remote:{replica_id}"), watermark)) - }) - .collect() + fn exclusive_sql_txn_error(&self) -> DbError { + DbError::transaction( + "a SQL transaction handle is active on this database handle; use it until commit or rollback", + ) } fn record_sync_conflict_with_data( @@ -15001,473 +9313,17 @@ impl Db { "NULL".to_string() }, resolved_at_micros = resolved_at_micros, - resolved_by = resolved_by, - resolution_note = resolution_note, - policy_name = policy_name, - local_record_json = conflict - .local_row_json - .as_ref() - .map(|value| sql_text_literal(&value.to_string())) - .unwrap_or_else(|| "NULL".to_string()), - ); - let _ = self.execute(&sql)?; - Ok(conflict_id) - } - - fn sync_capture_local_row_json( - &self, - table: &TableSchema, - primary_key: &serde_json::Map, - ) -> Result> { - let (mut runtime, snapshot_lsn) = self.runtime_for_targeted_row_source_inspection()?; - if let Some(snapshot_lsn) = snapshot_lsn { - self.load_runtime_table_row_sources_at_snapshot( - &mut runtime, - &[table.name.as_str()], - snapshot_lsn, - )?; - } - let Some(source) = runtime.table_row_source(&table.name) else { - return Ok(None); - }; - for row in source.rows() { - let row = row?; - let values = row.values(); - let mut matches = true; - for pk_col in &table.primary_key_columns { - let column = table - .columns - .iter() - .find(|column| column.name == *pk_col) - .ok_or_else(|| { - DbError::sql(format!( - "table '{}' missing primary key column '{}'", - table.name, pk_col - )) - })?; - let json_value = primary_key.get(pk_col).ok_or_else(|| { - DbError::sql(format!( - "missing primary key column '{pk_col}' in record for table '{}'", - table.name - )) - })?; - let expected = json_to_column_value(&table.name, column, json_value)?; - let Some(actual) = values.get( - table - .columns - .iter() - .position(|candidate| candidate.name == *pk_col) - .ok_or_else(|| { - DbError::sql(format!( - "table '{}' missing primary key column '{}'", - table.name, pk_col - )) - })?, - ) else { - matches = false; - break; - }; - if actual != &expected { - matches = false; - break; - } - } - if matches { - return Ok(Some(crate::sync::build_after_json(table, values))); - } - } - Ok(None) - } - - fn sync_apply_import_record( - &self, - _batch: &SyncChangeBatch, - record: &SyncJournalRecord, - table: &TableSchema, - policy: &SyncConflictPolicy, - ) -> Result { - let primary_key = record - .primary_key - .as_object() - .ok_or_else(|| DbError::sql("primary_key must be an object"))?; - let local_row_json = self.sync_capture_local_row_json(table, primary_key)?; - let operation = match record.operation.as_str() { - "insert" => SyncOperation::Insert, - "update" => SyncOperation::Update, - "delete" => SyncOperation::Delete, - other => return Err(DbError::sql(format!("unsupported operation '{other}'"))), - }; - - let remote_wins = match policy { - SyncConflictPolicy::Record | SyncConflictPolicy::Stop => false, - SyncConflictPolicy::LastWriterWins => true, - SyncConflictPolicy::OriginPriority => { - let config = self.sync_conflict_policy()?; - match self - .inner - .sync_ctx - .replica_id() - .or_else(|| self.sync_read_metadata("replica_id").ok().flatten()) - { - Some(local_replica_id) => { - let remote_index = config - .origin_priority - .iter() - .position(|replica| replica == &record.replica_id); - let local_index = config - .origin_priority - .iter() - .position(|replica| replica == &local_replica_id); - matches!((remote_index, local_index), (Some(remote), Some(local)) if remote < local) - } - None => false, - } - } - }; - - let apply_remote_replace = |operation: SyncOperation| -> Result<()> { - let sql = format!( - "DELETE FROM {} WHERE {}", - sql_identifier(&table.name), - table - .primary_key_columns - .iter() - .enumerate() - .map(|(idx, pk_col)| format!("{} = ${}", sql_identifier(pk_col), idx + 1)) - .collect::>() - .join(" AND ") - ); - let mut where_values = Vec::with_capacity(table.primary_key_columns.len()); - for pk_col in &table.primary_key_columns { - let column = table - .columns - .iter() - .find(|column| column.name == *pk_col) - .ok_or_else(|| { - DbError::sql(format!( - "table '{}' missing primary key column '{}'", - table.name, pk_col - )) - })?; - let json_value = primary_key.get(pk_col).ok_or_else(|| { - DbError::sql(format!( - "missing primary key column '{pk_col}' in record for table '{}'", - table.name - )) - })?; - where_values.push(json_to_column_value(&table.name, column, json_value)?); - } - let _ = self.execute_with_params(&sql, &where_values)?; - - if matches!(operation, SyncOperation::Delete) { - return Ok(()); - } - - let after = record - .after - .as_ref() - .ok_or_else(|| DbError::sql("remote record missing after payload"))? - .as_object() - .ok_or_else(|| DbError::sql("remote record after payload must be an object"))?; - let mut columns = Vec::with_capacity(table.columns.len()); - let mut values = Vec::with_capacity(table.columns.len()); - for column in &table.columns { - let json_value = after.get(&column.name).ok_or_else(|| { - DbError::sql(format!( - "missing column '{}' in after payload for table '{}'", - column.name, table.name - )) - })?; - columns.push(sql_identifier(&column.name)); - values.push(json_to_column_value(&table.name, column, json_value)?); - } - let sql = format!( - "INSERT INTO {} ({}) VALUES ({})", - sql_identifier(&table.name), - columns.join(", "), - (1..=values.len()) - .map(|idx| format!("${idx}")) - .collect::>() - .join(", ") - ); - let _ = self.execute_with_params(&sql, &values)?; - Ok(()) - }; - - match operation { - SyncOperation::Insert => { - let after = record - .after - .as_ref() - .ok_or_else(|| DbError::sql("insert record missing after payload"))? - .as_object() - .ok_or_else(|| DbError::sql("after must be an object for insert"))?; - let mut columns = Vec::with_capacity(table.columns.len()); - let mut values = Vec::with_capacity(table.columns.len()); - for column in &table.columns { - let json_value = after.get(&column.name).ok_or_else(|| { - DbError::sql(format!( - "missing column '{}' in after payload for table '{}'", - column.name, table.name - )) - })?; - columns.push(sql_identifier(&column.name)); - values.push(json_to_column_value(&table.name, column, json_value)?); - } - let sql = format!( - "INSERT INTO {} ({}) VALUES ({})", - sql_identifier(&table.name), - columns.join(", "), - (1..=values.len()) - .map(|idx| format!("${idx}")) - .collect::>() - .join(", ") - ); - match self.execute_with_params(&sql, &values) { - Ok(_) => Ok(SyncImportRecordOutcome::Applied), - Err(DbError::Constraint { message }) if remote_wins => { - apply_remote_replace(SyncOperation::Insert)?; - Ok(SyncImportRecordOutcome::Resolved(SyncConflictRecordData { - conflict_type: "insert_insert".to_string(), - message, - local_row_json, - resolution: Some("remote_applied".to_string()), - resolved_at_micros: Some(current_time_micros()), - resolved_by: Some("sync_policy".to_string()), - resolution_note: None, - policy_name: Some(policy.as_str().to_string()), - })) - } - Err(DbError::Constraint { message }) => { - Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { - conflict_type: if local_row_json.is_some() { - "insert_insert".to_string() - } else { - "constraint_error".to_string() - }, - message, - local_row_json, - resolution: None, - resolved_at_micros: None, - resolved_by: None, - resolution_note: None, - policy_name: Some(policy.as_str().to_string()), - })) - } - Err(error) => Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { - conflict_type: "apply_error".to_string(), - message: error.to_string(), - local_row_json, - resolution: None, - resolved_at_micros: None, - resolved_by: None, - resolution_note: None, - policy_name: Some(policy.as_str().to_string()), - })), - } - } - SyncOperation::Update => { - let after = record - .after - .as_ref() - .ok_or_else(|| DbError::sql("update record missing after payload"))? - .as_object() - .ok_or_else(|| DbError::sql("after must be an object for update"))?; - let mut params = - Vec::with_capacity(table.columns.len() + table.primary_key_columns.len()); - let mut expressions = Vec::with_capacity(table.columns.len()); - for column in &table.columns { - let json_value = after.get(&column.name).ok_or_else(|| { - DbError::sql(format!( - "missing column '{}' in update payload for table '{}'", - column.name, table.name - )) - })?; - params.push(json_to_column_value(&table.name, column, json_value)?); - expressions.push(format!( - "{} = ${}", - sql_identifier(&column.name), - params.len() - )); - } - for pk_col in &table.primary_key_columns { - let column = table - .columns - .iter() - .find(|column| column.name == *pk_col) - .ok_or_else(|| { - DbError::sql(format!( - "table '{}' missing primary key column '{}'", - table.name, pk_col - )) - })?; - let json_value = primary_key.get(pk_col).ok_or_else(|| { - DbError::sql(format!( - "missing primary key column '{pk_col}' in record for table '{}'", - table.name - )) - })?; - params.push(json_to_column_value(&table.name, column, json_value)?); - } - let sql = format!( - "UPDATE {} SET {} WHERE {}", - sql_identifier(&table.name), - expressions.join(", "), - table - .primary_key_columns - .iter() - .enumerate() - .map(|(idx, pk_col)| format!( - "{} = ${}", - sql_identifier(pk_col), - table.columns.len() + idx + 1 - )) - .collect::>() - .join(" AND ") - ); - match self.execute_with_params(&sql, ¶ms) { - Ok(result) if result.affected_rows() == 0 => { - Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { - conflict_type: "missing_target".to_string(), - message: "update affected no rows".to_string(), - local_row_json, - resolution: None, - resolved_at_micros: None, - resolved_by: None, - resolution_note: None, - policy_name: Some(policy.as_str().to_string()), - })) - } - Ok(_) => Ok(SyncImportRecordOutcome::Applied), - Err(DbError::Constraint { message }) if remote_wins => { - apply_remote_replace(SyncOperation::Update)?; - Ok(SyncImportRecordOutcome::Resolved(SyncConflictRecordData { - conflict_type: "update_update".to_string(), - message, - local_row_json, - resolution: Some("remote_applied".to_string()), - resolved_at_micros: Some(current_time_micros()), - resolved_by: Some("sync_policy".to_string()), - resolution_note: None, - policy_name: Some(policy.as_str().to_string()), - })) - } - Err(DbError::Constraint { message }) => { - Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { - conflict_type: if local_row_json.is_some() { - "update_update".to_string() - } else { - "constraint_error".to_string() - }, - message, - local_row_json, - resolution: None, - resolved_at_micros: None, - resolved_by: None, - resolution_note: None, - policy_name: Some(policy.as_str().to_string()), - })) - } - Err(error) => Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { - conflict_type: "apply_error".to_string(), - message: error.to_string(), - local_row_json, - resolution: None, - resolved_at_micros: None, - resolved_by: None, - resolution_note: None, - policy_name: Some(policy.as_str().to_string()), - })), - } - } - SyncOperation::Delete => { - let mut where_values = Vec::with_capacity(table.primary_key_columns.len()); - let mut where_parts = Vec::with_capacity(table.primary_key_columns.len()); - for pk_col in &table.primary_key_columns { - let column = table - .columns - .iter() - .find(|column| column.name == *pk_col) - .ok_or_else(|| { - DbError::sql(format!( - "table '{}' missing primary key column '{}'", - table.name, pk_col - )) - })?; - let json_value = primary_key.get(pk_col).ok_or_else(|| { - DbError::sql(format!( - "missing primary key column '{pk_col}' in record for table '{}'", - table.name - )) - })?; - where_values.push(json_to_column_value(&table.name, column, json_value)?); - where_parts.push(format!( - "{} = ${}", - sql_identifier(pk_col), - where_values.len() - )); - } - let sql = format!( - "DELETE FROM {} WHERE {}", - sql_identifier(&table.name), - where_parts.join(" AND ") - ); - match self.execute_with_params(&sql, &where_values) { - Ok(result) if result.affected_rows() == 0 => { - Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { - conflict_type: "missing_target".to_string(), - message: "delete affected no rows".to_string(), - local_row_json, - resolution: None, - resolved_at_micros: None, - resolved_by: None, - resolution_note: None, - policy_name: Some(policy.as_str().to_string()), - })) - } - Ok(_) => Ok(SyncImportRecordOutcome::Applied), - Err(DbError::Constraint { message }) if remote_wins => { - apply_remote_replace(SyncOperation::Delete)?; - Ok(SyncImportRecordOutcome::Resolved(SyncConflictRecordData { - conflict_type: "delete_update".to_string(), - message, - local_row_json, - resolution: Some("remote_applied".to_string()), - resolved_at_micros: Some(current_time_micros()), - resolved_by: Some("sync_policy".to_string()), - resolution_note: None, - policy_name: Some(policy.as_str().to_string()), - })) - } - Err(DbError::Constraint { message }) => { - Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { - conflict_type: if local_row_json.is_some() { - "delete_update".to_string() - } else { - "constraint_error".to_string() - }, - message, - local_row_json, - resolution: None, - resolved_at_micros: None, - resolved_by: None, - resolution_note: None, - policy_name: Some(policy.as_str().to_string()), - })) - } - Err(error) => Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { - conflict_type: "apply_error".to_string(), - message: error.to_string(), - local_row_json, - resolution: None, - resolved_at_micros: None, - resolved_by: None, - resolution_note: None, - policy_name: Some(policy.as_str().to_string()), - })), - } - } - } + resolved_by = resolved_by, + resolution_note = resolution_note, + policy_name = policy_name, + local_record_json = conflict + .local_row_json + .as_ref() + .map(|value| sql_text_literal(&value.to_string())) + .unwrap_or_else(|| "NULL".to_string()), + ); + let _ = self.execute(&sql)?; + Ok(conflict_id) } fn next_sync_conflict_id(&self) -> Result { @@ -15491,40 +9347,6 @@ impl Db { .ok_or_else(|| DbError::internal("sync conflict_id counter overflow")) } - fn sync_update_conflict_resolution( - &self, - conflict_id: i64, - resolution: Option<&str>, - resolved_by: Option<&str>, - note: Option<&str>, - resolved_at_micros: Option, - ) -> Result { - self.ensure_sync_tables()?; - let Some(_) = self.sync_conflict(conflict_id)? else { - return Ok(false); - }; - let sql = format!( - "UPDATE {table} SET resolved = {resolved}, resolution = {resolution}, resolved_at_micros = {resolved_at_micros}, resolved_by = {resolved_by}, resolution_note = {resolution_note} WHERE conflict_id = {conflict_id}", - table = crate::sync::CONFLICTS_TABLE, - resolved = if resolution.is_some() { 1 } else { 0 }, - resolution = resolution - .map(sql_text_literal) - .unwrap_or_else(|| "NULL".to_string()), - resolved_at_micros = resolved_at_micros - .map(|value| value.to_string()) - .unwrap_or_else(|| "NULL".to_string()), - resolved_by = resolved_by - .map(sql_text_literal) - .unwrap_or_else(|| "NULL".to_string()), - resolution_note = note - .map(sql_text_literal) - .unwrap_or_else(|| "NULL".to_string()), - conflict_id = conflict_id, - ); - let _ = self.execute(&sql)?; - Ok(true) - } - fn next_sync_session_id(&self) -> Result { let sql = format!( "SELECT COALESCE(MAX(session_id), 0) FROM {}", @@ -15546,49 +9368,6 @@ impl Db { .ok_or_else(|| DbError::internal("sync session_id counter overflow")) } - fn sync_update_session( - &self, - session_id: i64, - summary: &SyncRunSummary, - status: &str, - error: Option<&str>, - ended_at_micros: i64, - ) -> Result<()> { - self.ensure_sync_tables()?; - let ( - pushed_seen, - pushed_applied, - pushed_skipped, - pushed_conflicted, - pulled_seen, - pulled_applied, - pulled_skipped, - pulled_conflicted, - ) = sync_session_summary_counts(summary); - let sql = format!( - "UPDATE {table} SET remote_replica_id = {remote_replica_id}, ended_at_micros = {ended_at_micros}, status = {status}, error = {error}, pushed_batch_id = {pushed_batch_id}, pulled_batch_id = {pulled_batch_id}, pushed_seen = {pushed_seen}, pushed_applied = {pushed_applied}, pushed_skipped = {pushed_skipped}, pushed_conflicted = {pushed_conflicted}, pulled_seen = {pulled_seen}, pulled_applied = {pulled_applied}, pulled_skipped = {pulled_skipped}, pulled_conflicted = {pulled_conflicted}, retry_count = {retry_count} WHERE session_id = {session_id}", - table = crate::sync::SESSIONS_TABLE, - remote_replica_id = sql_nullable_text_literal(summary.remote_replica_id.as_deref()), - ended_at_micros = ended_at_micros, - status = sql_text_literal(status), - error = sql_nullable_text_literal(error), - pushed_batch_id = sql_nullable_text_literal(summary.pushed_batch_id.as_deref()), - pulled_batch_id = sql_nullable_text_literal(summary.pulled_batch_id.as_deref()), - pushed_seen = pushed_seen, - pushed_applied = pushed_applied, - pushed_skipped = pushed_skipped, - pushed_conflicted = pushed_conflicted, - pulled_seen = pulled_seen, - pulled_applied = pulled_applied, - pulled_skipped = pulled_skipped, - pulled_conflicted = pulled_conflicted, - retry_count = summary.retry_count as i64, - session_id = session_id, - ); - let _ = self.execute(&sql)?; - Ok(()) - } - fn ensure_sync_tables(&self) -> Result<()> { let _ = self.execute(crate::sync::METADATA_TABLE_DDL)?; let _ = self.execute(crate::sync::PEERS_TABLE_DDL)?; @@ -15627,28 +9406,6 @@ impl Db { Ok(()) } - fn sync_table_columns(&self, table_name: &str) -> Result> { - let sql = format!("PRAGMA table_info({})", sql_identifier(table_name)); - match self.execute(&sql) { - Ok(result) => Ok(result - .rows() - .iter() - .filter_map(|row| match row.values().get(1) { - Some(Value::Text(value)) => Some(value.clone()), - _ => None, - }) - .collect()), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(Vec::new()) - } else { - Err(error) - } - } - } - } - fn load_sync_status_from_db(&self) -> Result { let enabled = self .sync_read_metadata("enabled")? @@ -16060,26 +9817,6 @@ impl Db { } } - fn sync_status_query_result(&self) -> Result { - let status = self.sync_status()?; - Ok(QueryResult::with_rows( - vec![ - "enabled".to_string(), - "replica_id".to_string(), - "next_sequence".to_string(), - "journal_path".to_string(), - "journal_size_bytes".to_string(), - ], - vec![QueryRow::new(vec![ - Value::Bool(status.enabled), - status.replica_id.map_or(Value::Null, Value::Text), - sync_u64_to_i64(status.next_sequence, "next_sequence")?, - status.journal_path.map_or(Value::Null, Value::Text), - sync_u64_to_i64(status.journal_size_bytes, "journal_size_bytes")?, - ])], - )) - } - fn wal_metrics_query_result(&self) -> Result { let latest_lsn = self.inner.wal.latest_snapshot(); let file_size = self.inner.wal.file_size()?; @@ -16423,572 +10160,6 @@ impl Db { )) } - fn reactive_metrics_query_result(&self) -> Result { - let metrics = self.reactive_metrics(); - Ok(QueryResult::with_rows( - vec![ - "active_watch_count".to_string(), - "table_watch_count".to_string(), - "range_watch_count".to_string(), - "query_watch_count".to_string(), - "change_stream_count".to_string(), - "events_published".to_string(), - "events_delivered".to_string(), - "events_dropped".to_string(), - "lagged_watch_count".to_string(), - "row_change_events_truncated".to_string(), - ], - vec![QueryRow::new(vec![ - sync_usize_to_i64(metrics.active_watch_count, "active_watch_count")?, - sync_usize_to_i64(metrics.table_watch_count, "table_watch_count")?, - sync_usize_to_i64(metrics.range_watch_count, "range_watch_count")?, - sync_usize_to_i64(metrics.query_watch_count, "query_watch_count")?, - sync_usize_to_i64(metrics.change_stream_count, "change_stream_count")?, - sync_u64_to_i64(metrics.events_published, "events_published")?, - sync_u64_to_i64(metrics.events_delivered, "events_delivered")?, - sync_u64_to_i64(metrics.events_dropped, "events_dropped")?, - sync_usize_to_i64(metrics.lagged_watch_count, "lagged_watch_count")?, - sync_u64_to_i64( - metrics.row_change_events_truncated, - "row_change_events_truncated", - )?, - ])], - )) - } - - fn reactive_subscriptions_query_result(&self) -> Result { - let rows = self - .reactive_subscriptions() - .into_iter() - .map(|subscription| { - Ok(QueryRow::new(vec![ - sync_u64_to_i64(subscription.watch_id, "watch_id")?, - Value::Text(subscription.kind.as_str().to_string()), - Value::Int64(subscription.created_at_micros), - sync_usize_to_i64(subscription.queue_capacity, "queue_capacity")?, - sync_usize_to_i64(subscription.queue_depth, "queue_depth")?, - sync_u64_to_i64( - subscription.last_delivered_event_id, - "last_delivered_event_id", - )?, - sync_u64_to_i64(subscription.dropped_events, "dropped_events")?, - Value::Bool(subscription.lagged), - Value::Text(subscription.dependencies_json), - ])) - }) - .collect::>>()?; - Ok(QueryResult::with_rows( - vec![ - "watch_id".to_string(), - "kind".to_string(), - "created_at_micros".to_string(), - "queue_capacity".to_string(), - "queue_depth".to_string(), - "last_delivered_event_id".to_string(), - "dropped_events".to_string(), - "lagged".to_string(), - "dependencies_json".to_string(), - ], - rows, - )) - } - - fn sync_journal_query_result(&self, since_sequence: u64) -> Result { - let records = self.sync_pending_changes(since_sequence, usize::MAX)?; - let rows = records - .into_iter() - .map(|record| { - Ok(QueryRow::new(vec![ - sync_u64_to_i64(record.sequence, "sequence")?, - Value::Text(record.replica_id), - sync_u64_to_i64(record.transaction_lsn, "transaction_lsn")?, - Value::Text(record.table), - Value::Text(record.operation), - Value::Text(record.primary_key.to_string()), - record - .after - .map_or(Value::Null, |value| Value::Text(value.to_string())), - Value::Int64(i64::from(record.schema_cookie)), - Value::Int64(record.committed_at_micros), - ])) - }) - .collect::>>()?; - Ok(QueryResult::with_rows( - vec![ - "sequence".to_string(), - "replica_id".to_string(), - "transaction_lsn".to_string(), - "table_name".to_string(), - "operation".to_string(), - "primary_key_json".to_string(), - "after_json".to_string(), - "schema_cookie".to_string(), - "committed_at_micros".to_string(), - ], - rows, - )) - } - - fn sync_peers_query_result(&self) -> Result { - let peers = self.sync_peers()?; - let rows = peers - .into_iter() - .map(|peer| { - Ok(QueryRow::new(vec![ - Value::Text(peer.name), - Value::Text(peer.endpoint), - peer.token_env.map_or(Value::Null, Value::Text), - Value::Int64(peer.created_at_micros), - Value::Int64(peer.updated_at_micros), - ])) - }) - .collect::>>()?; - Ok(QueryResult::with_rows( - vec![ - "name".to_string(), - "endpoint".to_string(), - "token_env".to_string(), - "created_at_micros".to_string(), - "updated_at_micros".to_string(), - ], - rows, - )) - } - - fn sync_retention_query_result(&self) -> Result { - let retention = self.sync_retention_report()?; - let first_sequence = match retention.first_sequence { - Some(value) => sync_u64_to_i64(value, "first_sequence")?, - None => Value::Null, - }; - let last_sequence = match retention.last_sequence { - Some(value) => sync_u64_to_i64(value, "last_sequence")?, - None => Value::Null, - }; - let safe_prune_through = match retention.safe_prune_through { - Some(value) => sync_u64_to_i64(value, "safe_prune_through")?, - None => Value::Null, - }; - - Ok(QueryResult::with_rows( - vec![ - "journal_records".to_string(), - "first_sequence".to_string(), - "last_sequence".to_string(), - "safe_prune_through".to_string(), - "prunable_records".to_string(), - "blocked_by_json".to_string(), - "journal_size_bytes".to_string(), - ], - vec![QueryRow::new(vec![ - sync_u64_to_i64(retention.journal_records as u64, "journal_records")?, - first_sequence, - last_sequence, - safe_prune_through, - sync_u64_to_i64(retention.prunable_records as u64, "prunable_records")?, - Value::Text( - serde_json::to_string(&retention.blocked_by).map_err(|error| { - DbError::internal(format!( - "failed to encode sync retention blocked_by: {error}" - )) - })?, - ), - sync_u64_to_i64(retention.journal_size_bytes, "journal_size_bytes")?, - ])], - )) - } - - fn sync_peer_lag_query_result(&self) -> Result { - let peer_lag = self.sync_peer_lag_report()?; - let rows = peer_lag - .into_iter() - .map(|lag| { - let in_watermark = match lag.in_watermark { - Some(value) => sync_u64_to_i64(value, "in_watermark")?, - None => Value::Null, - }; - let out_watermark = match lag.out_watermark { - Some(value) => sync_u64_to_i64(value, "out_watermark")?, - None => Value::Null, - }; - let local_high_watermark = match lag.local_high_watermark { - Some(value) => sync_u64_to_i64(value, "local_high_watermark")?, - None => Value::Null, - }; - let in_lag = match lag.in_lag { - Some(value) => sync_u64_to_i64(value, "in_lag")?, - None => Value::Null, - }; - let out_lag = match lag.out_lag { - Some(value) => sync_u64_to_i64(value, "out_lag")?, - None => Value::Null, - }; - Ok(QueryRow::new(vec![ - Value::Text(lag.peer_name), - lag.remote_replica_id.map_or(Value::Null, Value::Text), - in_watermark, - out_watermark, - local_high_watermark, - in_lag, - out_lag, - ])) - }) - .collect::>>()?; - Ok(QueryResult::with_rows( - vec![ - "peer_name".to_string(), - "remote_replica_id".to_string(), - "in_watermark".to_string(), - "out_watermark".to_string(), - "local_high_watermark".to_string(), - "in_lag".to_string(), - "out_lag".to_string(), - ], - rows, - )) - } - - fn sync_doctor_query_result(&self) -> Result { - let report = self.sync_operational_doctor_report()?; - Ok(QueryResult::with_rows( - vec![ - "enabled".to_string(), - "replica_id".to_string(), - "highest_severity".to_string(), - "journal_records".to_string(), - "journal_size_bytes".to_string(), - "unresolved_conflicts".to_string(), - "guidance_json".to_string(), - ], - vec![QueryRow::new(vec![ - Value::Bool(report.status.enabled), - report.status.replica_id.map_or(Value::Null, Value::Text), - Value::Text(report.highest_severity.to_string()), - sync_u64_to_i64(report.integrity.total_records as u64, "journal_records")?, - sync_u64_to_i64(report.retention.journal_size_bytes, "journal_size_bytes")?, - sync_u64_to_i64(report.unresolved_conflicts as u64, "unresolved_conflicts")?, - Value::Text(serde_json::to_string(&report.guidance).map_err(|error| { - DbError::internal(format!("failed to encode sync doctor guidance: {error}")) - })?), - ])], - )) - } - - fn sync_scopes_query_result(&self) -> Result { - let scopes = self.sync_scopes()?; - let rows = scopes - .into_iter() - .map(|scope| { - let SyncScope { - name, - include_tables, - row_filter, - filter_columns, - created_at_micros, - updated_at_micros, - } = scope; - Ok(QueryRow::new(vec![ - Value::Text(name), - Value::Text(serde_json::to_string(&include_tables).map_err(|error| { - DbError::internal(format!( - "failed to encode sync scope include tables: {error}" - )) - })?), - row_filter.map_or(Value::Null, Value::Text), - Value::Text(serde_json::to_string(&filter_columns).map_err(|error| { - DbError::internal(format!( - "failed to encode sync scope filter columns: {error}" - )) - })?), - Value::Int64(created_at_micros), - Value::Int64(updated_at_micros), - ])) - }) - .collect::>>()?; - Ok(QueryResult::with_rows( - vec![ - "name".to_string(), - "include_tables_json".to_string(), - "row_filter".to_string(), - "filter_columns_json".to_string(), - "created_at_micros".to_string(), - "updated_at_micros".to_string(), - ], - rows, - )) - } - - fn sync_scope_tables_query_result(&self) -> Result { - let mut rows = Vec::new(); - for scope in self.sync_scopes()? { - let scope_name = scope.name; - for table_name in scope.include_tables { - rows.push(QueryRow::new(vec![ - Value::Text(scope_name.clone()), - Value::Text(table_name), - ])); - } - } - Ok(QueryResult::with_rows( - vec!["scope_name".to_string(), "table_name".to_string()], - rows, - )) - } - - fn sync_peer_scopes_query_result(&self) -> Result { - let bindings = self.sync_peer_scope_bindings()?; - let rows = bindings - .into_iter() - .map(|binding| { - Ok(QueryRow::new(vec![ - Value::Text(binding.peer_name), - Value::Text(binding.scope_name), - Value::Int64(binding.created_at_micros), - Value::Int64(binding.updated_at_micros), - ])) - }) - .collect::>>()?; - Ok(QueryResult::with_rows( - vec![ - "peer_name".to_string(), - "scope_name".to_string(), - "created_at_micros".to_string(), - "updated_at_micros".to_string(), - ], - rows, - )) - } - - fn sync_sessions_query_result(&self) -> Result { - let sessions = self.sync_sessions()?; - let rows = sessions - .into_iter() - .map(|session| { - Ok(QueryRow::new(vec![ - Value::Int64(session.session_id), - Value::Text(session.peer_name), - Value::Text(session.direction.to_string()), - session.remote_replica_id.map_or(Value::Null, Value::Text), - Value::Int64(session.started_at_micros), - session.ended_at_micros.map_or(Value::Null, Value::Int64), - Value::Text(session.status), - session.error.map_or(Value::Null, Value::Text), - session.pushed_batch_id.map_or(Value::Null, Value::Text), - session.pulled_batch_id.map_or(Value::Null, Value::Text), - Value::Int64(session.pushed_seen), - Value::Int64(session.pushed_applied), - Value::Int64(session.pushed_skipped), - Value::Int64(session.pushed_conflicted), - Value::Int64(session.pulled_seen), - Value::Int64(session.pulled_applied), - Value::Int64(session.pulled_skipped), - Value::Int64(session.pulled_conflicted), - Value::Int64(session.retry_count), - ])) - }) - .collect::>>()?; - Ok(QueryResult::with_rows( - vec![ - "session_id".to_string(), - "peer_name".to_string(), - "direction".to_string(), - "remote_replica_id".to_string(), - "started_at_micros".to_string(), - "ended_at_micros".to_string(), - "status".to_string(), - "error".to_string(), - "pushed_batch_id".to_string(), - "pulled_batch_id".to_string(), - "pushed_seen".to_string(), - "pushed_applied".to_string(), - "pushed_skipped".to_string(), - "pushed_conflicted".to_string(), - "pulled_seen".to_string(), - "pulled_applied".to_string(), - "pulled_skipped".to_string(), - "pulled_conflicted".to_string(), - "retry_count".to_string(), - ], - rows, - )) - } - - fn sync_conflict_policy_query_result(&self) -> Result { - let policy = self.sync_conflict_policy()?; - Ok(QueryResult::with_rows( - vec![ - "default_policy".to_string(), - "origin_priority_json".to_string(), - ], - vec![QueryRow::new(vec![ - Value::Text(policy.default_policy.to_string()), - Value::Text( - serde_json::to_string(&policy.origin_priority).map_err(|error| { - DbError::internal(format!( - "failed to encode sync conflict policy origin priority: {error}" - )) - })?, - ), - ])], - )) - } - - fn sync_conflicts_query_result(&self) -> Result { - let sql = format!( - "SELECT * FROM {} WHERE resolved = 0 ORDER BY conflict_id", - crate::sync::CONFLICTS_TABLE - ); - match self.execute(&sql) { - Ok(result) => Ok(result), - Err(error) => { - let message = error.to_string(); - if message.contains("no such table") || message.contains("unknown table") { - Ok(QueryResult::with_rows( - vec![ - "conflict_id".to_string(), - "batch_id".to_string(), - "remote_replica_id".to_string(), - "remote_sequence".to_string(), - "table_name".to_string(), - "operation".to_string(), - "conflict_type".to_string(), - "message".to_string(), - "primary_key_json".to_string(), - "remote_record_json".to_string(), - "local_row_json".to_string(), - "created_at_micros".to_string(), - "resolved".to_string(), - "resolution".to_string(), - "resolved_at_micros".to_string(), - "resolved_by".to_string(), - "resolution_note".to_string(), - "policy_name".to_string(), - "local_record_json".to_string(), - ], - Vec::new(), - )) - } else { - Err(error) - } - } - } - } - - fn sync_relay_status_query_result(&self) -> Result { - let status = self.sync_relay_status(None, false, false, false, None)?; - Ok(QueryResult::with_rows( - vec![ - "relay_id".to_string(), - "protocol_version".to_string(), - "database_replica_id".to_string(), - "production_mode".to_string(), - "secure_transport_required".to_string(), - "insecure_override_enabled".to_string(), - "active_sessions".to_string(), - "active_streams".to_string(), - "started_at_micros".to_string(), - ], - vec![QueryRow::new(vec![ - Value::Text(status.relay_id), - Value::Int64(i64::from(status.protocol_version)), - status - .database_replica_id - .map(Value::Text) - .unwrap_or(Value::Null), - Value::Bool(status.production_mode), - Value::Bool(status.secure_transport_required), - Value::Bool(status.insecure_override_enabled), - sync_u64_to_i64(status.active_sessions, "active_sessions")?, - sync_u64_to_i64(status.active_streams, "active_streams")?, - Value::Int64(status.started_at_micros), - ])], - )) - } - - fn sync_relay_sessions_query_result(&self) -> Result { - self.query_table_or_empty( - crate::sync::RELAY_SESSIONS_TABLE, - &[ - "session_id", - "tenant_id", - "subject_id", - "subject_kind", - "request_id", - "operation", - "scope_name", - "shape_id", - "started_at_micros", - "ended_at_micros", - "status", - "error", - "rows_seen", - "bytes_seen", - ], - "started_at_micros, session_id", - ) - } - - fn sync_shapes_query_result(&self) -> Result { - self.query_table_or_empty( - crate::sync::SHAPES_TABLE, - &[ - "shape_id", - "name", - "scope_name", - "tenant_id", - "allowed_roles_json", - "allowed_subjects_json", - "created_at_micros", - "updated_at_micros", - "retention_ttl_micros", - "max_records", - "ack_deadline_micros", - "heartbeat_micros", - ], - "shape_id", - ) - } - - fn sync_shape_clients_query_result(&self) -> Result { - self.query_table_or_empty( - crate::sync::SHAPE_CLIENTS_TABLE, - &[ - "shape_id", - "tenant_id", - "client_replica_id", - "subject_id", - "session_id", - "last_ack_sequence", - "last_ack_watermark", - "last_changeset_id", - "last_seen_at_micros", - "retention_blocking", - "status", - ], - "shape_id, client_replica_id", - ) - } - - fn sync_changeset_history_query_result(&self) -> Result { - self.query_table_or_empty( - crate::sync::CHANGESET_HISTORY_TABLE, - &[ - "changeset_id", - "source_replica_id", - "source_kind", - "scope_name", - "shape_id", - "record_count", - "bytes", - "created_at_micros", - "applied_at_micros", - "outcome", - "integrity_hash", - ], - "created_at_micros, changeset_id", - ) - } - fn query_table_or_empty( &self, table_name: &str, @@ -17020,46 +10191,6 @@ impl Db { } } - pub(crate) fn sync_post_commit( - &self, - runtime: &mut EngineRuntime, - committed_lsn: u64, - ) -> Result<()> { - let mutations = runtime.take_sync_mutations(); - if mutations.is_empty() { - return Ok(()); - } - if !self.inner.sync_ctx.capture_enabled() { - return Ok(()); - } - let enabled = if self.inner.sync_ctx.is_enabled() { - true - } else { - let status = self.load_sync_status_from_runtime(runtime)?; - if status.enabled { - self.inner.sync_ctx.set_enabled(true); - if let Some(replica_id) = status.replica_id.as_deref() { - self.inner.sync_ctx.set_replica_id(replica_id); - } - self.inner.sync_ctx.set_next_sequence(status.next_sequence); - } - status.enabled - }; - if !enabled { - return Ok(()); - } - self.inner - .sync_ctx - .pending_mutations - .lock() - .map_err(|_| DbError::internal("sync pending mutations lock poisoned"))? - .extend(mutations); - self.inner - .sync_ctx - .flush_journal(&self.inner.vfs, committed_lsn)?; - Ok(()) - } - fn take_reactive_pending_commit( &self, runtime: &mut EngineRuntime, diff --git a/crates/decentdb/src/db/branch_ops.rs b/crates/decentdb/src/db/branch_ops.rs new file mode 100644 index 00000000..7989e8cc --- /dev/null +++ b/crates/decentdb/src/db/branch_ops.rs @@ -0,0 +1,220 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +impl Db { + /// Compares two refs (`main`, branch name, named snapshot, or branch head ID). + pub fn branch_diff( + &self, + left_ref: &str, + right_ref: &str, + ) -> Result { + let left_db = self.materialize_ref_db(left_ref)?; + let right_db = self.materialize_ref_db(right_ref)?; + diff_materialized_refs(left_ref, right_ref, &left_db, &right_db) + } + /// Restores a non-main branch head to another branch, named snapshot, or head ID. + pub fn branch_restore( + &self, + branch_name: &str, + target_ref: &str, + dry_run: bool, + ) -> Result { + if self.inner.sql_txn_active.load(Ordering::Acquire) { + return Err(DbError::transaction( + "cannot restore a branch while a SQL transaction is active", + )); + } + if branch_name == crate::branch::DEFAULT_BRANCH_NAME { + return Err(DbError::transaction( + "restore currently targets non-main branches; create a branch from the restore point to inspect main rollback candidates", + )); + } + let branch = crate::branch::branch_by_name(self, branch_name)? + .ok_or_else(|| DbError::transaction(format!("unknown branch '{branch_name}'")))?; + let target_head = self.resolve_branch_target_head(target_ref)?; + let diff = self.branch_diff(branch_name, target_ref)?; + if dry_run { + return Ok(crate::branch::BranchRestoreReport { + branch: branch_name.to_string(), + target_ref: target_ref.to_string(), + dry_run: true, + previous_head_id: branch.current_head_id, + target_head_id: target_head.head_id, + new_head_id: None, + changed_table_count: diff.changed_table_count, + added_row_count: diff.added_row_count, + updated_row_count: diff.updated_row_count, + deleted_row_count: diff.deleted_row_count, + }); + } + let new_head = crate::branch::restore_branch_head(self, &branch, &target_head, target_ref)?; + self.refresh_named_snapshot_retention()?; + Ok(crate::branch::BranchRestoreReport { + branch: branch_name.to_string(), + target_ref: target_ref.to_string(), + dry_run: false, + previous_head_id: branch.current_head_id, + target_head_id: target_head.head_id, + new_head_id: Some(new_head.head_id), + changed_table_count: diff.changed_table_count, + added_row_count: diff.added_row_count, + updated_row_count: diff.updated_row_count, + deleted_row_count: diff.deleted_row_count, + }) + } + /// Merges clean primary-key row changes from a source branch into a target ref. + pub fn branch_merge( + &self, + source_branch: &str, + target_ref: &str, + dry_run: bool, + ) -> Result { + if self.inner.sql_txn_active.load(Ordering::Acquire) { + return Err(DbError::transaction( + "cannot merge a branch while a SQL transaction is active", + )); + } + if source_branch == crate::branch::DEFAULT_BRANCH_NAME { + return Err(DbError::transaction( + "merge source must be a non-main branch", + )); + } + let source = crate::branch::branch_by_name(self, source_branch)? + .ok_or_else(|| DbError::transaction(format!("unknown branch '{source_branch}'")))?; + let base_head_id = source.base_head_id.clone().ok_or_else(|| { + DbError::transaction(format!("branch '{source_branch}' has no merge base")) + })?; + if target_ref != crate::branch::DEFAULT_BRANCH_NAME + && crate::branch::branch_by_name(self, target_ref)?.is_none() + { + return Err(DbError::transaction(format!( + "merge target must be 'main' or a branch; got '{target_ref}'" + ))); + } + + let base_db = self.materialize_branch_head_db(&base_head_id)?; + let source_db = self.materialize_branch_db(&source)?; + let target_db = self.materialize_ref_db(target_ref)?; + let plan = build_merge_plan( + source_branch, + target_ref, + &base_head_id, + &base_db, + &source_db, + &target_db, + )?; + if dry_run || !plan.conflicts.is_empty() { + return Ok(plan.into_report(dry_run)); + } + let sql = plan + .changes + .iter() + .map(|change| change.sql.as_str()) + .collect::>() + .join("\n"); + if !sql.trim().is_empty() { + if target_ref == crate::branch::DEFAULT_BRANCH_NAME { + crate::reactive::with_change_source(ChangeSource::BranchMerge, || { + self.execute_batch(&sql) + })?; + } else { + self.execute_batch_on_branch(&sql, target_ref)?; + } + } + Ok(plan.into_report(false)) + } + /// Creates a branch from `main`, another branch, a named snapshot, or a branch head. + pub fn branch_create( + &self, + name: &str, + from: Option<&str>, + ) -> Result { + let source = from.unwrap_or(crate::branch::DEFAULT_BRANCH_NAME); + let (source_lsn, parent_head_id) = if source == crate::branch::DEFAULT_BRANCH_NAME { + let initial_lsn = self.inner.wal.latest_snapshot(); + self.inner.wal.set_retained_snapshot_lsn(Some(initial_lsn)); + self.checkpoint_wal()?; + let source_lsn = self.inner.wal.latest_snapshot(); + let parent_head_id = crate::branch::main_branch_head(self)?.map(|head| head.head_id); + (source_lsn, parent_head_id) + } else if let Some(branch) = crate::branch::branch_by_name(self, source)? { + let source_lsn = self.branch_lsn(source)?.ok_or_else(|| { + DbError::transaction(format!("branch '{source}' has no current head")) + })?; + (source_lsn, branch.current_head_id) + } else if let Some(snapshot) = self.snapshot_get(source)? { + (snapshot.snapshot_lsn, Some(snapshot.head_id)) + } else if let Some(source_lsn) = crate::branch::branch_head_lsn_by_id(self, source)? { + (source_lsn, Some(source.to_string())) + } else { + return Err(DbError::transaction(format!( + "unknown branch, snapshot, or head '{source}'" + ))); + }; + self.inner.wal.set_retained_snapshot_lsn(Some(source_lsn)); + let schema_cookie = self.current_schema_cookie_at_snapshot(source_lsn)?; + let result = crate::branch::create_branch( + self, + name, + source_lsn, + schema_cookie, + parent_head_id.as_deref(), + ); + self.refresh_named_snapshot_retention()?; + result + } + /// Lists branches. + pub fn branch_list(&self) -> Result> { + crate::branch::list_branches(self) + } + /// Deletes a non-main branch. + pub fn branch_delete(&self, name: &str) -> Result { + let deleted = crate::branch::delete_branch(self, name)?; + if deleted { + self.refresh_named_snapshot_retention()?; + } + Ok(deleted) + } + /// Renames a non-main branch. + pub fn branch_rename(&self, old_name: &str, new_name: &str) -> Result { + crate::branch::rename_branch(self, old_name, new_name) + } + /// Resolves a branch name to its current retained WAL LSN. + pub fn branch_lsn(&self, name: &str) -> Result> { + crate::branch::branch_lsn_by_name(self, name) + } + /// Adds a named commit marker to a non-main branch. + pub fn branch_commit( + &self, + name: &str, + message: &str, + ) -> Result { + if self.inner.sql_txn_active.load(Ordering::Acquire) { + return Err(DbError::transaction( + "cannot create a branch commit marker while a SQL transaction is active", + )); + } + if name == crate::branch::DEFAULT_BRANCH_NAME { + return Err(DbError::transaction( + "branch commit markers are only supported on non-main branches", + )); + } + let branch = crate::branch::branch_by_name(self, name)? + .ok_or_else(|| DbError::transaction(format!("unknown branch '{name}'")))?; + let head = crate::branch::commit_branch(self, &branch, message)?; + self.refresh_named_snapshot_retention()?; + Ok(crate::branch::BranchLogEntry { + head_id: head.head_id, + branch_id: head.branch_id, + parent_head_id: head.parent_head_id, + message: head.message, + created_at_micros: head.created_at_micros, + sql: None, + }) + } + /// Returns branch head history newest first. + pub fn branch_log(&self, name: &str) -> Result> { + crate::branch::branch_log(self, name) + } +} diff --git a/crates/decentdb/src/db/pragmas.rs b/crates/decentdb/src/db/pragmas.rs new file mode 100644 index 00000000..2444281f --- /dev/null +++ b/crates/decentdb/src/db/pragmas.rs @@ -0,0 +1,426 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +impl Db { + pub(super) fn execute_pragma_command(&self, command: PragmaCommand) -> Result { + match command { + PragmaCommand::Query(target) => self.execute_pragma_query(target), + PragmaCommand::Call { target, argument } => self.execute_pragma_call(target, argument), + PragmaCommand::Set(target, value) => self.execute_pragma_set(target, value), + } + } + fn execute_pragma_query(&self, target: PragmaTarget) -> Result { + match target.name { + PragmaName::PageSize => Ok(QueryResult::with_rows( + vec!["page_size".to_string()], + vec![QueryRow::new(vec![Value::Int64(i64::from( + self.inner.config.page_size, + ))])], + )), + PragmaName::CacheSize => Ok(QueryResult::with_rows( + vec!["cache_size".to_string()], + vec![QueryRow::new(vec![Value::Int64(cache_size_pages( + &self.inner.config, + ))])], + )), + PragmaName::DatabaseList => { + let file_name = if is_memory_path(&self.inner.path) { + ":memory:".to_string() + } else { + self.inner.path.display().to_string() + }; + Ok(QueryResult::with_rows( + vec!["seq".to_string(), "name".to_string(), "file".to_string()], + vec![QueryRow::new(vec![ + Value::Int64(0), + Value::Text("main".to_string()), + Value::Text(file_name), + ])], + )) + } + PragmaName::TableInfo => Err(DbError::sql( + "PRAGMA table_info(table_name) requires a table name argument", + )), + PragmaName::TableXInfo => Err(DbError::sql( + "PRAGMA table_xinfo(table_name) requires a table name argument", + )), + PragmaName::IndexList => Err(DbError::sql( + "PRAGMA index_list(table_name) requires a table name argument", + )), + PragmaName::IndexInfo => Err(DbError::sql( + "PRAGMA index_info(index_name) requires an index name argument", + )), + PragmaName::IndexXInfo => Err(DbError::sql( + "PRAGMA index_xinfo(index_name) requires an index name argument", + )), + PragmaName::ForeignKeyList => Err(DbError::sql( + "PRAGMA foreign_key_list(table_name) requires a table name argument", + )), + PragmaName::TableList => self.execute_compatibility_select(&format!( + "SELECT * FROM {}pragma_table_list()", + pragma_schema_function_prefix(target.schema) + )), + PragmaName::IntegrityCheck | PragmaName::QuickCheck => self.integrity_check_results(), + PragmaName::ForeignKeys => Ok(QueryResult::with_rows( + vec!["foreign_keys".to_string()], + vec![QueryRow::new(vec![Value::Int64(1)])], + )), + PragmaName::JournalMode => Ok(QueryResult::with_rows( + vec!["journal_mode".to_string()], + vec![QueryRow::new(vec![Value::Text("wal".to_string())])], + )), + PragmaName::Synchronous => Ok(QueryResult::with_rows( + vec!["synchronous".to_string()], + vec![QueryRow::new(vec![Value::Int64( + pragma_synchronous_mode_value(self.inner.config.wal_sync_mode), + )])], + )), + PragmaName::WalCheckpoint => self.execute_pragma_wal_checkpoint(None), + PragmaName::SchemaVersion => { + let runtime = self.runtime_for_metadata_inspection()?; + let version = match target.schema { + Some(PragmaSchema::Temp) => runtime.temp_schema_cookie, + _ => runtime.catalog.schema_cookie, + }; + Ok(QueryResult::with_rows( + vec!["schema_version".to_string()], + vec![QueryRow::new(vec![Value::Int64(i64::from(version))])], + )) + } + PragmaName::UserVersion => self.execute_application_pragma_query("user_version"), + PragmaName::ApplicationId => self.execute_application_pragma_query("application_id"), + PragmaName::Encoding => Ok(QueryResult::with_rows( + vec!["encoding".to_string()], + vec![QueryRow::new(vec![Value::Text("UTF-8".to_string())])], + )), + PragmaName::LockingMode => Ok(QueryResult::with_rows( + vec!["locking_mode".to_string()], + vec![QueryRow::new(vec![Value::Text("normal".to_string())])], + )), + PragmaName::TempStore => Ok(QueryResult::with_rows( + vec!["temp_store".to_string()], + vec![QueryRow::new(vec![Value::Int64(1)])], + )), + PragmaName::BusyTimeout => Ok(QueryResult::with_rows( + vec!["busy_timeout".to_string()], + vec![QueryRow::new(vec![Value::Int64( + i64::try_from(self.inner.busy_timeout_ms.load(Ordering::Acquire)) + .unwrap_or(i64::MAX), + )])], + )), + PragmaName::FlushPlanCache => { + self.flush_plan_cache()?; + Ok(QueryResult::with_affected_rows(0)) + } + } + } + fn execute_pragma_call( + &self, + target: PragmaTarget, + argument: Option, + ) -> Result { + match target.name { + PragmaName::TableInfo => { + let table_name = pragma_required_argument(&target, argument)?; + self.execute_pragma_table_info(&table_name, target.schema, false) + } + PragmaName::TableXInfo => { + let table_name = pragma_required_argument(&target, argument)?; + self.execute_compatibility_select(&format!( + "SELECT * FROM {}pragma_table_xinfo({})", + pragma_schema_function_prefix(target.schema), + sql_string_literal(&table_name) + )) + } + PragmaName::IndexList => { + let table_name = pragma_required_argument(&target, argument)?; + self.execute_compatibility_select(&format!( + "SELECT * FROM {}pragma_index_list({})", + pragma_schema_function_prefix(target.schema), + sql_string_literal(&table_name) + )) + } + PragmaName::IndexInfo => { + let index_name = pragma_required_argument(&target, argument)?; + self.execute_compatibility_select(&format!( + "SELECT * FROM {}pragma_index_info({})", + pragma_schema_function_prefix(target.schema), + sql_string_literal(&index_name) + )) + } + PragmaName::IndexXInfo => { + let index_name = pragma_required_argument(&target, argument)?; + self.execute_compatibility_select(&format!( + "SELECT * FROM {}pragma_index_xinfo({})", + pragma_schema_function_prefix(target.schema), + sql_string_literal(&index_name) + )) + } + PragmaName::ForeignKeyList => { + let table_name = pragma_required_argument(&target, argument)?; + self.execute_compatibility_select(&format!( + "SELECT * FROM {}pragma_foreign_key_list({})", + pragma_schema_function_prefix(target.schema), + sql_string_literal(&table_name) + )) + } + PragmaName::FlushPlanCache => { + self.flush_plan_cache()?; + Ok(QueryResult::with_affected_rows(0)) + } + PragmaName::WalCheckpoint => self.execute_pragma_wal_checkpoint(argument.as_deref()), + other => Err(DbError::sql(format!( + "PRAGMA {} does not accept call syntax", + pragma_name_sql(&other) + ))), + } + } + fn execute_pragma_table_info( + &self, + table_name: &str, + schema: Option, + extended: bool, + ) -> Result { + let runtime = self.runtime_for_metadata_inspection()?; + let table = match schema { + Some(PragmaSchema::Temp) => runtime + .temp_table_schema(table_name) + .ok_or_else(|| DbError::sql(format!("unknown temporary table {table_name}")))?, + Some(PragmaSchema::Main) => runtime + .catalog + .table(table_name) + .ok_or_else(|| DbError::sql(format!("unknown table {table_name}")))?, + None => runtime + .table_schema(table_name) + .ok_or_else(|| DbError::sql(format!("unknown table {table_name}")))?, + }; + let rows = table + .columns + .iter() + .enumerate() + .map(|(cid, column)| { + let mut values = vec![ + Value::Int64(i64::try_from(cid).unwrap_or(i64::MAX)), + Value::Text(column.name.clone()), + Value::Text(column.column_type.as_str().to_string()), + Value::Int64(if column.nullable { 0 } else { 1 }), + column.default_sql.clone().map_or(Value::Null, Value::Text), + Value::Int64(if column.primary_key { 1 } else { 0 }), + ]; + if extended { + let hidden = if column.generated_sql.is_none() { + 0 + } else if column.generated_stored { + 3 + } else { + 2 + }; + values.push(Value::Int64(hidden)); + } + QueryRow::new(values) + }) + .collect(); + let mut columns = vec![ + "cid".to_string(), + "name".to_string(), + "type".to_string(), + "notnull".to_string(), + "dflt_value".to_string(), + "pk".to_string(), + ]; + if extended { + columns.push("hidden".to_string()); + } + Ok(QueryResult::with_rows(columns, rows)) + } + fn execute_pragma_wal_checkpoint(&self, mode: Option<&str>) -> Result { + if let Some(mode) = mode { + match mode.trim().to_ascii_uppercase().as_str() { + "PASSIVE" | "FULL" | "RESTART" | "TRUNCATE" => {} + other => { + return Err(DbError::sql(format!( + "PRAGMA wal_checkpoint mode {other} is not supported; expected PASSIVE, FULL, RESTART, or TRUNCATE" + ))) + } + } + } + let active_readers = self.inner.wal.active_reader_count()?; + let retained_snapshot = self.inner.wal.retained_snapshot_lsn().is_some(); + let before_versions = self.inner.wal.version_count()?; + self.prepare_resident_payload_offset_caches_for_wal_checkpoint()?; + self.checkpoint_wal()?; + let after_versions = self.inner.wal.version_count()?; + let checkpointed = before_versions.saturating_sub(after_versions); + Ok(QueryResult::with_rows( + vec![ + "busy".to_string(), + "log".to_string(), + "checkpointed".to_string(), + ], + vec![QueryRow::new(vec![ + Value::Int64(i64::from(active_readers > 0 || retained_snapshot)), + Value::Int64(i64::try_from(before_versions).unwrap_or(i64::MAX)), + Value::Int64(i64::try_from(checkpointed).unwrap_or(i64::MAX)), + ])], + )) + } + fn execute_pragma_set(&self, target: PragmaTarget, value: PragmaValue) -> Result { + match target.name { + PragmaName::PageSize => { + let value = pragma_value_i64(&value)?; + if value == i64::from(self.inner.config.page_size) { + Ok(QueryResult::with_affected_rows(0)) + } else { + Err(DbError::sql( + "PRAGMA page_size cannot be changed on an open database; reopen with DbConfig::page_size", + )) + } + } + PragmaName::CacheSize => { + if pragma_value_i64(&value)? == cache_size_pages(&self.inner.config) { + Ok(QueryResult::with_affected_rows(0)) + } else { + Err(DbError::sql( + "PRAGMA cache_size cannot be changed on an open connection; reopen with DbConfig::cache_size_mb", + )) + } + } + PragmaName::IntegrityCheck + | PragmaName::DatabaseList + | PragmaName::TableInfo + | PragmaName::TableXInfo + | PragmaName::TableList + | PragmaName::IndexList + | PragmaName::IndexInfo + | PragmaName::IndexXInfo + | PragmaName::ForeignKeyList + | PragmaName::WalCheckpoint + | PragmaName::QuickCheck => Err(DbError::sql(format!( + "PRAGMA {} does not support assignment", + pragma_name_sql(&target.name) + ))), + PragmaName::FlushPlanCache => { + let value = parse_pragma_text_or_mode(&value, "PRAGMA flush_plan_cache")?; + if value == "LOCAL" { + self.flush_plan_cache()?; + Ok(QueryResult::with_affected_rows(0)) + } else { + Err(DbError::sql( + "PRAGMA flush_plan_cache accepts only local in this release", + )) + } + } + PragmaName::ForeignKeys => { + let value = parse_pragma_bool_value(&value, "PRAGMA foreign_keys")?; + if value { + Ok(QueryResult::with_affected_rows(0)) + } else { + Err(DbError::sql( + "PRAGMA foreign_keys cannot disable foreign key enforcement in DecentDB", + )) + } + } + PragmaName::JournalMode => { + let mode = parse_pragma_text_or_mode(&value, "PRAGMA journal_mode")?; + if mode == "WAL" { + Ok(QueryResult::with_rows( + vec!["journal_mode".to_string()], + vec![QueryRow::new(vec![Value::Text("wal".to_string())])], + )) + } else { + Err(DbError::sql( + "PRAGMA journal_mode supports only WAL in this compatibility slice", + )) + } + } + PragmaName::Synchronous => { + let requested = parse_pragma_synchronous_request(&value, "PRAGMA synchronous")?; + let current = self.inner.config.wal_sync_mode; + match requested { + SynchronousRequest::Full => { + if current == WalSyncMode::Full { + Ok(QueryResult::with_affected_rows(0)) + } else { + Err(DbError::sql( + "PRAGMA synchronous = FULL requires reopening with DbConfig::wal_sync_mode = Full", + )) + } + } + SynchronousRequest::Normal => { + if current == WalSyncMode::Normal + || matches!(current, WalSyncMode::AsyncCommit { .. }) + { + Ok(QueryResult::with_affected_rows(0)) + } else { + Err(DbError::sql( + "PRAGMA synchronous = NORMAL requires reopening with DbConfig::wal_sync_mode = Normal or AsyncCommit", + )) + } + } + SynchronousRequest::Off => { + if current == WalSyncMode::TestingOnlyUnsafeNoSync { + Ok(QueryResult::with_affected_rows(0)) + } else { + Err(DbError::sql( + "PRAGMA synchronous = OFF requires reopening with DbConfig::wal_sync_mode = TestingOnlyUnsafeNoSync", + )) + } + } + SynchronousRequest::Extra => Err(DbError::sql( + "PRAGMA synchronous = EXTRA is not supported by DecentDB", + )), + } + } + PragmaName::SchemaVersion => Err(DbError::sql( + "PRAGMA schema_version does not support assignment", + )), + PragmaName::UserVersion => self.execute_application_pragma_set("user_version", &value), + PragmaName::ApplicationId => { + self.execute_application_pragma_set("application_id", &value) + } + PragmaName::Encoding => { + let mode = parse_pragma_text_or_mode(&value, "PRAGMA encoding")?; + if mode == "UTF-8" || mode == "UTF8" { + Ok(QueryResult::with_affected_rows(0)) + } else { + Err(DbError::sql( + "PRAGMA encoding can only be set to UTF-8 in this compatibility slice", + )) + } + } + PragmaName::LockingMode => { + let mode = parse_pragma_text_or_mode(&value, "PRAGMA locking_mode")?; + if mode == "NORMAL" { + Ok(QueryResult::with_affected_rows(0)) + } else { + Err(DbError::sql( + "PRAGMA locking_mode supports only NORMAL in this compatibility slice", + )) + } + } + PragmaName::TempStore => { + let value = parse_pragma_text_or_mode(&value, "PRAGMA temp_store")?; + if matches!(value.as_str(), "DEFAULT" | "FILE" | "0" | "1") { + Ok(QueryResult::with_affected_rows(0)) + } else if value == "MEMORY" { + Err(DbError::sql( + "PRAGMA temp_store = MEMORY is not supported in this compatibility slice", + )) + } else { + Err(DbError::sql( + "PRAGMA temp_store accepts 0, 1, 'DEFAULT', or 'FILE' only", + )) + } + } + PragmaName::BusyTimeout => { + let value = pragma_value_i64(&value)?; + let value = u64::try_from(value).map_err(|_| { + DbError::sql("PRAGMA busy_timeout requires a non-negative integer") + })?; + self.inner.busy_timeout_ms.store(value, Ordering::Release); + Ok(QueryResult::with_affected_rows(0)) + } + } + } +} diff --git a/crates/decentdb/src/db/prepared_fast_paths.rs b/crates/decentdb/src/db/prepared_fast_paths.rs new file mode 100644 index 00000000..490532ff --- /dev/null +++ b/crates/decentdb/src/db/prepared_fast_paths.rs @@ -0,0 +1,2162 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +impl Db { + pub(crate) fn prepared_plan_cache_entries(&self) -> Result> { + self.inner + .prepared_plan_cache + .lock() + .map(|cache| cache.snapshot_entries()) + .map_err(|_| DbError::internal("prepared plan cache lock poisoned")) + } + pub(crate) fn prepared_insert_uses_direct_positional_params( + prepared_insert: &PreparedSimpleInsert, + param_count: usize, + ) -> bool { + prepared_insert.direct_positional_param_count == Some(param_count) + } + pub(crate) fn prepared_statement_cache_key(prepared: &PreparedStatement) -> usize { + if let Some(insert) = prepared.prepared_insert.as_ref() { + return Arc::as_ptr(insert) as usize; + } + Arc::as_ptr(&prepared.statement) as usize + } + pub(crate) fn prepared_insert_plan_for_runtime_state( + &self, + prepared: &PreparedStatement, + runtime: &mut EngineRuntime, + snapshot_lsn: u64, + indexes_maybe_stale: &mut bool, + prepared_insert_runtime_cache: &mut HashMap>, + ) -> Result>> { + let Some(prepared_insert) = prepared.prepared_insert.as_ref() else { + return Ok(None); + }; + + if *indexes_maybe_stale { + runtime.rebuild_stale_indexes(self.inner.config.page_size)?; + *indexes_maybe_stale = false; + } + + let cache_key = Self::prepared_statement_cache_key(prepared); + if let Some(plan) = prepared_insert_runtime_cache.get(&cache_key) { + if Self::prepared_insert_target_loaded(runtime, plan) { + return Ok(Some(Arc::clone(plan))); + } + prepared_insert_runtime_cache.remove(&cache_key); + } + + let needs_refresh = + prepared_insert.use_generic_validation || prepared_insert.use_generic_index_updates; + if !needs_refresh + && runtime.can_reuse_prepared_simple_insert(prepared_insert) + && Self::prepared_insert_target_loaded(runtime, prepared_insert) + { + prepared_insert_runtime_cache.insert(cache_key, Arc::clone(prepared_insert)); + return Ok(Some(Arc::clone(prepared_insert))); + } + + let table_names = + self.insert_dependency_table_names(runtime, &prepared_insert.table_name)?; + let table_refs = table_names.iter().map(String::as_str).collect::>(); + self.load_runtime_table_row_sources_at_snapshot(runtime, &table_refs, snapshot_lsn)?; + + if !needs_refresh && runtime.can_reuse_prepared_simple_insert(prepared_insert) { + prepared_insert_runtime_cache.insert(cache_key, Arc::clone(prepared_insert)); + return Ok(Some(Arc::clone(prepared_insert))); + } + + let SqlStatement::Insert(insert) = prepared.statement.as_ref() else { + return Ok(None); + }; + let Some(refreshed) = runtime.prepare_simple_insert(insert)? else { + return Ok(None); + }; + let refreshed = Arc::new(refreshed); + if runtime.can_reuse_prepared_simple_insert(refreshed.as_ref()) { + prepared_insert_runtime_cache.insert(cache_key, Arc::clone(&refreshed)); + } + Ok(Some(refreshed)) + } + pub(crate) fn prepared_insert_changes_persistent_table( + _runtime: &EngineRuntime, + prepared_insert: &PreparedSimpleInsert, + ) -> bool { + prepared_insert.catalog_table_name.is_some() + } + fn prepared_insert_target_loaded( + runtime: &EngineRuntime, + prepared_insert: &PreparedSimpleInsert, + ) -> bool { + if let Some(table_name) = prepared_insert.catalog_table_name.as_deref() { + runtime.tables.contains_key(table_name) + } else { + runtime.prepared_insert_target_loaded(&prepared_insert.table_name) + } + } + pub(crate) fn try_execute_prepared_simple_ordered_row_id_projection( + &self, + prepared: &PreparedStatement, + ) -> Result> { + if self.inner.sql_txn_active.load(Ordering::Acquire) + || self.inner.config.extension_unsigned_development_mode + || !self.inner.config.extension_trust_anchors.is_empty() + { + return Ok(None); + } + let Some(plan) = prepared.simple_ordered_row_id_projection.as_ref() else { + return Ok(None); + }; + let Some(runtime) = self.try_resident_read_for_single_process_statement( + prepared.statement.as_ref(), + Some(prepared), + )? + else { + return Ok(None); + }; + let result = runtime.execute_resolved_simple_ordered_row_id_projection( + ResolvedSimpleOrderedRowIdProjectionRequest { + table_name: plan.table_name.as_str(), + order_column: plan.order_column.as_str(), + projection_indexes: &plan.projection_indexes, + column_names: Arc::clone(&plan.column_names), + limit: plan.limit, + offset: plan.offset, + descending: plan.descending, + }, + )?; + drop(runtime); + if let Some(result) = result { + return self + .finalize_row_source_autocommit_statement(prepared.statement.as_ref(), Ok(result)) + .map(Some); + } + Ok(None) + } + pub(crate) fn try_execute_prepared_simple_row_id_projection( + &self, + prepared: &PreparedStatement, + params: &[Value], + ) -> Result> { + if self.inner.sql_txn_active.load(Ordering::Acquire) { + return Ok(None); + } + let Some(plan) = prepared.simple_row_id_projection.as_ref() else { + return Ok(None); + }; + let Some(Value::Int64(lookup_row_id)) = params.get(plan.param_index) else { + return Ok(None); + }; + + if !self.inner.config.extension_unsigned_development_mode + && self.inner.config.extension_trust_anchors.is_empty() + { + if let Some(runtime) = self.try_resident_read_for_prepared_table_statement( + prepared, + plan.table_name.as_str(), + )? { + let result = runtime.execute_resolved_simple_row_id_projection_at_snapshot( + ResolvedSimpleRowIdProjectionRequest { + table_name: plan.table_name.as_str(), + projection_indexes: &plan.projection_indexes, + column_names: Arc::clone(&plan.column_names), + lookup_row_id: *lookup_row_id, + pager: &self.inner.pager, + wal: &self.inner.wal, + snapshot_lsn: 0, + use_persistent_pk_index: self.inner.config.persistent_pk_index, + }, + )?; + drop(runtime); + if let Some(result) = result { + return Ok(Some(result)); + } + } + } + + let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + if let Some(runtime) = self.runtime_read_for_prepared_row_sources_at_snapshot( + &[plan.table_name.as_str()], + snapshot_lsn, + )? { + self.validate_prepared_schema_cookie( + prepared, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + let result = runtime.execute_resolved_simple_row_id_projection_at_snapshot( + ResolvedSimpleRowIdProjectionRequest { + table_name: plan.table_name.as_str(), + projection_indexes: &plan.projection_indexes, + column_names: Arc::clone(&plan.column_names), + lookup_row_id: *lookup_row_id, + pager: &self.inner.pager, + wal: &self.inner.wal, + snapshot_lsn, + use_persistent_pk_index: self.inner.config.persistent_pk_index, + }, + )?; + if result.is_some() { + drop(runtime); + drop(reader); + return Ok(result); + } + drop(runtime); + } + self.refresh_engine_from_snapshot(snapshot_lsn)?; + self.try_load_prepared_read_row_sources_at_snapshot( + &[plan.table_name.as_str()], + snapshot_lsn, + )?; + let Some(runtime) = self.runtime_read_for_fast_read_at_snapshot(snapshot_lsn)? else { + drop(reader); + return Ok(None); + }; + self.validate_prepared_schema_cookie( + prepared, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + let result = runtime.execute_resolved_simple_row_id_projection_at_snapshot( + ResolvedSimpleRowIdProjectionRequest { + table_name: plan.table_name.as_str(), + projection_indexes: &plan.projection_indexes, + column_names: Arc::clone(&plan.column_names), + lookup_row_id: *lookup_row_id, + pager: &self.inner.pager, + wal: &self.inner.wal, + snapshot_lsn, + use_persistent_pk_index: self.inner.config.persistent_pk_index, + }, + )?; + drop(runtime); + drop(reader); + Ok(result) + } + pub(crate) fn try_execute_prepared_simple_indexed_projection( + &self, + prepared: &PreparedStatement, + params: &[Value], + ) -> Result> { + if self.inner.sql_txn_active.load(Ordering::Acquire) { + return Ok(None); + } + let Some(plan) = prepared.simple_indexed_projection.as_ref() else { + return Ok(None); + }; + + if !self.inner.config.extension_unsigned_development_mode + && self.inner.config.extension_trust_anchors.is_empty() + { + if let Some(runtime) = self.try_resident_read_for_single_process_statement( + prepared.statement.as_ref(), + Some(prepared), + )? { + let result = self.execute_prepared_simple_indexed_projection_in_runtime( + &runtime, plan, params, + )?; + drop(runtime); + if let Some(result) = result { + return self + .finalize_row_source_autocommit_statement( + prepared.statement.as_ref(), + Ok(result), + ) + .map(Some); + } + } + } + + let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + if let Some(runtime) = self.runtime_read_for_prepared_row_sources_at_snapshot( + &[plan.table_name.as_str()], + snapshot_lsn, + )? { + self.validate_prepared_schema_cookie( + prepared, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + let result = + self.execute_prepared_simple_indexed_projection_in_runtime(&runtime, plan, params)?; + if result.is_some() { + drop(runtime); + drop(reader); + return Ok(result); + } + drop(runtime); + } + + self.refresh_engine_from_snapshot(snapshot_lsn)?; + self.try_load_prepared_read_row_sources_at_snapshot( + &[plan.table_name.as_str()], + snapshot_lsn, + )?; + let Some(runtime) = self.runtime_read_for_fast_read_at_snapshot(snapshot_lsn)? else { + drop(reader); + return Ok(None); + }; + self.validate_prepared_schema_cookie( + prepared, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + let result = + self.execute_prepared_simple_indexed_projection_in_runtime(&runtime, plan, params)?; + drop(runtime); + drop(reader); + Ok(result) + } + pub(crate) fn try_execute_prepared_simple_row_id_range_projection( + &self, + prepared: &PreparedStatement, + params: &[Value], + ) -> Result> { + if self.inner.sql_txn_active.load(Ordering::Acquire) { + return Ok(None); + } + let Some(plan) = prepared.simple_row_id_range_projection.as_ref() else { + return Ok(None); + }; + let lower_bound = if let Some(bound) = plan.lower_bound { + let Some(Value::Int64(value)) = params.get(bound.param_index) else { + return Ok(None); + }; + Some(SimpleRangeBoundValue { + inclusive: bound.inclusive, + value: Value::Int64(*value), + }) + } else { + None + }; + let upper_bound = if let Some(bound) = plan.upper_bound { + let Some(Value::Int64(value)) = params.get(bound.param_index) else { + return Ok(None); + }; + Some(SimpleRangeBoundValue { + inclusive: bound.inclusive, + value: Value::Int64(*value), + }) + } else { + None + }; + let Some(Value::Int64(limit_value)) = params.get(plan.limit_param_index) else { + return Ok(None); + }; + let limit = Some(usize::try_from((*limit_value).max(0)).unwrap_or(usize::MAX)); + + let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + if let Some(runtime) = self.runtime_read_for_prepared_row_sources_at_snapshot( + &[plan.table_name.as_str()], + snapshot_lsn, + )? { + self.validate_prepared_schema_cookie( + prepared, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + let result = runtime.execute_resolved_simple_row_id_range_projection_at_snapshot( + ResolvedSimpleRowIdRangeProjectionRequest { + table_name: plan.table_name.as_str(), + projection_indexes: &plan.projection_indexes, + column_names: Arc::clone(&plan.column_names), + filter_column: plan.filter_column.as_str(), + lower_bound: lower_bound.clone(), + upper_bound: upper_bound.clone(), + limit, + pager: &self.inner.pager, + wal: &self.inner.wal, + snapshot_lsn, + use_persistent_pk_index: self.inner.config.persistent_pk_index, + }, + )?; + if result.is_some() { + drop(runtime); + drop(reader); + return Ok(result); + } + drop(runtime); + } + self.refresh_engine_from_snapshot(snapshot_lsn)?; + self.try_load_prepared_read_row_sources_at_snapshot( + &[plan.table_name.as_str()], + snapshot_lsn, + )?; + let Some(runtime) = self.runtime_read_for_fast_read_at_snapshot(snapshot_lsn)? else { + drop(reader); + return Ok(None); + }; + self.validate_prepared_schema_cookie( + prepared, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + let result = runtime.execute_resolved_simple_row_id_range_projection_at_snapshot( + ResolvedSimpleRowIdRangeProjectionRequest { + table_name: plan.table_name.as_str(), + projection_indexes: &plan.projection_indexes, + column_names: Arc::clone(&plan.column_names), + filter_column: plan.filter_column.as_str(), + lower_bound, + upper_bound, + limit, + pager: &self.inner.pager, + wal: &self.inner.wal, + snapshot_lsn, + use_persistent_pk_index: self.inner.config.persistent_pk_index, + }, + )?; + drop(runtime); + drop(reader); + Ok(result) + } + pub(crate) fn try_execute_prepared_simple_row_id_join_projection( + &self, + prepared: &PreparedStatement, + params: &[Value], + ) -> Result> { + if self.inner.sql_txn_active.load(Ordering::Acquire) { + return Ok(None); + } + let Some(plan) = prepared.simple_row_id_join_projection.as_ref() else { + return Ok(None); + }; + let Some(Value::Int64(lookup_row_id)) = params.get(plan.param_index) else { + return Ok(None); + }; + + let join_tables = [ + plan.left_table_name.as_str(), + plan.right_table_name.as_str(), + ]; + let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + if let Some(runtime) = + self.runtime_read_for_prepared_row_sources_at_snapshot(&join_tables, snapshot_lsn)? + { + self.validate_prepared_schema_cookie( + prepared, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + let result = runtime.execute_resolved_simple_row_id_join_projection_at_snapshot( + ResolvedSimpleRowIdJoinProjectionRequest { + left_table_name: plan.left_table_name.as_str(), + right_table_name: plan.right_table_name.as_str(), + left_projection_indexes: &plan.left_projection_indexes, + right_projection_indexes: &plan.right_projection_indexes, + projections: &plan.projections, + column_names: Arc::clone(&plan.column_names), + lookup_row_id: *lookup_row_id, + pager: &self.inner.pager, + wal: &self.inner.wal, + snapshot_lsn, + use_persistent_pk_index: self.inner.config.persistent_pk_index, + }, + )?; + if result.is_some() { + drop(runtime); + drop(reader); + return Ok(result); + } + drop(runtime); + } + self.refresh_engine_from_snapshot(snapshot_lsn)?; + self.try_load_prepared_read_row_sources_at_snapshot(&join_tables, snapshot_lsn)?; + let Some(runtime) = self.runtime_read_for_fast_read_at_snapshot(snapshot_lsn)? else { + drop(reader); + return Ok(None); + }; + self.validate_prepared_schema_cookie( + prepared, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + let result = runtime.execute_resolved_simple_row_id_join_projection_at_snapshot( + ResolvedSimpleRowIdJoinProjectionRequest { + left_table_name: plan.left_table_name.as_str(), + right_table_name: plan.right_table_name.as_str(), + left_projection_indexes: &plan.left_projection_indexes, + right_projection_indexes: &plan.right_projection_indexes, + projections: &plan.projections, + column_names: Arc::clone(&plan.column_names), + lookup_row_id: *lookup_row_id, + pager: &self.inner.pager, + wal: &self.inner.wal, + snapshot_lsn, + use_persistent_pk_index: self.inner.config.persistent_pk_index, + }, + )?; + drop(runtime); + drop(reader); + Ok(result) + } + pub(crate) fn try_execute_prepared_simple_scalar_filtered_aggregate( + &self, + prepared: &PreparedStatement, + params: &[Value], + ) -> Result> { + if self.inner.sql_txn_active.load(Ordering::Acquire) { + return Ok(None); + } + let Some(plan) = prepared.simple_scalar_filtered_aggregate.as_ref() else { + return Ok(None); + }; + let Some(Value::Int64(param_value)) = params.get(plan.param_index) else { + return Ok(None); + }; + let SqlStatement::Query(query) = prepared.statement.as_ref() else { + return Ok(None); + }; + + let reader = self.inner.wal.begin_reader_with_pager(&self.inner.pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + self.refresh_engine_from_snapshot(snapshot_lsn)?; + let Some(runtime) = self.runtime_read_for_fast_read_at_snapshot(snapshot_lsn)? else { + return Ok(None); + }; + self.validate_prepared_schema_cookie( + prepared, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + let has_resident_source = runtime.table_row_source(plan.table_name.as_str()).is_some(); + if !has_resident_source && !runtime.has_deferred_tables() { + return Ok(None); + } + let state = runtime.persisted_table_state(plan.table_name.as_str()); + if !has_resident_source && state.is_none() { + return Ok(None); + }; + let state = state.unwrap_or_default(); + let key = PreparedScalarAggregateCacheKey { + snapshot_lsn, + pointer_head_page_id: state.pointer.head_page_id, + pointer_logical_len: state.pointer.logical_len, + pointer_flags: state.pointer.flags, + checksum: state.checksum, + row_count: state.row_count, + param_value: *param_value, + }; + if let Some(result) = plan + .cache + .lock() + .map_err(|_| DbError::internal("prepared aggregate cache lock poisoned"))? + .get(&key) + { + drop(runtime); + drop(reader); + return Ok(Some(result)); + } + + let result = if has_resident_source { + runtime.try_execute_simple_grouped_numeric_aggregate_query(query, params)? + } else { + runtime.try_execute_simple_deferred_paged_grouped_numeric_aggregate_query( + query, + params, + &self.inner.pager, + &self.inner.wal, + snapshot_lsn, + )? + }; + if let Some(result) = result.as_ref() { + plan.cache + .lock() + .map_err(|_| DbError::internal("prepared aggregate cache lock poisoned"))? + .insert(key, result.clone()); + } + drop(runtime); + drop(reader); + Ok(result) + } + pub(crate) fn execute_autocommit_temp_only_statement( + &self, + statement: &crate::sql::ast::Statement, + params: &[Value], + ) -> Result { + let mut working = self.engine_snapshot()?; + let result = working.execute_statement(statement, params, self.inner.config.page_size)?; + self.install_temp_runtime(working)?; + Ok(result) + } + pub(crate) fn execute_autocommit_insert_in_place( + &self, + statement: &crate::sql::ast::Statement, + params: &[Value], + ) -> Result { + let insert_table_names = if let crate::sql::ast::Statement::Insert(insert) = statement { + let table_names = { + self.refresh_engine_from_storage()?; + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + self.insert_dependency_table_names(&runtime, &insert.table_name)? + }; + let table_refs = table_names.iter().map(String::as_str).collect::>(); + self.load_simple_write_row_sources_at_latest_snapshot(&table_refs)?; + Some(table_names) + } else { + None + }; + let result = self.execute_autocommit_in_place(|runtime| { + runtime.execute_statement(statement, params, self.inner.config.page_size) + })?; + if let Some(table_names) = &insert_table_names { + let table_refs = table_names.iter().map(String::as_str).collect::>(); + self.redefer_persisted_tables_after_write(&table_refs)?; + } + Ok(result) + } + pub(crate) fn execute_autocommit_prepared_insert_in_place( + &self, + prepared: &PreparedSimpleInsert, + params: &[Value], + ) -> Result { + let table_names = { + self.refresh_engine_from_storage()?; + let runtime = self + .inner + .engine + .read() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + self.insert_dependency_table_names(&runtime, &prepared.table_name)? + }; + let table_refs = table_names.iter().map(String::as_str).collect::>(); + self.load_simple_write_row_sources_at_latest_snapshot(&table_refs)?; + let mut runtime = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + self.configure_runtime_sync_capture(&mut runtime)?; + if !runtime.can_reuse_prepared_simple_insert(prepared) { + drop(runtime); + let result = self.execute_autocommit_in_place(|runtime| { + runtime.execute_prepared_simple_insert( + prepared, + params, + self.inner.config.page_size, + ) + })?; + self.redefer_persisted_tables_after_write(&table_refs)?; + return Ok(result); + } + let result = match runtime.execute_prepared_simple_insert( + prepared, + params, + self.inner.config.page_size, + ) { + Ok(result) => result, + Err(error) => { + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + runtime.rebuild_stale_indexes(self.inner.config.page_size)?; + let reactive_pending = self.take_reactive_pending_commit(&mut runtime); + self.begin_write()?; + if let Err(error) = runtime.persist_to_db(self) { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + let committed_lsn = match self.commit() { + Ok(lsn) => lsn, + Err(error) => { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + if !runtime.sync_mutations.is_empty() { + self.sync_post_commit(&mut runtime, committed_lsn)?; + } + self.sync_temp_state_from_runtime(&runtime)?; + self.inner + .last_runtime_lsn + .store(committed_lsn, Ordering::Release); + self.inner + .writer_last_commit_lsn + .store(committed_lsn, Ordering::Release); + drop(runtime); + self.publish_reactive_commit(reactive_pending, committed_lsn); + self.redefer_persisted_tables_after_write(&table_refs)?; + Ok(result) + } + pub(crate) fn execute_autocommit_simple_update_in_place( + &self, + prepared_update: &PreparedSimpleUpdate, + params: &[Value], + ) -> Result { + self.load_simple_write_row_sources_at_latest_snapshot(&[prepared_update + .table_name + .as_str()])?; + let mut runtime = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + self.configure_runtime_sync_capture(&mut runtime)?; + if !runtime.can_reuse_prepared_simple_update(prepared_update) { + drop(runtime); + let result = self.execute_autocommit_in_place(|runtime| { + runtime.execute_prepared_simple_update( + prepared_update, + params, + self.inner.config.page_size, + ) + })?; + self.redefer_persisted_tables_after_write(&[prepared_update.table_name.as_str()])?; + return Ok(result); + } + let result = match runtime.execute_prepared_simple_update( + prepared_update, + params, + self.inner.config.page_size, + ) { + Ok(result) => result, + Err(error) => { + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + if result.affected_rows() == 0 && !self.runtime_has_persistent_commit_work(&runtime)? { + self.sync_temp_state_from_runtime(&runtime)?; + drop(runtime); + self.redefer_persisted_tables_after_write(&[prepared_update.table_name.as_str()])?; + return Ok(result); + } + if !prepared_update + .indexes + .iter() + .all(|index| runtime.prepared_btree_index_is_fresh(index)) + { + runtime.rebuild_stale_indexes(self.inner.config.page_size)?; + } + let reactive_pending = self.take_reactive_pending_commit(&mut runtime); + self.begin_write()?; + if let Err(error) = runtime.persist_to_db(self) { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + let committed_lsn = match self.commit() { + Ok(lsn) => lsn, + Err(error) => { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + if !runtime.sync_mutations.is_empty() { + self.sync_post_commit(&mut runtime, committed_lsn)?; + } + self.sync_temp_state_from_runtime(&runtime)?; + self.inner + .last_runtime_lsn + .store(committed_lsn, Ordering::Release); + self.inner + .writer_last_commit_lsn + .store(committed_lsn, Ordering::Release); + drop(runtime); + self.publish_reactive_commit(reactive_pending, committed_lsn); + self.redefer_persisted_tables_after_write(&[prepared_update.table_name.as_str()])?; + Ok(result) + } + pub(crate) fn execute_autocommit_simple_delete_in_place( + &self, + prepared_delete: &PreparedSimpleDelete, + params: &[Value], + ) -> Result { + let table_names = prepared_delete.affected_table_names(); + let row_source_table_names = prepared_delete.required_row_source_table_names(); + let child_index_targets = prepared_delete.child_index_hydration_targets(); + self.load_simple_write_row_sources_and_child_indexes_at_latest_snapshot( + &row_source_table_names, + &child_index_targets, + )?; + let mut runtime = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + self.configure_runtime_sync_capture(&mut runtime)?; + if !runtime.can_reuse_prepared_simple_delete(prepared_delete) { + drop(runtime); + let result = self.execute_autocommit_in_place(|runtime| { + runtime.execute_prepared_simple_delete( + prepared_delete, + params, + self.inner.config.page_size, + ) + })?; + self.redefer_persisted_tables_after_write(&table_names)?; + return Ok(result); + } + let result = match runtime.execute_prepared_simple_delete( + prepared_delete, + params, + self.inner.config.page_size, + ) { + Ok(result) => result, + Err(error) => { + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + if result.affected_rows() == 0 && !self.runtime_has_persistent_commit_work(&runtime)? { + self.sync_temp_state_from_runtime(&runtime)?; + drop(runtime); + self.redefer_persisted_tables_after_write(&table_names)?; + return Ok(result); + } + runtime.rebuild_stale_indexes(self.inner.config.page_size)?; + let reactive_pending = self.take_reactive_pending_commit(&mut runtime); + self.begin_write()?; + if let Err(error) = runtime.persist_to_db(self) { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + let committed_lsn = match self.commit() { + Ok(lsn) => lsn, + Err(error) => { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + if !runtime.sync_mutations.is_empty() { + self.sync_post_commit(&mut runtime, committed_lsn)?; + } + self.sync_temp_state_from_runtime(&runtime)?; + self.inner + .last_runtime_lsn + .store(committed_lsn, Ordering::Release); + self.inner + .writer_last_commit_lsn + .store(committed_lsn, Ordering::Release); + drop(runtime); + self.publish_reactive_commit(reactive_pending, committed_lsn); + self.redefer_persisted_tables_after_write(&table_names)?; + Ok(result) + } + pub(crate) fn try_execute_autocommit_prepared_insert_in_place( + &self, + prepared_statement: &PreparedStatement, + prepared_insert: &PreparedSimpleInsert, + params: &[Value], + ) -> Result> { + if !self.can_use_autocommit_prepared_insert_fast_path(&prepared_insert.table_name)? { + return Ok(None); + } + let single_table = [prepared_insert.table_name.as_str()]; + let mut dependency_tables = Vec::new(); + let table_refs: &[&str] = if prepared_insert.row_source_dependency_tables.is_empty() { + &single_table + } else { + dependency_tables.reserve(prepared_insert.row_source_dependency_tables.len() + 1); + dependency_tables.push(prepared_insert.table_name.as_str()); + for parent_table_name in &prepared_insert.row_source_dependency_tables { + if !dependency_tables + .iter() + .any(|name| identifiers_equal(name, parent_table_name)) + { + dependency_tables.push(parent_table_name.as_str()); + } + } + &dependency_tables + }; + self.load_simple_write_row_sources_at_latest_snapshot(table_refs)?; + let mut runtime = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + self.configure_runtime_sync_capture(&mut runtime)?; + self.validate_prepared_schema_cookie( + prepared_statement, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + if !runtime.can_reuse_prepared_simple_insert(prepared_insert) { + return Ok(None); + } + let result = match runtime.execute_prepared_simple_insert( + prepared_insert, + params, + self.inner.config.page_size, + ) { + Ok(result) => result, + Err(error) => { + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + runtime.rebuild_stale_indexes(self.inner.config.page_size)?; + let reactive_pending = self.take_reactive_pending_commit(&mut runtime); + self.begin_write()?; + if let Err(error) = runtime.persist_to_db(self) { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + let committed_lsn = match self.commit() { + Ok(lsn) => lsn, + Err(error) => { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + if !runtime.sync_mutations.is_empty() { + self.sync_post_commit(&mut runtime, committed_lsn)?; + } + self.sync_temp_state_from_runtime(&runtime)?; + self.inner + .last_runtime_lsn + .store(committed_lsn, Ordering::Release); + self.inner + .writer_last_commit_lsn + .store(committed_lsn, Ordering::Release); + drop(runtime); + self.publish_reactive_commit(reactive_pending, committed_lsn); + self.redefer_persisted_tables_after_write(table_refs)?; + Ok(Some(result)) + } + pub(crate) fn try_execute_autocommit_prepared_insert_in_place_mut( + &self, + prepared_statement: &PreparedStatement, + prepared_insert: &PreparedSimpleInsert, + params: &mut [Value], + ) -> Result> { + if !self.can_use_autocommit_prepared_insert_fast_path(&prepared_insert.table_name)? + || !Self::prepared_insert_uses_direct_positional_params(prepared_insert, params.len()) + { + return Ok(None); + } + let single_table = [prepared_insert.table_name.as_str()]; + let mut dependency_tables = Vec::new(); + let table_refs: &[&str] = if prepared_insert.row_source_dependency_tables.is_empty() { + &single_table + } else { + dependency_tables.reserve(prepared_insert.row_source_dependency_tables.len() + 1); + dependency_tables.push(prepared_insert.table_name.as_str()); + for parent_table_name in &prepared_insert.row_source_dependency_tables { + if !dependency_tables + .iter() + .any(|name| identifiers_equal(name, parent_table_name)) + { + dependency_tables.push(parent_table_name.as_str()); + } + } + &dependency_tables + }; + self.load_simple_write_row_sources_at_latest_snapshot(table_refs)?; + let mut runtime = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + self.configure_runtime_sync_capture(&mut runtime)?; + self.validate_prepared_schema_cookie( + prepared_statement, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + if !runtime.can_reuse_prepared_simple_insert(prepared_insert) { + return Ok(None); + } + let affected = match runtime.execute_prepared_simple_insert_positional_params_in_place( + prepared_insert, + params, + self.inner.config.page_size, + ) { + Ok(affected) => affected, + Err(error) => { + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + runtime.rebuild_stale_indexes(self.inner.config.page_size)?; + let reactive_pending = self.take_reactive_pending_commit(&mut runtime); + self.begin_write()?; + if let Err(error) = runtime.persist_to_db(self) { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + let committed_lsn = match self.commit() { + Ok(lsn) => lsn, + Err(error) => { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + if !runtime.sync_mutations.is_empty() { + self.sync_post_commit(&mut runtime, committed_lsn)?; + } + self.sync_temp_state_from_runtime(&runtime)?; + self.inner + .last_runtime_lsn + .store(committed_lsn, Ordering::Release); + self.inner + .writer_last_commit_lsn + .store(committed_lsn, Ordering::Release); + let redefer_after_write = + self.runtime_should_redefer_persisted_tables_after_write(&runtime, table_refs); + drop(runtime); + self.publish_reactive_commit(reactive_pending, committed_lsn); + if redefer_after_write { + self.redefer_persisted_tables_after_write(table_refs)?; + } + Ok(Some(QueryResult::with_affected_rows(affected))) + } + pub(crate) fn try_execute_autocommit_prepared_update_in_place( + &self, + prepared_statement: &PreparedStatement, + prepared_update: &PreparedSimpleUpdate, + params: &[Value], + ) -> Result> { + self.load_simple_write_row_sources_at_latest_snapshot(&[prepared_update + .table_name + .as_str()])?; + let mut runtime = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + self.configure_runtime_sync_capture(&mut runtime)?; + self.validate_prepared_schema_cookie( + prepared_statement, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + if !runtime.can_reuse_prepared_simple_update(prepared_update) { + return Ok(None); + } + let result = match runtime.execute_prepared_simple_update( + prepared_update, + params, + self.inner.config.page_size, + ) { + Ok(result) => result, + Err(error) => { + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + if result.affected_rows() == 0 && !self.runtime_has_persistent_commit_work(&runtime)? { + self.sync_temp_state_from_runtime(&runtime)?; + drop(runtime); + self.redefer_persisted_tables_after_write(&[prepared_update.table_name.as_str()])?; + return Ok(Some(result)); + } + if !prepared_update + .indexes + .iter() + .all(|index| runtime.prepared_btree_index_is_fresh(index)) + { + runtime.rebuild_stale_indexes(self.inner.config.page_size)?; + } + let reactive_pending = self.take_reactive_pending_commit(&mut runtime); + self.begin_write()?; + if let Err(error) = runtime.persist_to_db(self) { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + let committed_lsn = match self.commit() { + Ok(lsn) => lsn, + Err(error) => { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + if !runtime.sync_mutations.is_empty() { + self.sync_post_commit(&mut runtime, committed_lsn)?; + } + let runtime_schema_cookie = runtime.catalog.schema_cookie; + if self.inner.catalog.schema_cookie()? != runtime_schema_cookie { + self.inner + .catalog + .replace(runtime.catalog.as_ref().clone())?; + } + self.sync_temp_state_from_runtime(&runtime)?; + self.inner + .last_runtime_lsn + .store(committed_lsn, Ordering::Release); + self.inner + .writer_last_commit_lsn + .store(committed_lsn, Ordering::Release); + drop(runtime); + self.publish_reactive_commit(reactive_pending, committed_lsn); + self.redefer_persisted_tables_after_write(&[prepared_update.table_name.as_str()])?; + Ok(Some(result)) + } + pub(crate) fn try_execute_autocommit_prepared_delete_in_place( + &self, + prepared_statement: &PreparedStatement, + prepared_delete: &PreparedSimpleDelete, + params: &[Value], + ) -> Result> { + if let Some(result) = self.try_execute_zero_row_index_delete_against_current_runtime( + prepared_statement, + prepared_delete, + params, + )? { + return Ok(Some(result)); + } + let table_names = prepared_delete.affected_table_names(); + let row_source_table_names = prepared_delete.required_row_source_table_names(); + let child_index_targets = prepared_delete.child_index_hydration_targets(); + self.load_simple_write_row_sources_and_child_indexes_at_latest_snapshot( + &row_source_table_names, + &child_index_targets, + )?; + let mut runtime = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + self.configure_runtime_sync_capture(&mut runtime)?; + self.validate_prepared_schema_cookie( + prepared_statement, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + if !runtime.can_reuse_prepared_simple_delete(prepared_delete) { + return Ok(None); + } + let result = match runtime.execute_prepared_simple_delete( + prepared_delete, + params, + self.inner.config.page_size, + ) { + Ok(result) => result, + Err(error) => { + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + if result.affected_rows() == 0 && !self.runtime_has_persistent_commit_work(&runtime)? { + self.sync_temp_state_from_runtime(&runtime)?; + drop(runtime); + self.redefer_persisted_tables_after_write(&table_names)?; + return Ok(Some(result)); + } + runtime.rebuild_stale_indexes(self.inner.config.page_size)?; + let reactive_pending = self.take_reactive_pending_commit(&mut runtime); + self.begin_write()?; + if let Err(error) = runtime.persist_to_db(self) { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + let committed_lsn = match self.commit() { + Ok(lsn) => lsn, + Err(error) => { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + if !runtime.sync_mutations.is_empty() { + self.sync_post_commit(&mut runtime, committed_lsn)?; + } + let runtime_schema_cookie = runtime.catalog.schema_cookie; + if self.inner.catalog.schema_cookie()? != runtime_schema_cookie { + self.inner + .catalog + .replace(runtime.catalog.as_ref().clone())?; + } + self.sync_temp_state_from_runtime(&runtime)?; + self.inner + .last_runtime_lsn + .store(committed_lsn, Ordering::Release); + self.inner + .writer_last_commit_lsn + .store(committed_lsn, Ordering::Release); + drop(runtime); + self.publish_reactive_commit(reactive_pending, committed_lsn); + self.redefer_persisted_tables_after_write(&table_names)?; + Ok(Some(result)) + } + pub(crate) fn execute_autocommit_in_place(&self, apply: F) -> Result + where + F: FnOnce(&mut EngineRuntime) -> Result, + { + let mut runtime = self + .inner + .engine + .write() + .map_err(|_| DbError::internal("engine runtime lock poisoned"))?; + self.configure_runtime_sync_capture(&mut runtime)?; + let result = match apply(&mut runtime) { + Ok(result) => result, + Err(error) => { + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + if !self.runtime_has_persistent_commit_work(&runtime)? + && runtime.sync_mutations.is_empty() + && !Self::runtime_has_stale_indexes(&runtime) + { + self.sync_temp_state_from_runtime(&runtime)?; + return Ok(result); + } + runtime.rebuild_stale_indexes(self.inner.config.page_size)?; + let reactive_pending = self.take_reactive_pending_commit(&mut runtime); + self.begin_write()?; + if let Err(error) = runtime.persist_to_db(self) { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + let committed_lsn = match self.commit() { + Ok(lsn) => lsn, + Err(error) => { + let _ = self.rollback(); + self.restore_runtime_from_storage(&mut runtime)?; + return Err(error); + } + }; + self.sync_post_commit(&mut runtime, committed_lsn)?; + let runtime_schema_cookie = runtime.catalog.schema_cookie; + if self.inner.catalog.schema_cookie()? != runtime_schema_cookie { + self.inner + .catalog + .replace(runtime.catalog.as_ref().clone())?; + } + self.sync_temp_state_from_runtime(&runtime)?; + self.inner + .last_runtime_lsn + .store(committed_lsn, Ordering::Release); + self.inner + .writer_last_commit_lsn + .store(committed_lsn, Ordering::Release); + drop(runtime); + self.publish_reactive_commit(reactive_pending, committed_lsn); + Ok(result) + } + pub(crate) fn try_prepare_from_plan_cache( + &self, + prepared_sql: &str, + ) -> Result> { + let persistent_cookie = self.inner.catalog.schema_cookie()?; + let temp_cookie = self + .inner + .temp_state + .lock() + .map_err(|_| DbError::internal("temp schema lock poisoned"))? + .schema_cookie; + let policy_gen = self.inner.policy_mask_generation.current(); + let key = crate::plan_cache::PlanCacheKey::new( + prepared_sql.to_string(), + parameter_shape_for_prepared_sql(prepared_sql), + persistent_cookie, + temp_cookie, + policy_gen, + ); + let Some(bundle) = self + .inner + .prepared_plan_cache + .lock() + .map_err(|_| DbError::internal("prepared plan cache lock poisoned"))? + .get(&key, persistent_cookie, temp_cookie, policy_gen) + else { + return Ok(None); + }; + Ok(Some(PreparedStatement { + db: self.clone(), + schema_cookie: persistent_cookie, + temp_schema_cookie: temp_cookie, + statement: Arc::clone(&bundle.statement), + prepared_sql: prepared_sql.to_string(), + simple_row_id_projection: bundle.simple_row_id_projection, + simple_indexed_projection: bundle.simple_indexed_projection, + simple_row_id_range_projection: bundle.simple_row_id_range_projection, + simple_ordered_row_id_projection: bundle.simple_ordered_row_id_projection, + simple_row_id_join_projection: bundle.simple_row_id_join_projection, + simple_scalar_filtered_aggregate: bundle.simple_scalar_filtered_aggregate, + prepared_insert: bundle.prepared_insert, + prepared_update: bundle.prepared_update, + prepared_delete: bundle.prepared_delete, + read_only: bundle.read_only, + })) + } + pub(crate) fn prepared_simple_row_id_projection( + sql: &str, + runtime: &EngineRuntime, + ) -> Option { + let plan = parse_simple_row_id_projection_sql(sql)?; + if runtime.temp_table_schema(plan.table_name).is_some() + || runtime + .catalog + .views + .keys() + .any(|view_name| identifiers_equal(view_name, plan.table_name)) + { + return None; + } + let table = runtime.catalog.table(plan.table_name)?; + if !row_id_alias_column_name(table) + .is_some_and(|column_name| identifiers_equal(column_name, plan.filter_column)) + { + return None; + } + + let mut projection_indexes = Vec::with_capacity(plan.projection_columns.len()); + let mut column_names = Vec::with_capacity(plan.projection_columns.len()); + for projection_column in plan.projection_columns { + let index = table + .columns + .iter() + .position(|column| identifiers_equal(&column.name, projection_column))?; + projection_indexes.push(index); + column_names.push(projection_column.to_string()); + } + + Some(PreparedSimpleRowIdProjection { + table_name: table.name.clone(), + projection_indexes, + column_names: Arc::from(column_names), + param_index: plan.param_index, + }) + } + pub(crate) fn prepared_simple_indexed_projection( + statement: &SqlStatement, + runtime: &EngineRuntime, + ) -> Option { + let SqlStatement::Query(query) = statement else { + return None; + }; + if !query.ctes.is_empty() + || !query.order_by.is_empty() + || query.limit.is_some() + || query.offset.is_some() + { + return None; + } + let QueryBody::Select(select) = &query.body else { + return None; + }; + if select.distinct + || !select.distinct_on.is_empty() + || !select.group_by.is_empty() + || select.having.is_some() + || select.from.len() != 1 + { + return None; + } + let filter = select.filter.as_ref()?; + let FromItem::Table { name, alias } = &select.from[0] else { + return None; + }; + if runtime.temp_table_schema(name).is_some() + || runtime + .catalog + .views + .keys() + .any(|view_name| identifiers_equal(view_name, name)) + { + return None; + } + let table = runtime.catalog.table(name)?; + if !prepared_table_generated_columns_are_stored(table) { + return None; + } + let binding_name = alias.as_deref().unwrap_or(name); + + let (filter_table, filter_column, value_expr) = match filter { + Expr::Binary { left, op, right } if *op == BinaryOp::Eq => match (&**left, &**right) { + (Expr::Column { table, column }, value_expr) => { + (table.as_deref(), column.as_str(), value_expr) + } + (value_expr, Expr::Column { table, column }) => { + (table.as_deref(), column.as_str(), value_expr) + } + _ => return None, + }, + _ => return None, + }; + if let Some(filter_table) = filter_table { + if !identifiers_equal(filter_table, name) + && !identifiers_equal(filter_table, binding_name) + { + return None; + } + } + let value_source = prepared_simple_value_source(value_expr)?; + + let mut projection_indexes = Vec::with_capacity(select.projection.len()); + let mut column_names = Vec::with_capacity(select.projection.len()); + for item in &select.projection { + match item { + SelectItem::Expr { + expr, + alias: select_alias, + } => { + let Expr::Column { + table: projection_table, + column, + } = expr + else { + return None; + }; + if let Some(projection_table) = projection_table.as_deref() { + if !identifiers_equal(projection_table, name) + && !identifiers_equal(projection_table, binding_name) + { + return None; + } + } + let index = table + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, column))?; + projection_indexes.push(index); + column_names.push(select_alias.clone().unwrap_or_else(|| column.clone())); + } + SelectItem::Wildcard => { + for (index, column) in table.columns.iter().enumerate() { + projection_indexes.push(index); + column_names.push(column.name.clone()); + } + } + SelectItem::QualifiedWildcard(qualified_name) => { + if !identifiers_equal(qualified_name, name) + && !identifiers_equal(qualified_name, binding_name) + { + return None; + } + for (index, column) in table.columns.iter().enumerate() { + projection_indexes.push(index); + column_names.push(column.name.clone()); + } + } + } + } + + let lookup = + if row_id_alias_column_name(table) + .is_some_and(|column_name| identifiers_equal(column_name, filter_column)) + { + PreparedSimpleIndexedProjectionLookup::RowId { value_source } + } else { + let index_name = + runtime + .catalog + .indexes + .values() + .find(|index| { + index.fresh + && index.kind == crate::catalog::IndexKind::Btree + && identifiers_equal(&index.table_name, &table.name) + && index.predicate_sql.is_none() + && index.columns.len() == 1 + && index.columns[0].expression_sql.is_none() + && index.columns[0].column_name.as_ref().is_some_and( + |column_name| identifiers_equal(column_name, filter_column), + ) + }) + .map(|index| index.name.clone())?; + PreparedSimpleIndexedProjectionLookup::Index { + index_name, + value_source, + } + }; + + Some(PreparedSimpleIndexedProjection { + table_name: table.name.clone(), + projection_indexes, + column_names: Arc::from(column_names), + lookup, + }) + } + pub(crate) fn prepared_simple_row_id_range_projection( + sql: &str, + runtime: &EngineRuntime, + ) -> Option { + let plan = parse_simple_row_id_range_projection_sql(sql)?; + if runtime.temp_table_schema(plan.table_name).is_some() + || runtime + .catalog + .views + .keys() + .any(|view_name| identifiers_equal(view_name, plan.table_name)) + { + return None; + } + let table = runtime.catalog.table(plan.table_name)?; + let filter_column_index = table + .columns + .iter() + .position(|column| identifiers_equal(&column.name, plan.filter_column))?; + if !table + .primary_key_columns + .iter() + .any(|column| identifiers_equal(column, plan.filter_column)) + || table.columns[filter_column_index].column_type != ColumnType::Int64 + { + return None; + } + + let mut projection_indexes = Vec::with_capacity(plan.projection_columns.len()); + let mut column_names = Vec::with_capacity(plan.projection_columns.len()); + for projection_column in plan.projection_columns { + let index = table + .columns + .iter() + .position(|column| identifiers_equal(&column.name, projection_column))?; + projection_indexes.push(index); + column_names.push(projection_column.to_string()); + } + + Some(PreparedSimpleRowIdRangeProjection { + table_name: table.name.clone(), + projection_indexes, + column_names: Arc::from(column_names), + filter_column: table.columns[filter_column_index].name.clone(), + lower_bound: plan.lower_bound, + upper_bound: plan.upper_bound, + limit_param_index: plan.limit_param_index, + }) + } + pub(crate) fn prepared_simple_ordered_row_id_projection( + statement: &SqlStatement, + runtime: &EngineRuntime, + ) -> Option { + let SqlStatement::Query(query) = statement else { + return None; + }; + if !query.ctes.is_empty() || query.order_by.len() != 1 { + return None; + } + let crate::sql::ast::QueryBody::Select(select) = &query.body else { + return None; + }; + if select.filter.is_some() + || !select.group_by.is_empty() + || select.having.is_some() + || select.distinct + || !select.distinct_on.is_empty() + || select.from.len() != 1 + { + return None; + } + let crate::sql::ast::FromItem::Table { name, alias } = &select.from[0] else { + return None; + }; + if runtime.temp_table_schema(name).is_some() + || runtime + .catalog + .views + .keys() + .any(|view_name| identifiers_equal(view_name, name)) + { + return None; + } + let table = runtime.catalog.table(name)?; + if !prepared_table_generated_columns_are_stored(table) { + return None; + } + let mut projection_indexes = Vec::with_capacity(select.projection.len()); + let mut column_names = Vec::with_capacity(select.projection.len()); + for item in &select.projection { + let crate::sql::ast::SelectItem::Expr { + expr, + alias: select_alias, + } = item + else { + return None; + }; + let crate::sql::ast::Expr::Column { + table: projection_table, + column, + } = expr + else { + return None; + }; + if !prepared_scalar_column_matches_table(projection_table.as_deref(), name, alias) { + return None; + } + let index = table + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, column))?; + projection_indexes.push(index); + column_names.push(select_alias.clone().unwrap_or_else(|| column.clone())); + } + + let order = &query.order_by[0]; + if order.collation.is_some() { + return None; + } + let crate::sql::ast::Expr::Column { + table: order_table, + column: order_column, + } = &order.expr + else { + return None; + }; + if !prepared_scalar_column_matches_table(order_table.as_deref(), name, alias) { + return None; + } + let order_column_index = table + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, order_column))?; + if !row_id_alias_column_name(table) + .is_some_and(|column_name| identifiers_equal(column_name, order_column)) + || table.columns[order_column_index].column_type != ColumnType::Int64 + { + return None; + } + let limit = match query.limit.as_ref() { + Some(expr) => Some(prepared_usize_literal(expr)?), + None => None, + }; + let offset = match query.offset.as_ref() { + Some(expr) => prepared_usize_literal(expr)?, + None => 0, + }; + Some(PreparedSimpleOrderedRowIdProjection { + table_name: table.name.clone(), + order_column: table.columns[order_column_index].name.clone(), + projection_indexes, + column_names: Arc::from(column_names), + limit, + offset, + descending: order.descending, + }) + } + pub(crate) fn prepared_simple_row_id_join_projection( + statement: &SqlStatement, + runtime: &EngineRuntime, + ) -> Option { + let SqlStatement::Query(query) = statement else { + return None; + }; + if !query.ctes.is_empty() + || !query.order_by.is_empty() + || query.limit.is_some() + || query.offset.is_some() + { + return None; + } + let crate::sql::ast::QueryBody::Select(select) = &query.body else { + return None; + }; + if !select.group_by.is_empty() + || select.having.is_some() + || select.distinct + || !select.distinct_on.is_empty() + || select.from.len() != 1 + { + return None; + } + let filter = select.filter.as_ref()?; + let crate::sql::ast::FromItem::Join { + left, + right, + kind: crate::sql::ast::JoinKind::Inner, + constraint, + } = &select.from[0] + else { + return None; + }; + let crate::sql::ast::FromItem::Table { + name: left_name, + alias: left_alias, + } = &**left + else { + return None; + }; + let crate::sql::ast::FromItem::Table { + name: right_name, + alias: right_alias, + } = &**right + else { + return None; + }; + if runtime.temp_table_schema(left_name).is_some() + || runtime.temp_table_schema(right_name).is_some() + || runtime.catalog.views.keys().any(|view_name| { + identifiers_equal(view_name, left_name) || identifiers_equal(view_name, right_name) + }) + { + return None; + } + let left_schema = runtime.catalog.table(left_name)?; + let right_schema = runtime.catalog.table(right_name)?; + let left_rowid_column = row_id_alias_column_name(left_schema)?; + let right_rowid_column = row_id_alias_column_name(right_schema)?; + + let (join_a, join_b) = prepared_join_column_equality(constraint)?; + let join_a_side = + prepared_join_column_side(join_a.0, left_name, left_alias, right_name, right_alias)?; + let join_b_side = + prepared_join_column_side(join_b.0, left_name, left_alias, right_name, right_alias)?; + let (left_join_column, right_join_column) = match (join_a_side, join_b_side) { + (SimpleJoinProjectionSide::Left, SimpleJoinProjectionSide::Right) => { + (join_a.1, join_b.1) + } + (SimpleJoinProjectionSide::Right, SimpleJoinProjectionSide::Left) => { + (join_b.1, join_a.1) + } + _ => return None, + }; + if !identifiers_equal(left_join_column, left_rowid_column) + || !identifiers_equal(right_join_column, right_rowid_column) + { + return None; + } + + let (filter_table, filter_column, param_index) = prepared_join_filter_param(filter)?; + let filter_side = prepared_join_column_side( + filter_table, + left_name, + left_alias, + right_name, + right_alias, + )?; + match filter_side { + SimpleJoinProjectionSide::Left + if !identifiers_equal(filter_column, left_rowid_column) => + { + return None; + } + SimpleJoinProjectionSide::Right + if !identifiers_equal(filter_column, right_rowid_column) => + { + return None; + } + _ => {} + } + let mut projections = Vec::with_capacity(select.projection.len()); + let mut left_projection_indexes = Vec::new(); + let mut right_projection_indexes = Vec::new(); + let mut column_names = Vec::with_capacity(select.projection.len()); + for item in &select.projection { + let crate::sql::ast::SelectItem::Expr { expr, alias } = item else { + return None; + }; + let crate::sql::ast::Expr::Column { table, column } = expr else { + return None; + }; + let side = prepared_join_column_side( + table.as_deref(), + left_name, + left_alias, + right_name, + right_alias, + )?; + let schema = match side { + SimpleJoinProjectionSide::Left => left_schema, + SimpleJoinProjectionSide::Right => right_schema, + }; + let index = schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, column))?; + let projected_index = match side { + SimpleJoinProjectionSide::Left => { + push_prepared_join_projection_index(&mut left_projection_indexes, index) + } + SimpleJoinProjectionSide::Right => { + push_prepared_join_projection_index(&mut right_projection_indexes, index) + } + }; + projections.push(ResolvedSimpleJoinProjection { + side, + index: projected_index, + }); + column_names.push(alias.clone().unwrap_or_else(|| column.clone())); + } + + Some(PreparedSimpleRowIdJoinProjection { + left_table_name: left_schema.name.clone(), + right_table_name: right_schema.name.clone(), + left_projection_indexes, + right_projection_indexes, + projections, + column_names: Arc::from(column_names), + param_index, + }) + } + pub(crate) fn prepared_simple_scalar_filtered_aggregate( + statement: &SqlStatement, + runtime: &EngineRuntime, + ) -> Option { + let SqlStatement::Query(query) = statement else { + return None; + }; + if !query.ctes.is_empty() + || !query.order_by.is_empty() + || query.limit.is_some() + || query.offset.is_some() + { + return None; + } + let crate::sql::ast::QueryBody::Select(select) = &query.body else { + return None; + }; + if select.distinct + || !select.distinct_on.is_empty() + || !select.group_by.is_empty() + || select.having.is_some() + || select.from.len() != 1 + || select.projection.len() != 2 + { + return None; + } + let crate::sql::ast::FromItem::Table { name, alias } = &select.from[0] else { + return None; + }; + if runtime.temp_table_schema(name).is_some() + || runtime + .catalog + .views + .keys() + .any(|view_name| identifiers_equal(view_name, name)) + { + return None; + } + let table = runtime.catalog.table(name)?; + if !prepared_table_generated_columns_are_stored(table) { + return None; + } + let param_index = prepared_scalar_filter_param(select.filter.as_ref()?, name, alias)?; + let mut saw_count = false; + let mut saw_sum = false; + for item in &select.projection { + let crate::sql::ast::SelectItem::Expr { expr, .. } = item else { + return None; + }; + if prepared_scalar_count_star(expr) { + saw_count = true; + continue; + } + if let Some(sum_column) = prepared_scalar_sum_column(expr, name, alias) { + if table + .columns + .iter() + .any(|column| identifiers_equal(&column.name, sum_column)) + { + saw_sum = true; + continue; + } + } + return None; + } + if !saw_count || !saw_sum { + return None; + } + Some(PreparedSimpleScalarFilteredAggregate { + table_name: table.name.clone(), + param_index, + cache: Arc::new(Mutex::new(PreparedScalarAggregateCache::default())), + }) + } + pub(crate) fn prepared_simple_insert( + &self, + sql: &str, + statement: &crate::sql::ast::InsertStatement, + runtime: &EngineRuntime, + ) -> Result>> { + self.inner + .prepared_insert_cache + .lock() + .map_err(|_| DbError::internal("prepared insert cache lock poisoned"))? + .get_or_prepare( + sql, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + || runtime.prepare_simple_insert(statement), + ) + } + pub(crate) fn prepared_plan_accounted_size(bundle: &PreparedPlanBundle) -> u64 { + fn string_bytes(value: &str) -> u64 { + value.len() as u64 + } + fn string_slice_bytes(values: &[String]) -> u64 { + values.iter().map(|value| string_bytes(value)).sum() + } + + let mut total = crate::plan_cache::statement_accounted_size(bundle.statement.as_ref()) + .saturating_add(std::mem::size_of::() as u64); + if let Some(plan) = &bundle.simple_row_id_projection { + total = total + .saturating_add(128) + .saturating_add(string_bytes(&plan.table_name)) + .saturating_add( + (plan.projection_indexes.len() * std::mem::size_of::()) as u64, + ) + .saturating_add(string_slice_bytes(&plan.column_names)); + } + if let Some(plan) = &bundle.simple_indexed_projection { + total = total + .saturating_add(192) + .saturating_add(string_bytes(&plan.table_name)) + .saturating_add( + (plan.projection_indexes.len() * std::mem::size_of::()) as u64, + ) + .saturating_add(string_slice_bytes(&plan.column_names)); + } + if let Some(plan) = &bundle.simple_row_id_range_projection { + total = total + .saturating_add(160) + .saturating_add(string_bytes(&plan.table_name)) + .saturating_add(string_bytes(&plan.filter_column)) + .saturating_add( + (plan.projection_indexes.len() * std::mem::size_of::()) as u64, + ) + .saturating_add(string_slice_bytes(&plan.column_names)); + } + if let Some(plan) = &bundle.simple_ordered_row_id_projection { + total = total + .saturating_add(160) + .saturating_add(string_bytes(&plan.table_name)) + .saturating_add(string_bytes(&plan.order_column)) + .saturating_add( + (plan.projection_indexes.len() * std::mem::size_of::()) as u64, + ) + .saturating_add(string_slice_bytes(&plan.column_names)); + } + if let Some(plan) = &bundle.simple_row_id_join_projection { + total = total + .saturating_add(256) + .saturating_add(string_bytes(&plan.left_table_name)) + .saturating_add(string_bytes(&plan.right_table_name)) + .saturating_add( + ((plan.left_projection_indexes.len() + + plan.right_projection_indexes.len() + + plan.projections.len()) + * std::mem::size_of::()) as u64, + ) + .saturating_add(string_slice_bytes(&plan.column_names)); + } + if let Some(plan) = &bundle.simple_scalar_filtered_aggregate { + total = total + .saturating_add(128) + .saturating_add(string_bytes(&plan.table_name)); + } + if bundle.prepared_insert.is_some() { + total = total.saturating_add(512); + } + if bundle.prepared_update.is_some() { + total = total.saturating_add(384); + } + if bundle.prepared_delete.is_some() { + total = total.saturating_add(384); + } + total + } + pub(crate) fn prepared_read_row_source_row_limit(&self) -> usize { + let row_limit = self + .inner + .config + .cache_size_mb + .saturating_mul(PREPARED_READ_ROW_SOURCE_ROWS_PER_CACHE_MB); + if row_limit == 0 { + 0 + } else { + row_limit.max(PREPARED_READ_ROW_SOURCE_MIN_ROW_LIMIT) + } + } + pub(crate) fn prepared_insert_current_next_row_id( + runtime: &EngineRuntime, + prepared_insert: &PreparedSimpleInsert, + ) -> Result> { + let Some(table_name) = prepared_insert.catalog_table_name.as_deref() else { + return Ok(None); + }; + runtime + .catalog + .tables + .get(table_name) + .map(|table| Some(table.next_row_id)) + .ok_or_else(|| DbError::sql(format!("unknown table {}", prepared_insert.table_name))) + } + #[allow(clippy::too_many_arguments)] + pub(crate) fn try_execute_prepared_insert_in_runtime_state( + &self, + prepared: &PreparedStatement, + params: &[Value], + runtime: &mut EngineRuntime, + snapshot_lsn: u64, + persistent_changed: &mut bool, + indexes_maybe_stale: &mut bool, + prepared_insert_runtime_cache: &mut HashMap>, + ) -> Result> { + let Some(insert_plan) = self.prepared_insert_plan_for_runtime_state( + prepared, + runtime, + snapshot_lsn, + indexes_maybe_stale, + prepared_insert_runtime_cache, + )? + else { + return Ok(None); + }; + + let result = runtime.execute_prepared_simple_insert( + insert_plan.as_ref(), + params, + self.inner.config.page_size, + )?; + *persistent_changed |= + Self::prepared_insert_changes_persistent_table(runtime, insert_plan.as_ref()); + Ok(Some(result)) + } + #[allow(clippy::too_many_arguments)] + pub(crate) fn try_execute_prepared_insert_in_runtime_state_mut( + &self, + prepared: &PreparedStatement, + params: &mut [Value], + runtime: &mut EngineRuntime, + snapshot_lsn: u64, + persistent_changed: &mut bool, + indexes_maybe_stale: &mut bool, + prepared_insert_runtime_cache: &mut HashMap>, + prepared_insert_last_cache_key: &mut Option, + prepared_insert_last_plan: &mut Option>, + prepared_insert_last_next_row_id: &mut Option, + prepared_insert_candidate: &mut Vec, + ) -> Result> { + let Some(insert_plan) = self.prepared_insert_plan_for_runtime_state( + prepared, + runtime, + snapshot_lsn, + indexes_maybe_stale, + prepared_insert_runtime_cache, + )? + else { + return Ok(None); + }; + + if !Self::prepared_insert_uses_direct_positional_params(insert_plan.as_ref(), params.len()) + { + return Ok(None); + } + + let cache_key = Self::prepared_statement_cache_key(prepared); + *prepared_insert_last_cache_key = Some(cache_key); + *prepared_insert_last_plan = Some(Arc::clone(&insert_plan)); + let result = runtime + .execute_prepared_simple_insert_positional_params_in_place_with_candidate( + insert_plan.as_ref(), + params, + prepared_insert_candidate, + self.inner.config.page_size, + )?; + *prepared_insert_last_next_row_id = + Self::prepared_insert_current_next_row_id(runtime, insert_plan.as_ref())?; + if !*persistent_changed { + *persistent_changed |= + Self::prepared_insert_changes_persistent_table(runtime, insert_plan.as_ref()); + } + Ok(Some(QueryResult::with_affected_rows(result))) + } + #[allow(clippy::too_many_arguments)] + pub(crate) fn try_execute_prepared_update_in_runtime_state( + &self, + prepared_statement: &PreparedStatement, + prepared_update: &PreparedSimpleUpdate, + params: &[Value], + runtime: &mut EngineRuntime, + snapshot_lsn: u64, + persistent_changed: &mut bool, + indexes_maybe_stale: &mut bool, + ) -> Result> { + self.load_runtime_table_row_sources_at_snapshot( + runtime, + &[prepared_update.table_name.as_str()], + snapshot_lsn, + )?; + self.validate_prepared_schema_cookie( + prepared_statement, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + if !runtime.can_reuse_prepared_simple_update(prepared_update) { + return Ok(None); + } + let temp_only = self.statement_is_temp_only(runtime, prepared_statement.statement.as_ref()); + let result = runtime.execute_prepared_simple_update( + prepared_update, + params, + self.inner.config.page_size, + )?; + *persistent_changed |= !temp_only; + *indexes_maybe_stale |= Self::runtime_has_stale_indexes(runtime); + Ok(Some(result)) + } + #[allow(clippy::too_many_arguments)] + pub(crate) fn try_execute_prepared_delete_in_runtime_state( + &self, + prepared_statement: &PreparedStatement, + prepared_delete: &PreparedSimpleDelete, + params: &[Value], + runtime: &mut EngineRuntime, + snapshot_lsn: u64, + persistent_changed: &mut bool, + indexes_maybe_stale: &mut bool, + ) -> Result> { + let row_source_table_names = prepared_delete.required_row_source_table_names(); + let child_index_targets = prepared_delete.child_index_hydration_targets(); + self.load_runtime_table_row_sources_and_child_indexes_at_snapshot( + runtime, + &row_source_table_names, + &child_index_targets, + snapshot_lsn, + )?; + self.validate_prepared_schema_cookie( + prepared_statement, + runtime.catalog.schema_cookie, + runtime.temp_schema_cookie, + )?; + if !runtime.can_reuse_prepared_simple_delete(prepared_delete) { + return Ok(None); + } + let temp_only = self.statement_is_temp_only(runtime, prepared_statement.statement.as_ref()); + let result = runtime.execute_prepared_simple_delete( + prepared_delete, + params, + self.inner.config.page_size, + )?; + *persistent_changed |= !temp_only; + *indexes_maybe_stale |= Self::runtime_has_stale_indexes(runtime); + Ok(Some(result)) + } + pub(crate) fn try_execute_prepared_inspection_query( + &self, + prepared: &PreparedStatement, + params: &[Value], + ) -> Result> { + if let Some(result) = + self.try_execute_sync_inspection_query(&prepared.prepared_sql, params)? + { + return Ok(Some(result)); + } + if let Some(result) = crate::extensions::try_execute_extension_inspection_query( + self, + &prepared.prepared_sql, + params, + )? { + return Ok(Some(result)); + } + Ok(None) + } +} diff --git a/crates/decentdb/src/db/reactive_ops.rs b/crates/decentdb/src/db/reactive_ops.rs new file mode 100644 index 00000000..0cb67113 --- /dev/null +++ b/crates/decentdb/src/db/reactive_ops.rs @@ -0,0 +1,151 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +impl Db { + /// Subscribes to committed changes for one or more persistent user tables. + pub fn watch_table(&self, options: TableWatchOptions) -> Result { + let tables = self.validate_watch_tables(&options.tables)?; + self.reactive_hub().watch_table( + tables, + options.queue_capacity, + self.inner.wal.latest_snapshot(), + self.schema_cookie()?, + ) + } + /// Subscribes to committed changes intersecting a primary-key range. + pub fn watch_range(&self, mut options: RangeWatchOptions) -> Result { + let canonical = self.validate_watch_range_table(&options.table)?; + options.table = canonical; + self.reactive_hub().watch_range( + options, + self.inner.wal.latest_snapshot(), + self.schema_cookie()?, + ) + } + /// Executes a SELECT and subscribes to invalidations for its dependencies. + pub fn watch_query( + &self, + sql: &str, + params: &[Value], + options: QueryWatchOptions, + ) -> Result { + let statement = self.parsed_statement(sql)?; + if !statement_is_read_only(&statement) { + return Err(DbError::sql( + "query subscriptions require a read-only SELECT", + )); + } + let dependencies = self.query_watch_dependencies(&statement)?; + let result = self.execute_with_params(sql, params)?; + self.reactive_hub().watch_query( + dependencies, + options.queue_capacity, + self.inner.wal.latest_snapshot(), + self.schema_cookie()?, + result, + ) + } + /// Returns current reactive subscription counters. + #[must_use] + pub fn reactive_metrics(&self) -> ReactiveMetricsSnapshot { + self.reactive_hub_if_initialized() + .map_or_else(ReactiveMetricsSnapshot::default, |hub| { + hub.metrics_snapshot() + }) + } + /// Returns current reactive subscription details. + #[must_use] + pub fn reactive_subscriptions(&self) -> Vec { + self.reactive_hub_if_initialized() + .map_or_else(Vec::new, |hub| hub.subscription_snapshots()) + } + pub(crate) fn reactive_hub(&self) -> Arc { + Arc::clone(self.inner.reactive_hub.get_or_init(|| { + crate::reactive::acquire_hub( + self.inner.reactive_registry_key.clone(), + &self.inner.config, + ) + })) + } + fn reactive_hub_if_initialized(&self) -> Option<&Arc> { + self.inner.reactive_hub.get() + } + pub(crate) fn reactive_hub_if_available(&self) -> Option> { + self.reactive_hub_if_initialized() + .map(Arc::clone) + .or_else(|| crate::reactive::existing_hub(self.inner.reactive_registry_key.as_ref())) + } + pub(crate) fn reactive_has_watchers(&self) -> bool { + self.reactive_hub_if_available() + .is_some_and(|hub| hub.has_watchers()) + } + pub(crate) fn reactive_metrics_query_result(&self) -> Result { + let metrics = self.reactive_metrics(); + Ok(QueryResult::with_rows( + vec![ + "active_watch_count".to_string(), + "table_watch_count".to_string(), + "range_watch_count".to_string(), + "query_watch_count".to_string(), + "change_stream_count".to_string(), + "events_published".to_string(), + "events_delivered".to_string(), + "events_dropped".to_string(), + "lagged_watch_count".to_string(), + "row_change_events_truncated".to_string(), + ], + vec![QueryRow::new(vec![ + sync_usize_to_i64(metrics.active_watch_count, "active_watch_count")?, + sync_usize_to_i64(metrics.table_watch_count, "table_watch_count")?, + sync_usize_to_i64(metrics.range_watch_count, "range_watch_count")?, + sync_usize_to_i64(metrics.query_watch_count, "query_watch_count")?, + sync_usize_to_i64(metrics.change_stream_count, "change_stream_count")?, + sync_u64_to_i64(metrics.events_published, "events_published")?, + sync_u64_to_i64(metrics.events_delivered, "events_delivered")?, + sync_u64_to_i64(metrics.events_dropped, "events_dropped")?, + sync_usize_to_i64(metrics.lagged_watch_count, "lagged_watch_count")?, + sync_u64_to_i64( + metrics.row_change_events_truncated, + "row_change_events_truncated", + )?, + ])], + )) + } + pub(crate) fn reactive_subscriptions_query_result(&self) -> Result { + let rows = self + .reactive_subscriptions() + .into_iter() + .map(|subscription| { + Ok(QueryRow::new(vec![ + sync_u64_to_i64(subscription.watch_id, "watch_id")?, + Value::Text(subscription.kind.as_str().to_string()), + Value::Int64(subscription.created_at_micros), + sync_usize_to_i64(subscription.queue_capacity, "queue_capacity")?, + sync_usize_to_i64(subscription.queue_depth, "queue_depth")?, + sync_u64_to_i64( + subscription.last_delivered_event_id, + "last_delivered_event_id", + )?, + sync_u64_to_i64(subscription.dropped_events, "dropped_events")?, + Value::Bool(subscription.lagged), + Value::Text(subscription.dependencies_json), + ])) + }) + .collect::>>()?; + Ok(QueryResult::with_rows( + vec![ + "watch_id".to_string(), + "kind".to_string(), + "created_at_micros".to_string(), + "queue_capacity".to_string(), + "queue_depth".to_string(), + "last_delivered_event_id".to_string(), + "dropped_events".to_string(), + "lagged".to_string(), + "dependencies_json".to_string(), + ], + rows, + )) + } +} diff --git a/crates/decentdb/src/db/sync_ops.rs b/crates/decentdb/src/db/sync_ops.rs new file mode 100644 index 00000000..e551d55e --- /dev/null +++ b/crates/decentdb/src/db/sync_ops.rs @@ -0,0 +1,3779 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +impl Db { + pub(crate) fn sync_temp_state_from_runtime(&self, runtime: &EngineRuntime) -> Result<()> { + if runtime.temp_schema_cookie == 0 + && runtime.temp_tables.is_empty() + && runtime.temp_table_data.is_empty() + && runtime.temp_views.is_empty() + && runtime.temp_indexes.is_empty() + { + return Ok(()); + } + let changed = { + let mut state = self + .inner + .temp_state + .lock() + .map_err(|_| DbError::internal("temp schema lock poisoned"))?; + let before = state.schema_cookie; + state.update_from_runtime(runtime); + before != state.schema_cookie + }; + if changed { + crate::plan_cache::PlanCacheInvalidator::on_temp_schema_change(&*self.inner); + } + Ok(()) + } + pub fn sync_init_replica(&self, replica_id: &str) -> Result<()> { + self.ensure_sync_tables()?; + self.sync_upsert_metadata("replica_id", replica_id)?; + self.sync_upsert_metadata("enabled", "true")?; + self.sync_upsert_metadata("next_sequence", "1")?; + self.inner.sync_ctx.set_replica_id(replica_id); + self.inner.sync_ctx.set_enabled(true); + self.inner.sync_ctx.set_next_sequence(1); + self.inner.sync_ctx.ensure_journal_open(&self.inner.vfs)?; + Ok(()) + } + pub fn sync_create_scope( + &self, + name: &str, + include_tables: &[&str], + row_filter: Option<&str>, + ) -> Result<()> { + self.ensure_sync_tables()?; + let runtime = self.runtime_for_metadata_inspection()?; + let validation = + validate_sync_scope_definition(&runtime, name, include_tables, row_filter)?; + let created_at_micros = self + .sync_scope(name)? + .map(|scope| scope.created_at_micros) + .unwrap_or_else(current_time_micros); + let updated_at_micros = current_time_micros(); + let sql = format!( + "INSERT INTO {table} (name, include_tables_json, row_filter, filter_columns_json, created_at_micros, updated_at_micros) VALUES ({name}, {include_tables_json}, {row_filter}, {filter_columns_json}, {created_at_micros}, {updated_at_micros}) ON CONFLICT (name) DO UPDATE SET include_tables_json = {include_tables_json}, row_filter = {row_filter}, filter_columns_json = {filter_columns_json}, updated_at_micros = {updated_at_micros}", + table = crate::sync::SCOPES_TABLE, + name = sql_text_literal(&validation.name), + include_tables_json = sql_text_literal( + &serde_json::to_string(&validation.include_tables) + .map_err(|error| DbError::internal(format!("failed to encode scope tables: {error}")))?, + ), + row_filter = sql_nullable_text_literal(validation.row_filter.as_deref()), + filter_columns_json = sql_text_literal( + &serde_json::to_string(&validation.filter_columns) + .map_err(|error| DbError::internal(format!("failed to encode scope columns: {error}")))?, + ), + created_at_micros = created_at_micros, + updated_at_micros = updated_at_micros, + ); + let _ = self.execute(&sql)?; + Ok(()) + } + pub fn sync_drop_scope(&self, name: &str) -> Result { + self.ensure_sync_tables()?; + let scope_name = name.trim(); + if scope_name.is_empty() { + return Err(DbError::sql("sync scope name must not be empty")); + } + if self + .sync_peer_scope_bindings()? + .iter() + .any(|binding| binding.scope_name.eq_ignore_ascii_case(scope_name)) + { + return Err(DbError::sql(format!( + "cannot drop sync scope '{scope_name}' while peer bindings exist" + ))); + } + let sql = format!( + "DELETE FROM {} WHERE name = {}", + crate::sync::SCOPES_TABLE, + sql_text_literal(scope_name) + ); + let result = self.execute(&sql)?; + Ok(result.affected_rows() > 0) + } + pub fn sync_scope(&self, name: &str) -> Result> { + self.ensure_sync_tables()?; + let scope_name = name.trim(); + if scope_name.is_empty() { + return Err(DbError::sql("sync scope name must not be empty")); + } + let sql = format!( + "SELECT name, include_tables_json, row_filter, filter_columns_json, created_at_micros, updated_at_micros FROM {} WHERE name = {}", + crate::sync::SCOPES_TABLE, + sql_text_literal(scope_name) + ); + match self.execute(&sql) { + Ok(result) => Ok(result.rows().first().map(sync_scope_from_row).transpose()?), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(None) + } else { + Err(error) + } + } + } + } + pub fn sync_scopes(&self) -> Result> { + self.ensure_sync_tables()?; + let sql = format!( + "SELECT name, include_tables_json, row_filter, filter_columns_json, created_at_micros, updated_at_micros FROM {} ORDER BY name", + crate::sync::SCOPES_TABLE + ); + match self.execute(&sql) { + Ok(result) => result.rows().iter().map(sync_scope_from_row).collect(), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(Vec::new()) + } else { + Err(error) + } + } + } + } + pub fn sync_bind_peer_scope(&self, peer_name: &str, scope_name: &str) -> Result<()> { + self.ensure_sync_tables()?; + let peer_name = peer_name.trim(); + if peer_name.is_empty() { + return Err(DbError::sql("sync peer name must not be empty")); + } + if self.sync_peer(peer_name)?.is_none() { + return Err(DbError::sql(format!("sync peer '{peer_name}' not found"))); + } + let scope = self + .sync_scope(scope_name)? + .ok_or_else(|| DbError::sql(format!("sync scope '{scope_name}' not found")))?; + let existing = self.sync_peer_scope_binding_row(peer_name)?; + let created_at_micros = existing + .as_ref() + .map(|binding| binding.created_at_micros) + .unwrap_or_else(current_time_micros); + let updated_at_micros = current_time_micros(); + let sql = format!( + "INSERT INTO {table} (peer_name, scope_name, created_at_micros, updated_at_micros) VALUES ({peer_name}, {scope_name}, {created_at_micros}, {updated_at_micros}) ON CONFLICT (peer_name) DO UPDATE SET scope_name = {scope_name}, updated_at_micros = {updated_at_micros}", + table = crate::sync::PEER_SCOPES_TABLE, + peer_name = sql_text_literal(peer_name), + scope_name = sql_text_literal(&scope.name), + created_at_micros = created_at_micros, + updated_at_micros = updated_at_micros, + ); + let _ = self.execute(&sql)?; + Ok(()) + } + pub fn sync_unbind_peer_scope(&self, peer_name: &str) -> Result { + self.ensure_sync_tables()?; + let peer_name = peer_name.trim(); + if peer_name.is_empty() { + return Err(DbError::sql("sync peer name must not be empty")); + } + let sql = format!( + "DELETE FROM {} WHERE peer_name = {}", + crate::sync::PEER_SCOPES_TABLE, + sql_text_literal(peer_name) + ); + let result = self.execute(&sql)?; + Ok(result.affected_rows() > 0) + } + pub fn sync_peer_scope(&self, peer_name: &str) -> Result> { + self.ensure_sync_tables()?; + let peer_name = peer_name.trim(); + if peer_name.is_empty() { + return Err(DbError::sql("sync peer name must not be empty")); + } + self.sync_peer_scope_binding_row(peer_name) + } + pub fn sync_peer_scope_definition(&self, peer_name: &str) -> Result> { + let binding = match self.sync_peer_scope(peer_name)? { + Some(binding) => binding, + None => return Ok(None), + }; + match self.sync_scope(&binding.scope_name)? { + Some(scope) => Ok(Some(scope)), + None => Err(DbError::sql(format!( + "sync scope '{}' bound to peer '{}' was not found", + binding.scope_name, binding.peer_name + ))), + } + } + pub fn sync_peer_scope_bindings(&self) -> Result> { + self.ensure_sync_tables()?; + let sql = format!( + "SELECT peer_name, scope_name, created_at_micros, updated_at_micros FROM {} ORDER BY peer_name", + crate::sync::PEER_SCOPES_TABLE + ); + match self.execute(&sql) { + Ok(result) => result + .rows() + .iter() + .map(sync_peer_scope_binding_from_row) + .collect(), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(Vec::new()) + } else { + Err(error) + } + } + } + } + pub fn sync_export_batch_for_scope( + &self, + scope_name: &str, + since_seq: u64, + limit: usize, + ) -> Result { + let scope = self + .sync_scope(scope_name)? + .ok_or_else(|| DbError::sql(format!("sync scope '{scope_name}' not found")))?; + let records = self.sync_pending_changes(since_seq, limit)?; + let source_replica_id = records.first().map(|record| record.replica_id.clone()); + let source_high_watermark = records.last().map(|record| record.sequence); + let filtered = self.sync_filter_records_for_scope(&scope, records)?; + SyncChangeBatch::scoped_from_records(filtered, source_replica_id, source_high_watermark) + } + pub fn sync_import_batch_for_scope( + &self, + scope_name: &str, + batch: &SyncChangeBatch, + ) -> Result { + batch.validate()?; + let scope = self + .sync_scope(scope_name)? + .ok_or_else(|| DbError::sql(format!("sync scope '{scope_name}' not found")))?; + self.sync_validate_batch_for_scope(&scope, batch)?; + self.sync_import_batch(batch) + } + pub fn sync_import_batch_for_scope_with_policy( + &self, + scope_name: &str, + batch: &SyncChangeBatch, + policy: SyncConflictPolicy, + ) -> Result { + batch.validate()?; + let scope = self + .sync_scope(scope_name)? + .ok_or_else(|| DbError::sql(format!("sync scope '{scope_name}' not found")))?; + self.sync_validate_batch_for_scope(&scope, batch)?; + self.sync_import_batch_with_policy(batch, policy) + } + pub fn sync_add_peer(&self, name: &str, endpoint: &str, token_env: Option<&str>) -> Result<()> { + let name = name.trim(); + if name.is_empty() { + return Err(DbError::sql("sync peer name must not be empty")); + } + if !(endpoint.starts_with("http://") || endpoint.starts_with("https://")) { + return Err(DbError::sql( + "sync peer endpoint must start with http:// or https://", + )); + } + if token_env.is_some_and(|value| value.trim().is_empty()) { + return Err(DbError::sql("sync peer token_env must not be empty")); + } + + self.ensure_sync_tables()?; + let now = current_time_micros(); + let token_sql = token_env + .map(sql_text_literal) + .unwrap_or_else(|| "NULL".to_string()); + let sql = format!( + "INSERT INTO {table} (name, endpoint, token_env, created_at_micros, updated_at_micros) VALUES ({name}, {endpoint}, {token_env}, {now}, {now}) ON CONFLICT (name) DO UPDATE SET endpoint = {endpoint}, token_env = {token_env}, updated_at_micros = {now}", + table = crate::sync::PEERS_TABLE, + name = sql_text_literal(name), + endpoint = sql_text_literal(endpoint), + token_env = token_sql, + now = now, + ); + let _ = self.execute(&sql)?; + Ok(()) + } + pub fn sync_remove_peer(&self, name: &str) -> Result { + self.ensure_sync_tables()?; + let sql = format!( + "DELETE FROM {} WHERE name = {}", + crate::sync::PEERS_TABLE, + sql_text_literal(name) + ); + let result = self.execute(&sql)?; + Ok(result.affected_rows() > 0) + } + pub fn sync_peers(&self) -> Result> { + self.ensure_sync_tables()?; + let sql = format!( + "SELECT name, endpoint, token_env, created_at_micros, updated_at_micros FROM {} ORDER BY name", + crate::sync::PEERS_TABLE + ); + match self.execute(&sql) { + Ok(result) => result.rows().iter().map(sync_peer_from_row).collect(), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(Vec::new()) + } else { + Err(error) + } + } + } + } + pub fn sync_peer(&self, name: &str) -> Result> { + self.ensure_sync_tables()?; + let sql = format!( + "SELECT name, endpoint, token_env, created_at_micros, updated_at_micros FROM {} WHERE name = {}", + crate::sync::PEERS_TABLE, + sql_text_literal(name) + ); + match self.execute(&sql) { + Ok(result) => Ok(result.rows().first().map(sync_peer_from_row).transpose()?), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(None) + } else { + Err(error) + } + } + } + } + fn sync_peer_scope_binding_row(&self, peer_name: &str) -> Result> { + let sql = format!( + "SELECT peer_name, scope_name, created_at_micros, updated_at_micros FROM {} WHERE peer_name = {}", + crate::sync::PEER_SCOPES_TABLE, + sql_text_literal(peer_name) + ); + match self.execute(&sql) { + Ok(result) => Ok(result + .rows() + .first() + .map(sync_peer_scope_binding_from_row) + .transpose()?), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(None) + } else { + Err(error) + } + } + } + } + pub fn sync_sessions(&self) -> Result> { + self.ensure_sync_tables()?; + let sql = format!( + "SELECT session_id, peer_name, direction, remote_replica_id, started_at_micros, ended_at_micros, status, error, pushed_batch_id, pulled_batch_id, pushed_seen, pushed_applied, pushed_skipped, pushed_conflicted, pulled_seen, pulled_applied, pulled_skipped, pulled_conflicted, retry_count FROM {} ORDER BY session_id", + crate::sync::SESSIONS_TABLE + ); + match self.execute(&sql) { + Ok(result) => result.rows().iter().map(sync_session_from_row).collect(), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(Vec::new()) + } else { + Err(error) + } + } + } + } + pub fn sync_start_session( + &self, + peer_name: &str, + direction: SyncRunDirection, + remote_replica_id: Option<&str>, + ) -> Result { + self.ensure_sync_tables()?; + let session_id = self.next_sync_session_id()?; + let started_at_micros = current_time_micros(); + let sql = format!( + "INSERT INTO {table} (session_id, peer_name, direction, remote_replica_id, started_at_micros, ended_at_micros, status, error, pushed_batch_id, pulled_batch_id, pushed_seen, pushed_applied, pushed_skipped, pushed_conflicted, pulled_seen, pulled_applied, pulled_skipped, pulled_conflicted, retry_count) VALUES ({session_id}, {peer_name}, {direction}, {remote_replica_id}, {started_at_micros}, NULL, 'started', NULL, NULL, NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0)", + table = crate::sync::SESSIONS_TABLE, + session_id = session_id, + peer_name = sql_text_literal(peer_name), + direction = sql_text_literal(direction.as_str()), + remote_replica_id = remote_replica_id + .map(sql_text_literal) + .unwrap_or_else(|| "NULL".to_string()), + started_at_micros = started_at_micros, + ); + let _ = self.execute(&sql)?; + Ok(session_id) + } + pub fn sync_finish_session_success( + &self, + session_id: i64, + summary: &SyncRunSummary, + ) -> Result<()> { + self.sync_update_session(session_id, summary, "success", None, current_time_micros()) + } + pub fn sync_finish_session_failed( + &self, + session_id: i64, + summary: &SyncRunSummary, + error: &str, + ) -> Result<()> { + self.sync_update_session( + session_id, + summary, + "failed", + Some(error), + current_time_micros(), + ) + } + pub fn sync_integrity_report(&self) -> Result { + let local_replica_id = self.sync_read_metadata("replica_id").ok().flatten(); + crate::sync::inspect_journal_integrity( + self.inner.sync_ctx.journal_path(), + &self.inner.vfs, + local_replica_id.as_deref(), + ) + } + pub fn sync_peer_lag_report(&self) -> Result> { + self.ensure_sync_tables()?; + let local_high_watermark = self.sync_integrity_report()?.last_sequence; + let peers = self.sync_peers()?; + let sessions = self.sync_sessions()?; + let mut latest_successful_remote_replica_ids: HashMap> = + HashMap::new(); + for session in sessions.iter().rev() { + if session.status == "success" { + latest_successful_remote_replica_ids + .entry(session.peer_name.clone()) + .or_insert_with(|| session.remote_replica_id.clone()); + } + } + + peers + .into_iter() + .map(|peer| { + let remote_replica_id = latest_successful_remote_replica_ids + .get(&peer.name) + .cloned() + .flatten(); + let in_watermark = match remote_replica_id.as_deref() { + Some(replica_id) => self.sync_peer_watermark(replica_id)?, + None => None, + }; + let out_watermark = self.sync_peer_out_watermark(&peer.name)?; + let in_lag = match (local_high_watermark, in_watermark) { + (Some(local_high), Some(in_watermark)) if local_high >= in_watermark => { + Some(local_high - in_watermark) + } + _ => None, + }; + let out_lag = match (local_high_watermark, out_watermark) { + (Some(local_high), Some(out_watermark)) if local_high >= out_watermark => { + Some(local_high - out_watermark) + } + _ => None, + }; + Ok(SyncPeerLag { + peer_name: peer.name, + remote_replica_id, + in_watermark, + out_watermark, + local_high_watermark, + in_lag, + out_lag, + }) + }) + .collect() + } + pub fn sync_retention_report(&self) -> Result { + let integrity = self.sync_integrity_report()?; + let peer_lag = self.sync_peer_lag_report()?; + let journal_size_bytes = self.sync_status()?.journal_size_bytes; + let mut watermark_entries = peer_lag + .iter() + .flat_map(|peer| { + let inbound = peer.remote_replica_id.as_ref().zip(peer.in_watermark).map( + |(remote_replica_id, watermark)| { + (format!("remote:{remote_replica_id}"), watermark) + }, + ); + let outbound = peer + .out_watermark + .map(|watermark| (peer.peer_name.clone(), watermark)); + inbound.into_iter().chain(outbound) + }) + .collect::>(); + watermark_entries.extend(self.sync_peer_watermark_entries()?); + watermark_entries.extend( + self.sync_shape_clients()? + .into_iter() + .filter(|client| client.retention_blocking) + .map(|client| { + ( + format!( + "shape:{}:client:{}", + client.shape_id, client.client_replica_id + ), + client.last_ack_watermark, + ) + }), + ); + watermark_entries.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1))); + watermark_entries.dedup(); + let lowest_watermark = watermark_entries + .iter() + .map(|(_, watermark)| *watermark) + .min(); + let safe_prune_through = if integrity.total_records == 0 { + None + } else { + lowest_watermark.and_then(|watermark| watermark.checked_sub(1)) + }; + let blocked_by = if integrity.total_records == 0 { + Vec::new() + } else if let Some(lowest_watermark) = lowest_watermark { + if integrity + .last_sequence + .is_some_and(|local_high| lowest_watermark <= local_high) + { + watermark_entries + .iter() + .filter(|(_, watermark)| *watermark == lowest_watermark) + .map(|(label, _)| label.clone()) + .collect::>() + } else { + Vec::new() + } + } else { + Vec::new() + }; + let prunable_records = match safe_prune_through { + Some(safe_through) => { + match crate::sync::read_journal_records( + self.inner.sync_ctx.journal_path(), + &self.inner.vfs, + 0, + usize::MAX, + ) { + Ok(records) => records + .into_iter() + .filter(|record| record.sequence <= safe_through) + .count(), + Err(_) => 0, + } + } + None => 0, + }; + + Ok(SyncRetentionReport { + journal_records: integrity.total_records, + first_sequence: integrity.first_sequence, + last_sequence: integrity.last_sequence, + safe_prune_through, + prunable_records, + blocked_by, + journal_size_bytes, + }) + } + pub fn sync_operational_doctor_report(&self) -> Result { + let status = self.sync_status()?; + let integrity = self.sync_integrity_report()?; + let retention = self.sync_retention_report()?; + let peer_lag = self.sync_peer_lag_report()?; + let unresolved_conflicts = self.sync_conflicts()?.len(); + let mut recent_sessions = self.sync_sessions()?; + if recent_sessions.len() > 5 { + recent_sessions = recent_sessions.split_off(recent_sessions.len() - 5); + } + let mut issues = integrity.issues.clone(); + let mut guidance = Vec::new(); + let mut highest_severity = integrity.highest_severity; + + if !status.enabled { + highest_severity = highest_severity.max(SyncDoctorSeverity::Warning); + guidance.push( + "sync is disabled; enable it before expecting journal growth or peer watermarks" + .to_string(), + ); + } + + if unresolved_conflicts > 0 { + highest_severity = highest_severity.max(SyncDoctorSeverity::Warning); + let message = format!("{unresolved_conflicts} unresolved conflict(s) need attention"); + issues.push(SyncJournalIssue { + line_number: 0, + sequence: None, + severity: SyncDoctorSeverity::Warning, + code: "unresolved_conflicts".to_string(), + message: message.clone(), + }); + guidance.push(message); + } + + if retention.journal_records > 0 && retention.safe_prune_through.is_none() { + highest_severity = highest_severity.max(SyncDoctorSeverity::Warning); + let message = if retention.blocked_by.is_empty() { + "safe prune is unavailable because no peer watermarks are known".to_string() + } else { + format!( + "safe prune is blocked by {}", + retention.blocked_by.join(", ") + ) + }; + issues.push(SyncJournalIssue { + line_number: 0, + sequence: None, + severity: SyncDoctorSeverity::Warning, + code: "retention_blocked".to_string(), + message: message.clone(), + }); + guidance.push(message); + } else if let Some(safe_through) = retention.safe_prune_through { + guidance.push(format!( + "safe prune is available through sequence {safe_through}" + )); + } + + if peer_lag.iter().any(|peer| { + peer.in_lag.is_some_and(|lag| lag > 0) || peer.out_lag.is_some_and(|lag| lag > 0) + }) { + highest_severity = highest_severity.max(SyncDoctorSeverity::Warning); + guidance.push("peer lag exists; inspect sys_sync_peer_lag before pruning".to_string()); + } + + if integrity.highest_severity == SyncDoctorSeverity::Error { + highest_severity = SyncDoctorSeverity::Error; + } + + if issues.is_empty() { + guidance.push("journal integrity is clean".to_string()); + } + + Ok(SyncOperationalDoctorReport { + status, + integrity, + retention, + peer_lag, + unresolved_conflicts, + recent_sessions, + highest_severity, + issues, + guidance, + }) + } + pub fn sync_status(&self) -> Result { + if self.inner.sync_ctx.is_enabled() { + return Ok(SyncStatus { + enabled: true, + replica_id: self.inner.sync_ctx.replica_id(), + next_sequence: self.inner.sync_ctx.next_sequence(), + journal_path: Some( + self.inner + .sync_ctx + .journal_path() + .to_string_lossy() + .to_string(), + ), + journal_size_bytes: self.inner.sync_ctx.journal_size_bytes(), + }); + } + self.load_sync_status_from_db() + } + pub fn sync_pending_changes( + &self, + since_seq: u64, + limit: usize, + ) -> Result> { + if !self.inner.sync_ctx.is_enabled() { + let loaded = self.load_sync_status_from_db()?; + if !loaded.enabled { + return Ok(Vec::new()); + } + } + crate::sync::read_journal_records( + self.inner.sync_ctx.journal_path(), + &self.inner.vfs, + since_seq, + limit, + ) + } + fn sync_scope_row_filter_expr(scope: &SyncScope) -> Result> { + match scope.row_filter.as_deref() { + Some(filter_sql) => Ok(Some(parse_expression_sql(filter_sql).map_err(|error| { + DbError::sql(format!( + "invalid row filter for sync scope '{}': {error}", + scope.name + )) + })?)), + None => Ok(None), + } + } + fn sync_scope_record_matches( + &self, + scope: &SyncScope, + record: &SyncJournalRecord, + ) -> Result { + if !scope + .include_tables + .iter() + .any(|table_name| table_name.eq_ignore_ascii_case(&record.table)) + { + return Ok(false); + } + let Some(expr) = Self::sync_scope_row_filter_expr(scope)? else { + return Ok(true); + }; + let runtime = self.runtime_for_metadata_inspection()?; + let table = runtime + .catalog + .table(&record.table) + .ok_or_else(|| DbError::sql(format!("unknown table '{}'", record.table)))?; + let payload = match record.operation.as_str() { + "delete" => &record.primary_key, + "insert" | "update" => record + .after + .as_ref() + .ok_or_else(|| DbError::sql("sync record missing after payload"))?, + other => { + return Err(DbError::sql(format!("unsupported operation '{other}'"))); + } + }; + let payload = payload.as_object().ok_or_else(|| { + DbError::sql(format!( + "sync record for table '{}' must use an object payload", + record.table + )) + })?; + + let mut values = Vec::with_capacity(scope.filter_columns.len()); + for column_name in &scope.filter_columns { + let column = table + .columns + .iter() + .find(|candidate| candidate.name.eq_ignore_ascii_case(column_name)) + .ok_or_else(|| { + DbError::sql(format!( + "sync scope column '{column_name}' is missing from table '{}'", + table.name + )) + })?; + let json_value = payload.get(&column.name).ok_or_else(|| { + DbError::sql(format!( + "sync record for table '{}' is missing scoped column '{}'", + table.name, column.name + )) + })?; + values.push(json_to_column_value(&table.name, column, json_value)?); + } + + row_satisfies_expression(&runtime, &table.name, &scope.filter_columns, &values, &expr) + } + fn sync_filter_records_for_scope( + &self, + scope: &SyncScope, + records: Vec, + ) -> Result> { + let mut filtered = Vec::new(); + for record in records { + if self.sync_scope_record_matches(scope, &record)? { + filtered.push(record); + } + } + Ok(filtered) + } + fn sync_validate_batch_for_scope( + &self, + scope: &SyncScope, + batch: &SyncChangeBatch, + ) -> Result<()> { + let runtime = self.runtime_for_metadata_inspection()?; + for record in &batch.records { + if !scope + .include_tables + .iter() + .any(|table_name| table_name.eq_ignore_ascii_case(&record.table)) + { + return Err(DbError::sql(format!( + "sync batch contains table '{}' which is outside scope '{}'", + record.table, scope.name + ))); + } + if scope.row_filter.is_some() { + let table = runtime + .catalog + .table(&record.table) + .ok_or_else(|| DbError::sql(format!("unknown table '{}'", record.table)))?; + let payload = match record.operation.as_str() { + "delete" => &record.primary_key, + "insert" | "update" => record + .after + .as_ref() + .ok_or_else(|| DbError::sql("sync record missing after payload"))?, + other => { + return Err(DbError::sql(format!("unsupported operation '{other}'"))); + } + }; + let payload = payload.as_object().ok_or_else(|| { + DbError::sql(format!( + "sync record for table '{}' must use an object payload", + record.table + )) + })?; + let mut values = Vec::with_capacity(scope.filter_columns.len()); + for column_name in &scope.filter_columns { + let column = table + .columns + .iter() + .find(|candidate| candidate.name.eq_ignore_ascii_case(column_name)) + .ok_or_else(|| { + DbError::sql(format!( + "sync scope column '{column_name}' is missing from table '{}'", + table.name + )) + })?; + let json_value = payload.get(&column.name).ok_or_else(|| { + DbError::sql(format!( + "sync record for table '{}' is missing scoped column '{}'", + table.name, column.name + )) + })?; + values.push(json_to_column_value(&table.name, column, json_value)?); + } + if !row_satisfies_expression( + &runtime, + &table.name, + &scope.filter_columns, + &values, + &Self::sync_scope_row_filter_expr(scope)? + .ok_or_else(|| DbError::internal("scope row filter expression missing"))?, + )? { + return Err(DbError::sql(format!( + "sync batch contains record for table '{}' that does not match scope '{}'", + record.table, scope.name + ))); + } + } + } + Ok(()) + } + pub fn sync_export_batch(&self, since_seq: u64, limit: usize) -> Result { + let records = self.sync_pending_changes(since_seq, limit)?; + SyncChangeBatch::from_records(records) + } + pub fn sync_create_changeset( + &self, + mut options: CreateChangesetOptions, + ) -> Result { + self.ensure_sync_tables()?; + let mut scoped_from_shape = false; + if let Some(principal) = options.principal.as_ref() { + principal.validate()?; + } + if let Some(shape_id) = options.shape_id.as_deref() { + let shape = self.sync_shape(shape_id)?.ok_or_else(|| { + DbError::sql(format!( + "SHAPE_NOT_FOUND: sync shape '{shape_id}' not found" + )) + })?; + self.sync_authorize_shape(options.principal.as_ref(), &shape)?; + options.scope_name = Some(shape.scope_name); + scoped_from_shape = true; + } + if let Some(scope_name) = options.scope_name.as_deref() { + if !scoped_from_shape { + self.sync_authorize_scope(options.principal.as_ref(), scope_name)?; + } + } + + let max_records = options + .max_records + .map(usize::try_from) + .transpose() + .map_err(|_| DbError::sql("max_records is too large"))? + .unwrap_or(usize::MAX); + let created_at_micros = current_time_micros(); + let tooling = self.get_tooling_metadata()?; + let runtime = self.runtime_for_metadata_inspection()?; + let schema_cookie = runtime.catalog.schema_cookie; + let tenant_id = options + .principal + .as_ref() + .map(|principal| principal.tenant_id.clone()) + .or_else(|| { + options + .shape_id + .as_deref() + .and_then(|shape_id| self.sync_shape(shape_id).ok().flatten()) + .map(|shape| shape.tenant_id) + }); + + let mut changeset = match &options.source { + SyncChangesetSource::Checkpoint { + peer, + since_sequence, + } => { + let batch = match options.scope_name.as_deref() { + Some(scope_name) => { + self.sync_export_batch_for_scope(scope_name, *since_sequence, max_records)? + } + None => self.sync_export_batch(*since_sequence, max_records)?, + }; + let source_replica_id = batch + .source_replica_id + .clone() + .or_else(|| self.sync_status().ok().and_then(|status| status.replica_id)) + .unwrap_or_else(|| "unknown".to_string()); + let records = batch + .records + .iter() + .map(sync_changeset_record_from_journal_record) + .collect::>(); + let start_checkpoint = batch.first_sequence; + let end_checkpoint = batch.last_sequence; + let source_high_watermark = batch + .source_high_watermark + .or(batch.last_sequence) + .or_else(|| { + self.sync_integrity_report() + .ok() + .and_then(|report| report.last_sequence) + }); + let changeset_id = sync_changeset_id( + "checkpoint", + &source_replica_id, + created_at_micros, + records.len(), + ); + SyncChangeset { + changeset_version: crate::sync::SYNC_CHANGESET_VERSION, + changeset_id, + source_replica_id, + source_kind: options.source.kind(), + tenant_id, + scope_name: options.scope_name.clone(), + shape_id: options.shape_id.clone(), + base_kind: "checkpoint".to_string(), + base_checkpoint: Some(SyncChangesetCheckpoint { + peer: peer.clone(), + sequence: *since_sequence, + }), + base_branch: None, + base_snapshot: None, + start_checkpoint, + end_checkpoint, + source_high_watermark, + schema_fingerprint: tooling.schema_fingerprint.clone(), + schema_cookie, + sync_contract_version: crate::sync::SYNC_CONTRACT_VERSION, + query_contract_fingerprint: None, + producer_capabilities: SyncChangesetCapabilities::default(), + limits: SyncChangesetLimits::default(), + records, + conflict_policy_hint: None, + created_at_micros, + integrity_hash: None, + } + } + SyncChangesetSource::Branch { from, to } => { + self.sync_create_diff_changeset(SyncDiffChangesetContext { + base_kind: "branch", + from_ref: from, + to_ref: to, + scope_name: options.scope_name.as_deref(), + shape_id: options.shape_id.as_deref(), + tenant_id: tenant_id.as_deref(), + schema_fingerprint: &tooling.schema_fingerprint, + schema_cookie, + created_at_micros, + max_records, + })? + } + SyncChangesetSource::Snapshot { from, to } => { + self.sync_create_diff_changeset(SyncDiffChangesetContext { + base_kind: "snapshot", + from_ref: from, + to_ref: to, + scope_name: options.scope_name.as_deref(), + shape_id: options.shape_id.as_deref(), + tenant_id: tenant_id.as_deref(), + schema_fingerprint: &tooling.schema_fingerprint, + schema_cookie, + created_at_micros, + max_records, + })? + } + }; + self.sync_finalize_changeset(&mut changeset, options.max_bytes)?; + self.sync_record_changeset_history(&changeset, "created", None)?; + Ok(changeset) + } + fn sync_create_diff_changeset( + &self, + ctx: SyncDiffChangesetContext<'_>, + ) -> Result { + let diff = self.branch_diff(ctx.from_ref, ctx.to_ref)?; + let table_infos = self + .list_tables()? + .into_iter() + .map(|table| (table.name.clone(), table)) + .collect::>(); + let mut records = Vec::new(); + let source_replica_id = format!("{}:{}:{}", ctx.base_kind, ctx.from_ref, ctx.to_ref); + let mut sequence = 1u64; + for table_diff in &diff.tables { + if matches!( + table_diff.status, + crate::branch::BranchTableDiffStatus::Unsupported + ) { + return Err(DbError::sql(format!( + "CHANGESET_UNSUPPORTED: branch/snapshot diff for table '{}' is unsupported: {}", + table_diff.table, + table_diff + .message + .clone() + .unwrap_or_else(|| "unsupported row diff".to_string()) + ))); + } + if table_diff.schema_changed { + return Err(DbError::sql(format!( + "SCHEMA_INCOMPATIBLE: changeset diff for table '{}' changes schema", + table_diff.table + ))); + } + let Some(table) = table_infos.get(&table_diff.table) else { + continue; + }; + let pk_names = &table.primary_key_columns; + let column_names = table + .columns + .iter() + .map(|column| column.name.clone()) + .collect::>(); + let table_context = BranchChangesetTableContext { + source_replica_id: &source_replica_id, + table_name: &table.name, + primary_key_columns: pk_names, + column_names: &column_names, + schema_cookie: ctx.schema_cookie, + created_at_micros: ctx.created_at_micros, + }; + for row in &table_diff.added { + records.push(sync_changeset_record_from_branch_row( + &table_context, + sequence, + "insert", + row, + )?); + sequence += 1; + } + for row in &table_diff.updated { + records.push(sync_changeset_record_from_branch_row( + &table_context, + sequence, + "update", + row, + )?); + sequence += 1; + } + for row in &table_diff.deleted { + records.push(sync_changeset_record_from_branch_row( + &table_context, + sequence, + "delete", + row, + )?); + sequence += 1; + } + if records.len() > ctx.max_records { + return Err(DbError::sql( + "BATCH_TOO_LARGE: changeset exceeds max_records", + )); + } + } + + let source_kind = if ctx.base_kind == "branch" { + crate::sync::SyncChangesetSourceKind::Branch + } else { + crate::sync::SyncChangesetSourceKind::Snapshot + }; + let changeset_id = sync_changeset_id( + ctx.base_kind, + &source_replica_id, + ctx.created_at_micros, + records.len(), + ); + Ok(SyncChangeset { + changeset_version: crate::sync::SYNC_CHANGESET_VERSION, + changeset_id, + source_replica_id, + source_kind, + tenant_id: ctx.tenant_id.map(str::to_string), + scope_name: ctx.scope_name.map(str::to_string), + shape_id: ctx.shape_id.map(str::to_string), + base_kind: ctx.base_kind.to_string(), + base_checkpoint: None, + base_branch: (ctx.base_kind == "branch").then(|| ctx.from_ref.to_string()), + base_snapshot: (ctx.base_kind == "snapshot").then(|| ctx.from_ref.to_string()), + start_checkpoint: records.first().map(|record| record.origin_sequence), + end_checkpoint: records.last().map(|record| record.origin_sequence), + source_high_watermark: records.last().map(|record| record.origin_sequence), + schema_fingerprint: ctx.schema_fingerprint.to_string(), + schema_cookie: ctx.schema_cookie, + sync_contract_version: crate::sync::SYNC_CONTRACT_VERSION, + query_contract_fingerprint: None, + producer_capabilities: SyncChangesetCapabilities { + before_images: true, + ..SyncChangesetCapabilities::default() + }, + limits: SyncChangesetLimits::default(), + records, + conflict_policy_hint: None, + created_at_micros: ctx.created_at_micros, + integrity_hash: None, + }) + } + pub fn sync_inspect_changeset( + &self, + changeset: &SyncChangeset, + options: InspectChangesetOptions, + ) -> Result { + self.sync_validate_changeset_envelope(changeset)?; + let bytes = serde_json::to_vec(changeset) + .map_err(|error| DbError::internal(format!("failed to serialize changeset: {error}")))? + .len() as u64; + let mut tables = BTreeSet::new(); + let mut operations = BTreeMap::new(); + let mut warnings = Vec::new(); + for record in &changeset.records { + tables.insert(record.table.clone()); + *operations.entry(record.operation.clone()).or_insert(0u64) += 1; + if record.operation == "delete" && record.before.is_none() { + warnings.push(format!( + "delete record for table '{}' cannot be inverted without before image", + record.table + )); + } + } + let compatibility = if options.check_local_compatibility { + match self.sync_check_changeset_compatibility(changeset) { + Ok(()) => SyncChangesetCompatibility { + checked_against_local_db: true, + status: "compatible".to_string(), + message: None, + }, + Err(error) => SyncChangesetCompatibility { + checked_against_local_db: true, + status: "incompatible".to_string(), + message: Some(error.to_string()), + }, + } + } else { + SyncChangesetCompatibility { + checked_against_local_db: false, + status: "not_checked".to_string(), + message: None, + } + }; + Ok(SyncChangesetInspection { + changeset_id: changeset.changeset_id.clone(), + valid_envelope: true, + source_kind: changeset.source_kind.clone(), + scope_name: changeset.scope_name.clone(), + shape_id: changeset.shape_id.clone(), + record_count: changeset.records.len() as u64, + bytes, + tables: tables.into_iter().collect(), + operations, + start_checkpoint: changeset.start_checkpoint, + end_checkpoint: changeset.end_checkpoint, + schema_fingerprint: changeset.schema_fingerprint.clone(), + compatibility, + warnings, + }) + } + pub fn sync_apply_changeset( + &self, + changeset: &SyncChangeset, + options: ApplyChangesetOptions, + ) -> Result { + self.ensure_sync_tables()?; + self.sync_validate_changeset_envelope(changeset)?; + if !options.atomic { + return Err(DbError::sql( + "CHANGESET_UNSUPPORTED: non-atomic changeset apply is not supported", + )); + } + if let Some(principal) = options.principal.as_ref() { + principal.validate()?; + } + let mut scope_authorized_via_shape = false; + if let Some(shape_id) = changeset.shape_id.as_deref() { + let shape = self.sync_shape(shape_id)?.ok_or_else(|| { + DbError::sql(format!( + "SHAPE_NOT_FOUND: sync shape '{shape_id}' not found" + )) + })?; + scope_authorized_via_shape = true; + self.sync_authorize_shape(options.principal.as_ref(), &shape)?; + } + if let Some(scope_name) = changeset.scope_name.as_deref() { + if !scope_authorized_via_shape { + self.sync_authorize_scope(options.principal.as_ref(), scope_name)?; + } + } + if matches!( + options.compatibility_mode, + crate::sync::SyncCompatibilityMode::Strict + ) { + self.sync_check_changeset_compatibility(changeset)?; + } + let integrity_hash = self.sync_changeset_integrity_hash(changeset)?; + if let Some(existing) = + self.sync_read_metadata(&changeset_applied_key(&changeset.changeset_id))? + { + if existing != integrity_hash { + return Err(DbError::sql(format!( + "CHANGESET_ID_COLLISION: changeset '{}' was already applied with a different integrity hash", + changeset.changeset_id + ))); + } + return Ok(SyncChangesetApplyResult { + outcome: "already_applied".to_string(), + changeset_id: changeset.changeset_id.clone(), + rows_seen: changeset.records.len() as u64, + rows_applied: 0, + rows_skipped: changeset.records.len() as u64, + rows_conflicted: 0, + checkpoint_after: changeset.source_high_watermark.or(changeset.end_checkpoint), + }); + } + + let journal_records = changeset + .records + .iter() + .map(sync_journal_record_from_changeset_record) + .collect::>>()?; + let batch = SyncChangeBatch::scoped_from_records( + journal_records, + Some(changeset.source_replica_id.clone()), + changeset.source_high_watermark.or(changeset.end_checkpoint), + )?; + let summary = match (changeset.scope_name.as_deref(), options.conflict_policy) { + (Some(scope_name), Some(policy)) => { + self.sync_import_batch_for_scope_with_policy(scope_name, &batch, policy)? + } + (Some(scope_name), None) => self.sync_import_batch_for_scope(scope_name, &batch)?, + (None, Some(policy)) => self.sync_import_batch_with_policy(&batch, policy)?, + (None, None) => self.sync_import_batch(&batch)?, + }; + self.sync_upsert_metadata( + &changeset_applied_key(&changeset.changeset_id), + &integrity_hash, + )?; + self.sync_record_changeset_history(changeset, "applied", Some(current_time_micros()))?; + Ok(SyncChangesetApplyResult { + outcome: if summary.conflicted > 0 { + "conflict_recorded".to_string() + } else { + "applied".to_string() + }, + changeset_id: changeset.changeset_id.clone(), + rows_seen: summary.seen as u64, + rows_applied: summary.applied as u64, + rows_skipped: summary.skipped as u64, + rows_conflicted: summary.conflicted as u64, + checkpoint_after: changeset.source_high_watermark.or(changeset.end_checkpoint), + }) + } + pub fn sync_invert_changeset( + &self, + changeset: &SyncChangeset, + _options: InvertChangesetOptions, + ) -> Result { + self.sync_validate_changeset_envelope(changeset)?; + let created_at_micros = current_time_micros(); + let mut inverse_records = Vec::with_capacity(changeset.records.len()); + for (index, record) in changeset.records.iter().enumerate() { + let operation = match record.operation.as_str() { + "insert" => "delete", + "delete" if record.before.is_some() => "insert", + "update" if record.before.is_some() => "update", + "delete" | "update" => { + return Err(DbError::sql(format!( + "CHANGESET_INVERSION_UNSUPPORTED: record {index} lacks before image" + ))); + } + other => { + return Err(DbError::sql(format!( + "CHANGESET_INVALID: unsupported record operation '{other}'" + ))); + } + }; + inverse_records.push(SyncChangesetRecord { + record_version: record.record_version, + table: record.table.clone(), + operation: operation.to_string(), + primary_key: record.primary_key.clone(), + origin_replica_id: format!("inverse:{}", changeset.changeset_id), + origin_sequence: (index as u64) + 1, + transaction_id: format!("txn:inverse:{}", changeset.changeset_id), + transaction_lsn: (index as u64) + 1, + schema_cookie: record.schema_cookie, + before_hash: None, + before: record.after.clone(), + after: if operation == "delete" { + None + } else { + record.before.clone() + }, + column_mask: record.column_mask.clone(), + tombstone: operation == "delete", + conflict_metadata: None, + }); + } + let source_replica_id = format!("inverse:{}", changeset.source_replica_id); + let mut inverse = SyncChangeset { + changeset_version: crate::sync::SYNC_CHANGESET_VERSION, + changeset_id: sync_changeset_id( + "inverse", + &source_replica_id, + created_at_micros, + inverse_records.len(), + ), + source_replica_id, + source_kind: changeset.source_kind.clone(), + tenant_id: changeset.tenant_id.clone(), + scope_name: changeset.scope_name.clone(), + shape_id: changeset.shape_id.clone(), + base_kind: format!("inverse:{}", changeset.base_kind), + base_checkpoint: changeset.base_checkpoint.clone(), + base_branch: changeset.base_branch.clone(), + base_snapshot: changeset.base_snapshot.clone(), + start_checkpoint: inverse_records.first().map(|record| record.origin_sequence), + end_checkpoint: inverse_records.last().map(|record| record.origin_sequence), + source_high_watermark: inverse_records.last().map(|record| record.origin_sequence), + schema_fingerprint: changeset.schema_fingerprint.clone(), + schema_cookie: changeset.schema_cookie, + sync_contract_version: changeset.sync_contract_version, + query_contract_fingerprint: changeset.query_contract_fingerprint.clone(), + producer_capabilities: SyncChangesetCapabilities { + before_images: true, + ..SyncChangesetCapabilities::default() + }, + limits: SyncChangesetLimits::default(), + records: inverse_records, + conflict_policy_hint: changeset.conflict_policy_hint.clone(), + created_at_micros, + integrity_hash: None, + }; + self.sync_finalize_changeset(&mut inverse, None)?; + Ok(inverse) + } + pub fn sync_create_shape(&self, options: CreateShapeOptions) -> Result { + self.ensure_sync_tables()?; + let shape_id = options.shape_id.trim(); + let scope_name = options.scope_name.trim(); + let tenant_id = options.tenant_id.trim(); + if shape_id.is_empty() { + return Err(DbError::sql("sync shape_id must not be empty")); + } + if scope_name.is_empty() { + return Err(DbError::sql("sync shape scope_name must not be empty")); + } + if tenant_id.is_empty() { + return Err(DbError::sql( + "TENANT_REQUIRED: sync shape tenant_id is required", + )); + } + let scope = self + .sync_scope(scope_name)? + .ok_or_else(|| DbError::sql(format!("sync scope '{scope_name}' not found")))?; + if scope.include_tables.is_empty() { + return Err(DbError::sql(format!( + "sync scope '{scope_name}' has no included tables" + ))); + } + let name = options.name.as_deref().unwrap_or(shape_id).trim(); + if name.is_empty() { + return Err(DbError::sql("sync shape name must not be empty")); + } + let now = current_time_micros(); + let existing = self.sync_shape(shape_id)?; + let created_at_micros = existing + .as_ref() + .map(|shape| shape.created_at_micros) + .unwrap_or(now); + let retention_ttl_micros = options + .retention_ttl_micros + .unwrap_or(30 * 24 * 60 * 60 * 1_000_000); + let max_records = options.max_records.unwrap_or(50_000); + let ack_deadline_micros = options.ack_deadline_micros.unwrap_or(30_000_000); + let heartbeat_micros = options.heartbeat_micros.unwrap_or(20_000_000); + let allowed_roles_json = + serde_json::to_string(&options.allowed_roles).map_err(|error| { + DbError::internal(format!("failed to encode shape allowed roles: {error}")) + })?; + let allowed_subjects_json = + serde_json::to_string(&options.allowed_subjects).map_err(|error| { + DbError::internal(format!("failed to encode shape allowed subjects: {error}")) + })?; + let sql = format!( + "INSERT INTO {table} (shape_id, name, scope_name, tenant_id, allowed_roles_json, allowed_subjects_json, created_at_micros, updated_at_micros, retention_ttl_micros, max_records, ack_deadline_micros, heartbeat_micros) VALUES ({shape_id}, {name}, {scope_name}, {tenant_id}, {allowed_roles_json}, {allowed_subjects_json}, {created_at_micros}, {updated_at_micros}, {retention_ttl_micros}, {max_records}, {ack_deadline_micros}, {heartbeat_micros}) ON CONFLICT (shape_id) DO UPDATE SET name = {name}, scope_name = {scope_name}, tenant_id = {tenant_id}, allowed_roles_json = {allowed_roles_json}, allowed_subjects_json = {allowed_subjects_json}, updated_at_micros = {updated_at_micros}, retention_ttl_micros = {retention_ttl_micros}, max_records = {max_records}, ack_deadline_micros = {ack_deadline_micros}, heartbeat_micros = {heartbeat_micros}", + table = crate::sync::SHAPES_TABLE, + shape_id = sql_text_literal(shape_id), + name = sql_text_literal(name), + scope_name = sql_text_literal(&scope.name), + tenant_id = sql_text_literal(tenant_id), + allowed_roles_json = sql_text_literal(&allowed_roles_json), + allowed_subjects_json = sql_text_literal(&allowed_subjects_json), + created_at_micros = created_at_micros, + updated_at_micros = now, + retention_ttl_micros = retention_ttl_micros, + max_records = max_records, + ack_deadline_micros = ack_deadline_micros, + heartbeat_micros = heartbeat_micros, + ); + let _ = self.execute(&sql)?; + self.sync_shape(shape_id)? + .ok_or_else(|| DbError::internal("sync shape missing after create/update")) + } + pub fn sync_drop_shape(&self, shape_id: &str) -> Result { + self.ensure_sync_tables()?; + let shape_id = shape_id.trim(); + if shape_id.is_empty() { + return Err(DbError::sql("sync shape_id must not be empty")); + } + let _ = self.execute(&format!( + "DELETE FROM {} WHERE shape_id = {}", + crate::sync::SHAPE_CLIENTS_TABLE, + sql_text_literal(shape_id) + ))?; + let result = self.execute(&format!( + "DELETE FROM {} WHERE shape_id = {}", + crate::sync::SHAPES_TABLE, + sql_text_literal(shape_id) + ))?; + Ok(result.affected_rows() > 0) + } + pub fn sync_shape(&self, shape_id: &str) -> Result> { + self.ensure_sync_tables()?; + let sql = format!( + "SELECT shape_id, name, scope_name, tenant_id, allowed_roles_json, allowed_subjects_json, created_at_micros, updated_at_micros, retention_ttl_micros, max_records, ack_deadline_micros, heartbeat_micros FROM {} WHERE shape_id = {}", + crate::sync::SHAPES_TABLE, + sql_text_literal(shape_id) + ); + match self.execute(&sql) { + Ok(result) => result.rows().first().map(sync_shape_from_row).transpose(), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(None) + } else { + Err(error) + } + } + } + } + pub fn sync_shapes(&self) -> Result> { + self.ensure_sync_tables()?; + let sql = format!( + "SELECT shape_id, name, scope_name, tenant_id, allowed_roles_json, allowed_subjects_json, created_at_micros, updated_at_micros, retention_ttl_micros, max_records, ack_deadline_micros, heartbeat_micros FROM {} ORDER BY shape_id", + crate::sync::SHAPES_TABLE, + ); + match self.execute(&sql) { + Ok(result) => result.rows().iter().map(sync_shape_from_row).collect(), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(Vec::new()) + } else { + Err(error) + } + } + } + } + pub fn sync_shape_clients(&self) -> Result> { + self.ensure_sync_tables()?; + let sql = format!( + "SELECT shape_id, tenant_id, client_replica_id, subject_id, session_id, last_ack_sequence, last_ack_watermark, last_changeset_id, last_seen_at_micros, retention_blocking, status FROM {} ORDER BY shape_id, client_replica_id", + crate::sync::SHAPE_CLIENTS_TABLE, + ); + match self.execute(&sql) { + Ok(result) => result + .rows() + .iter() + .map(sync_shape_client_from_row) + .collect(), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(Vec::new()) + } else { + Err(error) + } + } + } + } + pub fn sync_shape_snapshot( + &self, + shape_id: &str, + _client_replica_id: &str, + principal: Option, + ) -> Result { + self.ensure_sync_tables()?; + let shape = self.sync_shape(shape_id)?.ok_or_else(|| { + DbError::sql(format!( + "SHAPE_NOT_FOUND: sync shape '{shape_id}' not found" + )) + })?; + self.sync_authorize_shape(principal.as_ref(), &shape)?; + let scope = self + .sync_scope(&shape.scope_name)? + .ok_or_else(|| DbError::sql(format!("sync scope '{}' not found", shape.scope_name)))?; + let changeset = + self.sync_create_shape_snapshot_changeset(&shape, &scope, principal.as_ref())?; + let shape_sequence = changeset + .source_high_watermark + .or(changeset.end_checkpoint) + .unwrap_or(0); + Ok(SyncShapeDelivery { + message_type: "snapshot".to_string(), + shape_id: shape.shape_id, + shape_sequence, + ack_deadline_micros: current_time_micros() + shape.ack_deadline_micros, + checkpoint: SyncShapeCheckpoint { + shape_sequence, + source_high_watermark: changeset.source_high_watermark.unwrap_or(shape_sequence), + }, + changeset, + }) + } + pub fn sync_shape_changes( + &self, + shape_id: &str, + since_watermark: u64, + principal: Option, + ) -> Result { + let shape = self.sync_shape(shape_id)?.ok_or_else(|| { + DbError::sql(format!( + "SHAPE_NOT_FOUND: sync shape '{shape_id}' not found" + )) + })?; + self.sync_authorize_shape(principal.as_ref(), &shape)?; + let retention = self.sync_retention_report()?; + if let Some(first_sequence) = retention.first_sequence { + if since_watermark > 0 && since_watermark < first_sequence { + return Err(DbError::sql(format!( + "SHAPE_RESYNC_REQUIRED: since checkpoint {since_watermark} is below retained first sequence {first_sequence}" + ))); + } + } + let changeset = self.sync_create_changeset(CreateChangesetOptions { + source: SyncChangesetSource::Checkpoint { + peer: shape_id.to_string(), + since_sequence: since_watermark, + }, + scope_name: Some(shape.scope_name.clone()), + shape_id: Some(shape.shape_id.clone()), + max_records: Some(shape.max_records), + max_bytes: None, + principal, + })?; + let shape_sequence = changeset + .source_high_watermark + .or(changeset.end_checkpoint) + .unwrap_or(since_watermark); + Ok(SyncShapeDelivery { + message_type: "changeset".to_string(), + shape_id: shape.shape_id, + shape_sequence, + ack_deadline_micros: current_time_micros() + shape.ack_deadline_micros, + checkpoint: SyncShapeCheckpoint { + shape_sequence, + source_high_watermark: changeset.source_high_watermark.unwrap_or(shape_sequence), + }, + changeset, + }) + } + pub fn sync_ack_shape(&self, ack: ShapeAckOptions) -> Result { + self.sync_ack_shape_with_principal(ack, None) + } + pub fn sync_ack_shape_with_principal( + &self, + ack: ShapeAckOptions, + principal: Option<&SyncPrincipal>, + ) -> Result { + self.ensure_sync_tables()?; + let shape = self.sync_shape(&ack.shape_id)?.ok_or_else(|| { + DbError::sql(format!( + "SHAPE_NOT_FOUND: sync shape '{}' not found", + ack.shape_id + )) + })?; + self.sync_authorize_shape(principal, &shape)?; + if !shape.tenant_id.eq_ignore_ascii_case(&ack.tenant_id) { + return Err(DbError::sql(format!( + "AUTH_FORBIDDEN: shape '{}' belongs to tenant '{}'", + shape.shape_id, shape.tenant_id + ))); + } + let now = current_time_micros(); + let sql = format!( + "INSERT INTO {table} (shape_id, tenant_id, client_replica_id, subject_id, session_id, last_ack_sequence, last_ack_watermark, last_changeset_id, last_seen_at_micros, retention_blocking, status) VALUES ({shape_id}, {tenant_id}, {client_replica_id}, {subject_id}, {session_id}, {last_ack_sequence}, {last_ack_watermark}, {last_changeset_id}, {last_seen_at_micros}, 1, 'active') ON CONFLICT (shape_id, client_replica_id) DO UPDATE SET tenant_id = {tenant_id}, subject_id = {subject_id}, session_id = {session_id}, last_ack_sequence = {last_ack_sequence}, last_ack_watermark = {last_ack_watermark}, last_changeset_id = {last_changeset_id}, last_seen_at_micros = {last_seen_at_micros}, retention_blocking = 1, status = 'active'", + table = crate::sync::SHAPE_CLIENTS_TABLE, + shape_id = sql_text_literal(&shape.shape_id), + tenant_id = sql_text_literal(&ack.tenant_id), + client_replica_id = sql_text_literal(&ack.client_replica_id), + subject_id = sql_text_literal(&ack.subject_id), + session_id = sql_nullable_text_literal(ack.session_id.as_deref()), + last_ack_sequence = ack.shape_sequence, + last_ack_watermark = ack.source_high_watermark, + last_changeset_id = sql_nullable_text_literal(ack.changeset_id.as_deref()), + last_seen_at_micros = now, + ); + let _ = self.execute(&sql)?; + self.sync_shape_clients()? + .into_iter() + .find(|client| { + client.shape_id == shape.shape_id + && client.client_replica_id == ack.client_replica_id + }) + .ok_or_else(|| DbError::internal("sync shape client missing after ack")) + } + pub fn sync_relay_status( + &self, + relay_id: Option<&str>, + production_mode: bool, + secure_transport_required: bool, + insecure_override_enabled: bool, + started_at_micros: Option, + ) -> Result { + let status = self.sync_status()?; + let active_sessions = self + .sync_relay_sessions()? + .into_iter() + .filter(|session| session.ended_at_micros.is_none() && session.status == "started") + .count() as u64; + Ok(SyncRelayStatus { + relay_id: relay_id.unwrap_or("relay-local").to_string(), + protocol_version: crate::sync::SYNC_RELAY_PROTOCOL_VERSION, + database_replica_id: status.replica_id, + production_mode, + secure_transport_required, + insecure_override_enabled, + active_sessions, + active_streams: 0, + started_at_micros: started_at_micros.unwrap_or_else(current_time_micros), + }) + } + pub fn sync_relay_sessions(&self) -> Result> { + self.ensure_sync_tables()?; + let sql = format!( + "SELECT session_id, tenant_id, subject_id, subject_kind, request_id, operation, scope_name, shape_id, started_at_micros, ended_at_micros, status, error, rows_seen, bytes_seen FROM {} ORDER BY started_at_micros, session_id", + crate::sync::RELAY_SESSIONS_TABLE, + ); + match self.execute(&sql) { + Ok(result) => result + .rows() + .iter() + .map(sync_relay_session_from_row) + .collect(), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(Vec::new()) + } else { + Err(error) + } + } + } + } + pub fn sync_start_relay_session( + &self, + principal: &SyncPrincipal, + operation: &str, + scope_name: Option<&str>, + shape_id: Option<&str>, + ) -> Result { + self.ensure_sync_tables()?; + principal.validate()?; + let started_at_micros = current_time_micros(); + let session_id = principal.session_id.clone(); + let sql = format!( + "INSERT INTO {table} (session_id, tenant_id, subject_id, subject_kind, request_id, operation, scope_name, shape_id, started_at_micros, ended_at_micros, status, error, rows_seen, bytes_seen) VALUES ({session_id}, {tenant_id}, {subject_id}, {subject_kind}, {request_id}, {operation}, {scope_name}, {shape_id}, {started_at_micros}, NULL, 'started', NULL, 0, 0) ON CONFLICT (session_id) DO UPDATE SET tenant_id = {tenant_id}, subject_id = {subject_id}, subject_kind = {subject_kind}, request_id = {request_id}, operation = {operation}, scope_name = {scope_name}, shape_id = {shape_id}, started_at_micros = {started_at_micros}, ended_at_micros = NULL, status = 'started', error = NULL", + table = crate::sync::RELAY_SESSIONS_TABLE, + session_id = sql_text_literal(&session_id), + tenant_id = sql_text_literal(&principal.tenant_id), + subject_id = sql_text_literal(&principal.subject_id), + subject_kind = sql_text_literal(principal.subject_kind.as_str()), + request_id = sql_text_literal(&principal.request_id), + operation = sql_text_literal(operation), + scope_name = sql_nullable_text_literal(scope_name), + shape_id = sql_nullable_text_literal(shape_id), + started_at_micros = started_at_micros, + ); + let _ = self.execute(&sql)?; + self.sync_relay_sessions()? + .into_iter() + .find(|session| session.session_id == session_id) + .ok_or_else(|| DbError::internal("sync relay session missing after start")) + } + pub fn sync_finish_relay_session( + &self, + session_id: &str, + status: &str, + error: Option<&str>, + rows_seen: u64, + bytes_seen: u64, + ) -> Result<()> { + self.ensure_sync_tables()?; + let sql = format!( + "UPDATE {table} SET ended_at_micros = {ended_at_micros}, status = {status}, error = {error}, rows_seen = {rows_seen}, bytes_seen = {bytes_seen} WHERE session_id = {session_id}", + table = crate::sync::RELAY_SESSIONS_TABLE, + ended_at_micros = current_time_micros(), + status = sql_text_literal(status), + error = sql_nullable_text_literal(error), + rows_seen = rows_seen, + bytes_seen = bytes_seen, + session_id = sql_text_literal(session_id), + ); + let _ = self.execute(&sql)?; + Ok(()) + } + pub fn sync_changeset_history(&self) -> Result> { + self.ensure_sync_tables()?; + let sql = format!( + "SELECT changeset_id, source_replica_id, source_kind, scope_name, shape_id, record_count, bytes, created_at_micros, applied_at_micros, outcome, integrity_hash FROM {} ORDER BY created_at_micros, changeset_id", + crate::sync::CHANGESET_HISTORY_TABLE, + ); + match self.execute(&sql) { + Ok(result) => result + .rows() + .iter() + .map(sync_changeset_history_from_row) + .collect(), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(Vec::new()) + } else { + Err(error) + } + } + } + } + fn sync_authorize_scope( + &self, + principal: Option<&SyncPrincipal>, + scope_name: &str, + ) -> Result<()> { + if let Some(principal) = principal { + if !principal.allows_scope(scope_name) { + return Err(DbError::sql(format!( + "SCOPE_UNAUTHORIZED: principal '{}' cannot access scope '{}'", + principal.subject_id, scope_name + ))); + } + } + Ok(()) + } + fn sync_authorize_shape( + &self, + principal: Option<&SyncPrincipal>, + shape: &SyncShape, + ) -> Result<()> { + let Some(principal) = principal else { + return Ok(()); + }; + if !principal.tenant_id.eq_ignore_ascii_case(&shape.tenant_id) { + return Err(DbError::sql(format!( + "AUTH_FORBIDDEN: shape '{}' belongs to tenant '{}'", + shape.shape_id, shape.tenant_id + ))); + } + if !principal.allows_shape(&shape.shape_id) { + return Err(DbError::sql(format!( + "AUTH_FORBIDDEN: principal '{}' cannot access shape '{}'", + principal.subject_id, shape.shape_id + ))); + } + if !shape.allowed_subjects.is_empty() + && !shape + .allowed_subjects + .iter() + .any(|subject| subject == "*" || subject == &principal.subject_id) + { + return Err(DbError::sql(format!( + "AUTH_FORBIDDEN: subject '{}' is not allowed for shape '{}'", + principal.subject_id, shape.shape_id + ))); + } + if !shape.allowed_roles.is_empty() + && !principal.roles.iter().any(|role| { + shape + .allowed_roles + .iter() + .any(|allowed| allowed == "*" || allowed == role) + }) + { + return Err(DbError::sql(format!( + "AUTH_FORBIDDEN: principal '{}' lacks a role for shape '{}'", + principal.subject_id, shape.shape_id + ))); + } + Ok(()) + } + fn sync_create_shape_snapshot_changeset( + &self, + shape: &SyncShape, + scope: &SyncScope, + principal: Option<&SyncPrincipal>, + ) -> Result { + let created_at_micros = current_time_micros(); + let tooling = self.get_tooling_metadata()?; + let runtime = self.runtime_for_metadata_inspection()?; + let schema_cookie = runtime.catalog.schema_cookie; + let source_replica_id = self + .sync_status() + .ok() + .and_then(|status| status.replica_id) + .unwrap_or_else(|| "snapshot".to_string()); + let mut records = Vec::new(); + let mut origin_sequence = 1u64; + for table_name in &scope.include_tables { + let table = runtime.catalog.table(table_name).ok_or_else(|| { + DbError::sql(format!("sync scope table '{table_name}' does not exist")) + })?; + let column_sql = table + .columns + .iter() + .map(|column| sql_identifier(&column.name)) + .collect::>() + .join(", "); + let order_by = table + .primary_key_columns + .iter() + .map(|column| sql_identifier(column)) + .collect::>() + .join(", "); + let where_sql = scope + .row_filter + .as_ref() + .map(|filter| format!(" WHERE {filter}")) + .unwrap_or_default(); + let sql = format!( + "SELECT {column_sql} FROM {}{where_sql} ORDER BY {order_by}", + sql_identifier(&table.name) + ); + let result = self.execute(&sql)?; + for row in result.rows() { + let after = crate::sync::build_after_json(table, row.values()); + let primary_key = crate::sync::build_primary_key_json(table, row.values()); + records.push(SyncChangesetRecord { + record_version: 1, + table: table.name.clone(), + operation: "insert".to_string(), + primary_key, + origin_replica_id: source_replica_id.clone(), + origin_sequence, + transaction_id: format!("shape-snapshot:{}:{origin_sequence}", shape.shape_id), + transaction_lsn: origin_sequence, + schema_cookie, + before_hash: None, + before: None, + after: Some(after), + column_mask: table + .columns + .iter() + .map(|column| column.name.clone()) + .collect(), + tombstone: false, + conflict_metadata: None, + }); + origin_sequence += 1; + if records.len() as u64 > shape.max_records { + return Err(DbError::sql( + "BATCH_TOO_LARGE: shape snapshot exceeds max_records", + )); + } + } + } + let high_watermark = self.sync_integrity_report()?.last_sequence.unwrap_or(0); + let mut changeset = SyncChangeset { + changeset_version: crate::sync::SYNC_CHANGESET_VERSION, + changeset_id: sync_changeset_id( + "shape_snapshot", + &source_replica_id, + created_at_micros, + records.len(), + ), + source_replica_id, + source_kind: crate::sync::SyncChangesetSourceKind::Snapshot, + tenant_id: Some( + principal + .map(|principal| principal.tenant_id.clone()) + .unwrap_or_else(|| shape.tenant_id.clone()), + ), + scope_name: Some(scope.name.clone()), + shape_id: Some(shape.shape_id.clone()), + base_kind: "snapshot".to_string(), + base_checkpoint: None, + base_branch: None, + base_snapshot: Some(format!("shape:{}", shape.shape_id)), + start_checkpoint: records.first().map(|record| record.origin_sequence), + end_checkpoint: records.last().map(|record| record.origin_sequence), + source_high_watermark: Some(high_watermark), + schema_fingerprint: tooling.schema_fingerprint, + schema_cookie, + sync_contract_version: crate::sync::SYNC_CONTRACT_VERSION, + query_contract_fingerprint: None, + producer_capabilities: SyncChangesetCapabilities::default(), + limits: SyncChangesetLimits::default(), + records, + conflict_policy_hint: None, + created_at_micros, + integrity_hash: None, + }; + self.sync_finalize_changeset(&mut changeset, None)?; + self.sync_record_changeset_history(&changeset, "created", None)?; + Ok(changeset) + } + fn sync_finalize_changeset( + &self, + changeset: &mut SyncChangeset, + max_bytes: Option, + ) -> Result<()> { + changeset.limits.record_count = changeset.records.len() as u64; + changeset.limits.uncompressed_bytes = 0; + changeset.integrity_hash = None; + let bytes = serde_json::to_vec(changeset) + .map_err(|error| DbError::internal(format!("failed to serialize changeset: {error}")))? + .len() as u64; + if max_bytes.is_some_and(|limit| bytes > limit) { + return Err(DbError::sql(format!( + "BATCH_TOO_LARGE: changeset is {bytes} bytes" + ))); + } + changeset.limits.uncompressed_bytes = bytes; + let hash = self.sync_changeset_integrity_hash(changeset)?; + changeset.integrity_hash = Some(hash); + Ok(()) + } + fn sync_validate_changeset_envelope(&self, changeset: &SyncChangeset) -> Result<()> { + if changeset.changeset_version != crate::sync::SYNC_CHANGESET_VERSION { + return Err(DbError::sql(format!( + "CHANGESET_UNSUPPORTED: unsupported changeset version {}", + changeset.changeset_version + ))); + } + if changeset.sync_contract_version != crate::sync::SYNC_CONTRACT_VERSION { + return Err(DbError::sql(format!( + "CHANGESET_UNSUPPORTED: unsupported sync contract version {}", + changeset.sync_contract_version + ))); + } + if changeset.changeset_id.trim().is_empty() { + return Err(DbError::sql("CHANGESET_INVALID: changeset_id is required")); + } + let Some(expected_hash) = changeset.integrity_hash.as_deref() else { + return Err(DbError::sql( + "CHANGESET_INVALID: integrity_hash is required", + )); + }; + let actual_hash = self.sync_changeset_integrity_hash(changeset)?; + if expected_hash != actual_hash { + return Err(DbError::sql( + "CHANGESET_INVALID: integrity_hash does not match payload", + )); + } + for (index, record) in changeset.records.iter().enumerate() { + if record.record_version != 1 { + return Err(DbError::sql(format!( + "CHANGESET_UNSUPPORTED: record {index} uses version {}", + record.record_version + ))); + } + match record.operation.as_str() { + "insert" | "update" if record.after.is_none() => { + return Err(DbError::sql(format!( + "CHANGESET_INVALID: record {index} operation '{}' requires after image", + record.operation + ))); + } + "insert" | "update" | "delete" => {} + other => { + return Err(DbError::sql(format!( + "CHANGESET_INVALID: unsupported record operation '{other}'" + ))); + } + } + } + Ok(()) + } + fn sync_check_changeset_compatibility(&self, changeset: &SyncChangeset) -> Result<()> { + let tooling = self.get_tooling_metadata()?; + if tooling.schema_fingerprint != changeset.schema_fingerprint { + return Err(DbError::sql(format!( + "SCHEMA_INCOMPATIBLE: local schema fingerprint {} does not match changeset {}", + tooling.schema_fingerprint, changeset.schema_fingerprint + ))); + } + let runtime = self.runtime_for_metadata_inspection()?; + if runtime.catalog.schema_cookie != changeset.schema_cookie { + return Err(DbError::sql(format!( + "SCHEMA_INCOMPATIBLE: local schema_cookie {} does not match changeset {}", + runtime.catalog.schema_cookie, changeset.schema_cookie + ))); + } + Ok(()) + } + fn sync_changeset_integrity_hash(&self, changeset: &SyncChangeset) -> Result { + let mut clone = changeset.clone(); + clone.integrity_hash = None; + let bytes = serde_json::to_vec(&clone).map_err(|error| { + DbError::internal(format!("failed to serialize changeset: {error}")) + })?; + let digest = Sha256::digest(&bytes); + Ok(format!("sha256:{}", hex_encode(&digest))) + } + fn sync_record_changeset_history( + &self, + changeset: &SyncChangeset, + outcome: &str, + applied_at_micros: Option, + ) -> Result<()> { + self.ensure_sync_tables()?; + let sql = format!( + "INSERT INTO {table} (changeset_id, source_replica_id, source_kind, scope_name, shape_id, record_count, bytes, created_at_micros, applied_at_micros, outcome, integrity_hash) VALUES ({changeset_id}, {source_replica_id}, {source_kind}, {scope_name}, {shape_id}, {record_count}, {bytes}, {created_at_micros}, {applied_at_micros}, {outcome}, {integrity_hash}) ON CONFLICT (changeset_id) DO UPDATE SET applied_at_micros = COALESCE({applied_at_micros}, applied_at_micros), outcome = {outcome}, integrity_hash = {integrity_hash}", + table = crate::sync::CHANGESET_HISTORY_TABLE, + changeset_id = sql_text_literal(&changeset.changeset_id), + source_replica_id = sql_text_literal(&changeset.source_replica_id), + source_kind = sql_text_literal(changeset.source_kind.as_str()), + scope_name = sql_nullable_text_literal(changeset.scope_name.as_deref()), + shape_id = sql_nullable_text_literal(changeset.shape_id.as_deref()), + record_count = changeset.records.len(), + bytes = changeset.limits.uncompressed_bytes, + created_at_micros = changeset.created_at_micros, + applied_at_micros = applied_at_micros + .map(|value| value.to_string()) + .unwrap_or_else(|| "NULL".to_string()), + outcome = sql_text_literal(outcome), + integrity_hash = sql_nullable_text_literal(changeset.integrity_hash.as_deref()), + ); + let _ = self.execute(&sql)?; + Ok(()) + } + pub fn sync_conflict_policy(&self) -> Result { + let default_policy = self + .sync_read_metadata("conflict_policy")? + .map(|value| SyncConflictPolicy::from_str(&value)) + .transpose()? + .unwrap_or_default(); + let origin_priority = match self.sync_read_metadata("conflict_origin_priority")? { + Some(value) => serde_json::from_str::>(&value).map_err(|error| { + DbError::sql(format!( + "invalid sync conflict origin priority metadata: {error}" + )) + })?, + None => Vec::new(), + }; + Ok(SyncConflictPolicyConfig { + default_policy, + origin_priority, + }) + } + pub fn sync_set_conflict_policy( + &self, + policy: SyncConflictPolicy, + origin_priority: &[&str], + ) -> Result<()> { + self.ensure_sync_tables()?; + let origin_priority = origin_priority + .iter() + .map(|value| value.trim()) + .map(|value| { + if value.is_empty() { + Err(DbError::sql( + "sync conflict origin priority entries must not be empty", + )) + } else { + Ok(value.to_string()) + } + }) + .collect::>>()?; + self.sync_upsert_metadata("conflict_policy", policy.as_str())?; + self.sync_upsert_metadata( + "conflict_origin_priority", + &serde_json::to_string(&origin_priority).map_err(|error| { + DbError::internal(format!( + "failed to serialize sync conflict origin priority: {error}" + )) + })?, + )?; + Ok(()) + } + pub fn sync_import_batch(&self, batch: &SyncChangeBatch) -> Result { + let policy = self.sync_conflict_policy()?.default_policy; + self.sync_import_batch_with_policy(batch, policy) + } + pub fn sync_import_batch_with_policy( + &self, + batch: &SyncChangeBatch, + policy: SyncConflictPolicy, + ) -> Result { + batch.validate()?; + self.ensure_sync_tables()?; + + let runtime = self.runtime_for_metadata_inspection()?; + let schema_cookie = runtime.catalog.schema_cookie; + let local_replica_id = self + .inner + .sync_ctx + .replica_id() + .or_else(|| self.sync_read_metadata("replica_id").ok().flatten()); + let batch_source_replica_id = batch.source_replica_id.as_deref(); + let batch_watermark = batch.source_high_watermark.or(batch.last_sequence); + let current_peer_watermark = match batch_source_replica_id { + Some(replica_id) => self.sync_peer_watermark(replica_id)?, + None => None, + }; + + if let Some(local_replica_id) = local_replica_id.as_deref() { + if batch_source_replica_id == Some(local_replica_id) { + return Err(DbError::sql(format!( + "cannot import batch from same replica '{}'", + local_replica_id + ))); + } + } + + let _suppress_capture = self.inner.sync_ctx.suppress_capture(); + struct SyncImportTransaction<'a>(&'a Db, bool); + impl<'a> SyncImportTransaction<'a> { + fn new(db: &'a Db) -> Result { + db.begin_transaction()?; + Ok(Self(db, true)) + } + fn commit(mut self) -> Result<()> { + self.1 = false; + self.0.commit_transaction()?; + Ok(()) + } + } + impl Drop for SyncImportTransaction<'_> { + fn drop(&mut self) { + if self.1 { + let _ = self.0.rollback_transaction(); + } + } + } + + if let Some(batch_watermark) = batch_watermark { + if current_peer_watermark.is_some_and(|watermark| batch_watermark <= watermark) { + if let Some(replica_id) = batch_source_replica_id { + let watermark = current_peer_watermark + .map_or(batch_watermark, |current| current.max(batch_watermark)); + self.sync_upsert_metadata( + &peer_watermark_key(replica_id), + &watermark.to_string(), + )?; + } + return Ok(SyncImportSummary { + seen: batch.record_count, + applied: 0, + skipped: batch.record_count, + conflicted: 0, + }); + } + } + + let tx = SyncImportTransaction::new(self)?; + let mut applied = 0usize; + let mut skipped = 0usize; + let mut conflicted = 0usize; + let mut stop_conflict: Option<(SyncJournalRecord, SyncConflictRecordData)> = None; + + for record in &batch.records { + if let Some(local_replica_id) = local_replica_id.as_deref() { + if record.replica_id == local_replica_id { + return Err(DbError::sql(format!( + "cannot import record from same replica '{}'", + local_replica_id + ))); + } + } + + if let Some(watermark) = current_peer_watermark { + if record.sequence <= watermark { + skipped += 1; + continue; + } + } + + if record.schema_version != 1 { + return Err(DbError::sql(format!( + "unsupported sync record schema version {}", + record.schema_version + ))); + } + + if record.schema_cookie != schema_cookie { + return Err(DbError::sql(format!( + "schema mismatch for table '{}': record has schema_cookie {} but local schema is {}", + record.table, record.schema_cookie, schema_cookie + ))); + } + + let table = runtime + .catalog + .table(&record.table) + .ok_or_else(|| DbError::sql(format!("unknown table '{}'", record.table)))?; + if crate::sync::is_internal_table_name(&table.name) { + return Err(DbError::sql(format!( + "cannot import into internal table '{}'", + table.name + ))); + } + let marker_key = imported_record_key(&record.replica_id, record.sequence); + if self.sync_read_metadata(&marker_key)?.is_some() { + skipped += 1; + continue; + } + + let outcome = self.sync_apply_import_record(batch, record, table, &policy)?; + match outcome { + SyncImportRecordOutcome::Applied => { + self.sync_upsert_metadata(&marker_key, "applied")?; + applied += 1; + } + SyncImportRecordOutcome::Conflict(conflict) => { + if matches!(policy, SyncConflictPolicy::Stop) { + stop_conflict = Some((record.clone(), conflict)); + break; + } + self.record_sync_conflict_with_data(batch, record, &conflict)?; + conflicted += 1; + } + SyncImportRecordOutcome::Resolved(conflict) => { + self.sync_upsert_metadata(&marker_key, "applied")?; + self.record_sync_conflict_with_data(batch, record, &conflict)?; + applied += 1; + conflicted += 1; + } + } + } + + if let Some((record, conflict)) = stop_conflict { + drop(tx); + let conflict_id = self.record_sync_conflict_with_data(batch, &record, &conflict)?; + return Err(DbError::sql(format!( + "sync import stopped on conflict {}", + conflict_id + ))); + } + + if let (Some(replica_id), Some(batch_watermark)) = + (batch_source_replica_id, batch_watermark) + { + let watermark = current_peer_watermark + .map_or(batch_watermark, |current| current.max(batch_watermark)); + self.sync_upsert_metadata(&peer_watermark_key(replica_id), &watermark.to_string())?; + } + + crate::reactive::with_change_source(ChangeSource::SyncApply, || tx.commit())?; + Ok(SyncImportSummary { + seen: batch.record_count, + applied, + skipped, + conflicted, + }) + } + pub fn sync_import_records(&self, records: &[SyncJournalRecord]) -> Result { + let batch = SyncChangeBatch::from_records(records.to_vec())?; + self.sync_import_batch(&batch) + } + pub fn sync_peer_watermark(&self, replica_id: &str) -> Result> { + match self.sync_read_metadata(&peer_watermark_key(replica_id))? { + Some(value) => value + .parse::() + .map(Some) + .map_err(|error| DbError::sql(format!("invalid peer watermark value: {error}"))), + None => Ok(None), + } + } + pub fn sync_peer_out_watermark(&self, peer_name: &str) -> Result> { + match self.sync_read_metadata(&peer_out_watermark_key(peer_name))? { + Some(value) => value.parse::().map(Some).map_err(|error| { + DbError::sql(format!("invalid peer outbound watermark value: {error}")) + }), + None => Ok(None), + } + } + pub fn sync_set_peer_out_watermark(&self, peer_name: &str, watermark: u64) -> Result<()> { + self.ensure_sync_tables()?; + self.sync_upsert_metadata(&peer_out_watermark_key(peer_name), &watermark.to_string()) + } + pub fn sync_conflicts(&self) -> Result> { + self.ensure_sync_tables()?; + let sql = format!( + "SELECT * FROM {} WHERE resolved = 0 ORDER BY conflict_id", + crate::sync::CONFLICTS_TABLE + ); + match self.execute(&sql) { + Ok(result) => result.rows().iter().map(sync_conflict_from_row).collect(), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(Vec::new()) + } else { + Err(error) + } + } + } + } + pub fn sync_conflicts_all(&self) -> Result> { + self.ensure_sync_tables()?; + let sql = format!( + "SELECT * FROM {} ORDER BY conflict_id", + crate::sync::CONFLICTS_TABLE + ); + match self.execute(&sql) { + Ok(result) => result.rows().iter().map(sync_conflict_from_row).collect(), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(Vec::new()) + } else { + Err(error) + } + } + } + } + pub fn sync_conflict(&self, conflict_id: i64) -> Result> { + self.ensure_sync_tables()?; + let sql = format!( + "SELECT * FROM {} WHERE conflict_id = {}", + crate::sync::CONFLICTS_TABLE, + conflict_id + ); + match self.execute(&sql) { + Ok(result) => Ok(result + .rows() + .first() + .map(sync_conflict_from_row) + .transpose()?), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(None) + } else { + Err(error) + } + } + } + } + pub fn sync_resolve_conflict_keep_local( + &self, + conflict_id: i64, + resolved_by: Option<&str>, + note: Option<&str>, + ) -> Result { + self.sync_update_conflict_resolution( + conflict_id, + Some("keep_local"), + resolved_by, + note, + Some(current_time_micros()), + ) + } + pub fn sync_resolve_conflict_apply_remote( + &self, + conflict_id: i64, + resolved_by: Option<&str>, + note: Option<&str>, + ) -> Result { + let Some(conflict) = self.sync_conflict(conflict_id)? else { + return Ok(false); + }; + let record: SyncJournalRecord = serde_json::from_value(conflict.remote_record_json.clone()) + .map_err(|error| { + DbError::corruption(format!( + "malformed sync conflict remote_record_json: {error}" + )) + })?; + let batch = SyncChangeBatch::from_records(vec![record.clone()])?; + let policy = SyncConflictPolicy::LastWriterWins; + let _suppress_capture = self.inner.sync_ctx.suppress_capture(); + let tx = { + self.begin_transaction()?; + struct Tx<'a>(&'a Db, bool); + impl<'a> Drop for Tx<'a> { + fn drop(&mut self) { + if self.1 { + let _ = self.0.rollback_transaction(); + } + } + } + impl<'a> Tx<'a> { + fn commit(mut self) -> Result<()> { + self.1 = false; + self.0.commit_transaction()?; + Ok(()) + } + } + Tx(self, true) + }; + let runtime = self.runtime_for_metadata_inspection()?; + let table = runtime + .catalog + .table(&record.table) + .ok_or_else(|| DbError::sql(format!("unknown table '{}'", record.table)))?; + match self.sync_apply_import_record(&batch, &record, table, &policy)? { + SyncImportRecordOutcome::Applied => { + self.sync_upsert_metadata( + &imported_record_key(&record.replica_id, record.sequence), + "applied", + )?; + self.sync_update_conflict_resolution( + conflict_id, + Some("apply_remote"), + resolved_by, + note, + Some(current_time_micros()), + )?; + tx.commit()?; + Ok(true) + } + SyncImportRecordOutcome::Resolved(_) => { + self.sync_upsert_metadata( + &imported_record_key(&record.replica_id, record.sequence), + "applied", + )?; + self.sync_update_conflict_resolution( + conflict_id, + Some("apply_remote"), + resolved_by, + note, + Some(current_time_micros()), + )?; + tx.commit()?; + Ok(true) + } + SyncImportRecordOutcome::Conflict(conflict) => { + let _ = conflict; + Err(DbError::sql(format!( + "cannot apply remote conflict {} because replay now fails", + conflict_id + ))) + } + } + } + pub fn sync_reopen_conflict(&self, conflict_id: i64) -> Result { + self.sync_update_conflict_resolution(conflict_id, None, None, None, None) + } + pub fn sync_prune_journal_through(&self, sequence: u64) -> Result { + self.sync_prune_journal(sequence, false, false) + .map(|summary| summary.pruned) + } + pub fn sync_prune_journal( + &self, + through: u64, + dry_run: bool, + allow_data_loss: bool, + ) -> Result { + let retention = self.sync_retention_report()?; + let requested_through = through; + if !allow_data_loss && through > retention.safe_prune_through.unwrap_or(0) { + let message = if let Some(lowest_watermark) = + retention.safe_prune_through.map(|value| value + 1) + { + format!( + "cannot prune through {through}; lowest peer watermark is {lowest_watermark}" + ) + } else if retention.blocked_by.is_empty() { + format!("cannot prune through {through}; no peer watermarks are known") + } else { + format!("cannot prune through {through}; lowest peer watermark is 0") + }; + return Err(DbError::sql(message)); + } + + let records = crate::sync::read_journal_records( + self.inner.sync_ctx.journal_path(), + &self.inner.vfs, + 0, + usize::MAX, + )?; + if records.is_empty() { + return Ok(SyncPruneSummary { + requested_through, + effective_through: 0, + pruned: 0, + dry_run, + allow_data_loss, + blocked_by: retention.blocked_by, + }); + } + + let effective_through = records + .last() + .map(|record| record.sequence.min(through)) + .unwrap_or(0); + let total_records = records.len(); + let retained = records + .into_iter() + .filter(|record| record.sequence > through) + .collect::>(); + let pruned = total_records.saturating_sub(retained.len()); + + if dry_run || pruned == 0 { + return Ok(SyncPruneSummary { + requested_through, + effective_through, + pruned, + dry_run, + allow_data_loss, + blocked_by: retention.blocked_by, + }); + } + + let mut buffer = Vec::new(); + for record in &retained { + serde_json::to_writer(&mut buffer, record).map_err(|error| { + DbError::internal(format!("failed to serialize sync journal record: {error}")) + })?; + buffer.push(b'\n'); + } + + if self + .inner + .vfs + .file_exists(self.inner.sync_ctx.journal_path())? + { + let journal_file = self.inner.sync_ctx.journal_file_handle()?; + let journal_file = match journal_file { + Some(file) => file, + None => self.inner.vfs.open( + self.inner.sync_ctx.journal_path(), + OpenMode::OpenExisting, + FileKind::SyncJournal, + )?, + }; + + journal_file.set_len(0)?; + write_all_at(journal_file.as_ref(), 0, &buffer)?; + journal_file.sync_data()?; + self.inner + .sync_ctx + .set_journal_write_offset(buffer.len() as u64)?; + } + + Ok(SyncPruneSummary { + requested_through, + effective_through, + pruned, + dry_run, + allow_data_loss, + blocked_by: retention.blocked_by, + }) + } + pub fn sync_set_enabled(&self, enabled: bool) -> Result<()> { + self.ensure_sync_tables()?; + self.sync_upsert_metadata("enabled", if enabled { "true" } else { "false" })?; + self.inner.sync_ctx.set_enabled(enabled); + if enabled { + self.inner.sync_ctx.ensure_journal_open(&self.inner.vfs)?; + } + Ok(()) + } + pub fn sync_is_enabled(&self) -> Result { + if self.inner.sync_ctx.is_enabled() { + return Ok(true); + } + let status = self.load_sync_status_from_db()?; + if status.enabled { + self.inner.sync_ctx.set_enabled(true); + self.inner + .sync_ctx + .set_replica_id(&status.replica_id.unwrap_or_default()); + self.inner.sync_ctx.set_next_sequence(status.next_sequence); + } + Ok(status.enabled) + } + fn sync_upsert_metadata(&self, key: &str, value: &str) -> Result<()> { + let sql = format!( + "INSERT INTO {table} (key, value) VALUES ('{k}', '{v}') ON CONFLICT (key) DO UPDATE SET value = '{v}'", + table = crate::sync::METADATA_TABLE, + k = key.replace('\'', "''"), + v = value.replace('\'', "''"), + ); + let _ = self.execute(&sql)?; + Ok(()) + } + pub(crate) fn sync_read_metadata(&self, key: &str) -> Result> { + let sql = format!( + "SELECT value FROM {} WHERE key = '{}'", + crate::sync::METADATA_TABLE, + key.replace('\'', "''"), + ); + match self.execute(&sql) { + Ok(result) => { + if let Some(row) = result.rows().first() { + if let Some(val) = row.values().first() { + match val { + Value::Text(s) => return Ok(Some(s.clone())), + _ => return Ok(None), + } + } + } + Ok(None) + } + Err(e) => { + let msg = e.to_string(); + if msg.contains("no such table") || msg.contains("unknown table") { + return Ok(None); + } + Err(e) + } + } + } + fn sync_metadata_entries(&self) -> Result> { + let sql = format!("SELECT key, value FROM {}", crate::sync::METADATA_TABLE); + match self.execute(&sql) { + Ok(result) => result + .rows() + .iter() + .map(|row| { + let key = row + .values() + .first() + .and_then(|value| match value { + Value::Text(text) => Some(text.clone()), + _ => None, + }) + .ok_or_else(|| DbError::corruption("malformed sync metadata row"))?; + let value = row + .values() + .get(1) + .and_then(|value| match value { + Value::Text(text) => Some(text.clone()), + _ => None, + }) + .ok_or_else(|| DbError::corruption("malformed sync metadata row"))?; + Ok((key, value)) + }) + .collect(), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(Vec::new()) + } else { + Err(error) + } + } + } + } + fn sync_peer_watermark_entries(&self) -> Result> { + self.sync_metadata_entries()? + .into_iter() + .filter_map(|(key, value)| { + key.strip_prefix("peer_watermark:") + .map(|replica_id| (replica_id.to_string(), value)) + }) + .map(|(replica_id, value)| { + let watermark = value.parse::().map_err(|error| { + DbError::sql(format!( + "invalid peer watermark value for replica '{}': {}", + replica_id, error + )) + })?; + Ok((format!("remote:{replica_id}"), watermark)) + }) + .collect() + } + fn sync_capture_local_row_json( + &self, + table: &TableSchema, + primary_key: &serde_json::Map, + ) -> Result> { + let (mut runtime, snapshot_lsn) = self.runtime_for_targeted_row_source_inspection()?; + if let Some(snapshot_lsn) = snapshot_lsn { + self.load_runtime_table_row_sources_at_snapshot( + &mut runtime, + &[table.name.as_str()], + snapshot_lsn, + )?; + } + let Some(source) = runtime.table_row_source(&table.name) else { + return Ok(None); + }; + for row in source.rows() { + let row = row?; + let values = row.values(); + let mut matches = true; + for pk_col in &table.primary_key_columns { + let column = table + .columns + .iter() + .find(|column| column.name == *pk_col) + .ok_or_else(|| { + DbError::sql(format!( + "table '{}' missing primary key column '{}'", + table.name, pk_col + )) + })?; + let json_value = primary_key.get(pk_col).ok_or_else(|| { + DbError::sql(format!( + "missing primary key column '{pk_col}' in record for table '{}'", + table.name + )) + })?; + let expected = json_to_column_value(&table.name, column, json_value)?; + let Some(actual) = values.get( + table + .columns + .iter() + .position(|candidate| candidate.name == *pk_col) + .ok_or_else(|| { + DbError::sql(format!( + "table '{}' missing primary key column '{}'", + table.name, pk_col + )) + })?, + ) else { + matches = false; + break; + }; + if actual != &expected { + matches = false; + break; + } + } + if matches { + return Ok(Some(crate::sync::build_after_json(table, values))); + } + } + Ok(None) + } + fn sync_apply_import_record( + &self, + _batch: &SyncChangeBatch, + record: &SyncJournalRecord, + table: &TableSchema, + policy: &SyncConflictPolicy, + ) -> Result { + let primary_key = record + .primary_key + .as_object() + .ok_or_else(|| DbError::sql("primary_key must be an object"))?; + let local_row_json = self.sync_capture_local_row_json(table, primary_key)?; + let operation = match record.operation.as_str() { + "insert" => SyncOperation::Insert, + "update" => SyncOperation::Update, + "delete" => SyncOperation::Delete, + other => return Err(DbError::sql(format!("unsupported operation '{other}'"))), + }; + + let remote_wins = match policy { + SyncConflictPolicy::Record | SyncConflictPolicy::Stop => false, + SyncConflictPolicy::LastWriterWins => true, + SyncConflictPolicy::OriginPriority => { + let config = self.sync_conflict_policy()?; + match self + .inner + .sync_ctx + .replica_id() + .or_else(|| self.sync_read_metadata("replica_id").ok().flatten()) + { + Some(local_replica_id) => { + let remote_index = config + .origin_priority + .iter() + .position(|replica| replica == &record.replica_id); + let local_index = config + .origin_priority + .iter() + .position(|replica| replica == &local_replica_id); + matches!((remote_index, local_index), (Some(remote), Some(local)) if remote < local) + } + None => false, + } + } + }; + + let apply_remote_replace = |operation: SyncOperation| -> Result<()> { + let sql = format!( + "DELETE FROM {} WHERE {}", + sql_identifier(&table.name), + table + .primary_key_columns + .iter() + .enumerate() + .map(|(idx, pk_col)| format!("{} = ${}", sql_identifier(pk_col), idx + 1)) + .collect::>() + .join(" AND ") + ); + let mut where_values = Vec::with_capacity(table.primary_key_columns.len()); + for pk_col in &table.primary_key_columns { + let column = table + .columns + .iter() + .find(|column| column.name == *pk_col) + .ok_or_else(|| { + DbError::sql(format!( + "table '{}' missing primary key column '{}'", + table.name, pk_col + )) + })?; + let json_value = primary_key.get(pk_col).ok_or_else(|| { + DbError::sql(format!( + "missing primary key column '{pk_col}' in record for table '{}'", + table.name + )) + })?; + where_values.push(json_to_column_value(&table.name, column, json_value)?); + } + let _ = self.execute_with_params(&sql, &where_values)?; + + if matches!(operation, SyncOperation::Delete) { + return Ok(()); + } + + let after = record + .after + .as_ref() + .ok_or_else(|| DbError::sql("remote record missing after payload"))? + .as_object() + .ok_or_else(|| DbError::sql("remote record after payload must be an object"))?; + let mut columns = Vec::with_capacity(table.columns.len()); + let mut values = Vec::with_capacity(table.columns.len()); + for column in &table.columns { + let json_value = after.get(&column.name).ok_or_else(|| { + DbError::sql(format!( + "missing column '{}' in after payload for table '{}'", + column.name, table.name + )) + })?; + columns.push(sql_identifier(&column.name)); + values.push(json_to_column_value(&table.name, column, json_value)?); + } + let sql = format!( + "INSERT INTO {} ({}) VALUES ({})", + sql_identifier(&table.name), + columns.join(", "), + (1..=values.len()) + .map(|idx| format!("${idx}")) + .collect::>() + .join(", ") + ); + let _ = self.execute_with_params(&sql, &values)?; + Ok(()) + }; + + match operation { + SyncOperation::Insert => { + let after = record + .after + .as_ref() + .ok_or_else(|| DbError::sql("insert record missing after payload"))? + .as_object() + .ok_or_else(|| DbError::sql("after must be an object for insert"))?; + let mut columns = Vec::with_capacity(table.columns.len()); + let mut values = Vec::with_capacity(table.columns.len()); + for column in &table.columns { + let json_value = after.get(&column.name).ok_or_else(|| { + DbError::sql(format!( + "missing column '{}' in after payload for table '{}'", + column.name, table.name + )) + })?; + columns.push(sql_identifier(&column.name)); + values.push(json_to_column_value(&table.name, column, json_value)?); + } + let sql = format!( + "INSERT INTO {} ({}) VALUES ({})", + sql_identifier(&table.name), + columns.join(", "), + (1..=values.len()) + .map(|idx| format!("${idx}")) + .collect::>() + .join(", ") + ); + match self.execute_with_params(&sql, &values) { + Ok(_) => Ok(SyncImportRecordOutcome::Applied), + Err(DbError::Constraint { message }) if remote_wins => { + apply_remote_replace(SyncOperation::Insert)?; + Ok(SyncImportRecordOutcome::Resolved(SyncConflictRecordData { + conflict_type: "insert_insert".to_string(), + message, + local_row_json, + resolution: Some("remote_applied".to_string()), + resolved_at_micros: Some(current_time_micros()), + resolved_by: Some("sync_policy".to_string()), + resolution_note: None, + policy_name: Some(policy.as_str().to_string()), + })) + } + Err(DbError::Constraint { message }) => { + Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { + conflict_type: if local_row_json.is_some() { + "insert_insert".to_string() + } else { + "constraint_error".to_string() + }, + message, + local_row_json, + resolution: None, + resolved_at_micros: None, + resolved_by: None, + resolution_note: None, + policy_name: Some(policy.as_str().to_string()), + })) + } + Err(error) => Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { + conflict_type: "apply_error".to_string(), + message: error.to_string(), + local_row_json, + resolution: None, + resolved_at_micros: None, + resolved_by: None, + resolution_note: None, + policy_name: Some(policy.as_str().to_string()), + })), + } + } + SyncOperation::Update => { + let after = record + .after + .as_ref() + .ok_or_else(|| DbError::sql("update record missing after payload"))? + .as_object() + .ok_or_else(|| DbError::sql("after must be an object for update"))?; + let mut params = + Vec::with_capacity(table.columns.len() + table.primary_key_columns.len()); + let mut expressions = Vec::with_capacity(table.columns.len()); + for column in &table.columns { + let json_value = after.get(&column.name).ok_or_else(|| { + DbError::sql(format!( + "missing column '{}' in update payload for table '{}'", + column.name, table.name + )) + })?; + params.push(json_to_column_value(&table.name, column, json_value)?); + expressions.push(format!( + "{} = ${}", + sql_identifier(&column.name), + params.len() + )); + } + for pk_col in &table.primary_key_columns { + let column = table + .columns + .iter() + .find(|column| column.name == *pk_col) + .ok_or_else(|| { + DbError::sql(format!( + "table '{}' missing primary key column '{}'", + table.name, pk_col + )) + })?; + let json_value = primary_key.get(pk_col).ok_or_else(|| { + DbError::sql(format!( + "missing primary key column '{pk_col}' in record for table '{}'", + table.name + )) + })?; + params.push(json_to_column_value(&table.name, column, json_value)?); + } + let sql = format!( + "UPDATE {} SET {} WHERE {}", + sql_identifier(&table.name), + expressions.join(", "), + table + .primary_key_columns + .iter() + .enumerate() + .map(|(idx, pk_col)| format!( + "{} = ${}", + sql_identifier(pk_col), + table.columns.len() + idx + 1 + )) + .collect::>() + .join(" AND ") + ); + match self.execute_with_params(&sql, ¶ms) { + Ok(result) if result.affected_rows() == 0 => { + Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { + conflict_type: "missing_target".to_string(), + message: "update affected no rows".to_string(), + local_row_json, + resolution: None, + resolved_at_micros: None, + resolved_by: None, + resolution_note: None, + policy_name: Some(policy.as_str().to_string()), + })) + } + Ok(_) => Ok(SyncImportRecordOutcome::Applied), + Err(DbError::Constraint { message }) if remote_wins => { + apply_remote_replace(SyncOperation::Update)?; + Ok(SyncImportRecordOutcome::Resolved(SyncConflictRecordData { + conflict_type: "update_update".to_string(), + message, + local_row_json, + resolution: Some("remote_applied".to_string()), + resolved_at_micros: Some(current_time_micros()), + resolved_by: Some("sync_policy".to_string()), + resolution_note: None, + policy_name: Some(policy.as_str().to_string()), + })) + } + Err(DbError::Constraint { message }) => { + Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { + conflict_type: if local_row_json.is_some() { + "update_update".to_string() + } else { + "constraint_error".to_string() + }, + message, + local_row_json, + resolution: None, + resolved_at_micros: None, + resolved_by: None, + resolution_note: None, + policy_name: Some(policy.as_str().to_string()), + })) + } + Err(error) => Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { + conflict_type: "apply_error".to_string(), + message: error.to_string(), + local_row_json, + resolution: None, + resolved_at_micros: None, + resolved_by: None, + resolution_note: None, + policy_name: Some(policy.as_str().to_string()), + })), + } + } + SyncOperation::Delete => { + let mut where_values = Vec::with_capacity(table.primary_key_columns.len()); + let mut where_parts = Vec::with_capacity(table.primary_key_columns.len()); + for pk_col in &table.primary_key_columns { + let column = table + .columns + .iter() + .find(|column| column.name == *pk_col) + .ok_or_else(|| { + DbError::sql(format!( + "table '{}' missing primary key column '{}'", + table.name, pk_col + )) + })?; + let json_value = primary_key.get(pk_col).ok_or_else(|| { + DbError::sql(format!( + "missing primary key column '{pk_col}' in record for table '{}'", + table.name + )) + })?; + where_values.push(json_to_column_value(&table.name, column, json_value)?); + where_parts.push(format!( + "{} = ${}", + sql_identifier(pk_col), + where_values.len() + )); + } + let sql = format!( + "DELETE FROM {} WHERE {}", + sql_identifier(&table.name), + where_parts.join(" AND ") + ); + match self.execute_with_params(&sql, &where_values) { + Ok(result) if result.affected_rows() == 0 => { + Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { + conflict_type: "missing_target".to_string(), + message: "delete affected no rows".to_string(), + local_row_json, + resolution: None, + resolved_at_micros: None, + resolved_by: None, + resolution_note: None, + policy_name: Some(policy.as_str().to_string()), + })) + } + Ok(_) => Ok(SyncImportRecordOutcome::Applied), + Err(DbError::Constraint { message }) if remote_wins => { + apply_remote_replace(SyncOperation::Delete)?; + Ok(SyncImportRecordOutcome::Resolved(SyncConflictRecordData { + conflict_type: "delete_update".to_string(), + message, + local_row_json, + resolution: Some("remote_applied".to_string()), + resolved_at_micros: Some(current_time_micros()), + resolved_by: Some("sync_policy".to_string()), + resolution_note: None, + policy_name: Some(policy.as_str().to_string()), + })) + } + Err(DbError::Constraint { message }) => { + Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { + conflict_type: if local_row_json.is_some() { + "delete_update".to_string() + } else { + "constraint_error".to_string() + }, + message, + local_row_json, + resolution: None, + resolved_at_micros: None, + resolved_by: None, + resolution_note: None, + policy_name: Some(policy.as_str().to_string()), + })) + } + Err(error) => Ok(SyncImportRecordOutcome::Conflict(SyncConflictRecordData { + conflict_type: "apply_error".to_string(), + message: error.to_string(), + local_row_json, + resolution: None, + resolved_at_micros: None, + resolved_by: None, + resolution_note: None, + policy_name: Some(policy.as_str().to_string()), + })), + } + } + } + } + fn sync_update_conflict_resolution( + &self, + conflict_id: i64, + resolution: Option<&str>, + resolved_by: Option<&str>, + note: Option<&str>, + resolved_at_micros: Option, + ) -> Result { + self.ensure_sync_tables()?; + let Some(_) = self.sync_conflict(conflict_id)? else { + return Ok(false); + }; + let sql = format!( + "UPDATE {table} SET resolved = {resolved}, resolution = {resolution}, resolved_at_micros = {resolved_at_micros}, resolved_by = {resolved_by}, resolution_note = {resolution_note} WHERE conflict_id = {conflict_id}", + table = crate::sync::CONFLICTS_TABLE, + resolved = if resolution.is_some() { 1 } else { 0 }, + resolution = resolution + .map(sql_text_literal) + .unwrap_or_else(|| "NULL".to_string()), + resolved_at_micros = resolved_at_micros + .map(|value| value.to_string()) + .unwrap_or_else(|| "NULL".to_string()), + resolved_by = resolved_by + .map(sql_text_literal) + .unwrap_or_else(|| "NULL".to_string()), + resolution_note = note + .map(sql_text_literal) + .unwrap_or_else(|| "NULL".to_string()), + conflict_id = conflict_id, + ); + let _ = self.execute(&sql)?; + Ok(true) + } + fn sync_update_session( + &self, + session_id: i64, + summary: &SyncRunSummary, + status: &str, + error: Option<&str>, + ended_at_micros: i64, + ) -> Result<()> { + self.ensure_sync_tables()?; + let ( + pushed_seen, + pushed_applied, + pushed_skipped, + pushed_conflicted, + pulled_seen, + pulled_applied, + pulled_skipped, + pulled_conflicted, + ) = sync_session_summary_counts(summary); + let sql = format!( + "UPDATE {table} SET remote_replica_id = {remote_replica_id}, ended_at_micros = {ended_at_micros}, status = {status}, error = {error}, pushed_batch_id = {pushed_batch_id}, pulled_batch_id = {pulled_batch_id}, pushed_seen = {pushed_seen}, pushed_applied = {pushed_applied}, pushed_skipped = {pushed_skipped}, pushed_conflicted = {pushed_conflicted}, pulled_seen = {pulled_seen}, pulled_applied = {pulled_applied}, pulled_skipped = {pulled_skipped}, pulled_conflicted = {pulled_conflicted}, retry_count = {retry_count} WHERE session_id = {session_id}", + table = crate::sync::SESSIONS_TABLE, + remote_replica_id = sql_nullable_text_literal(summary.remote_replica_id.as_deref()), + ended_at_micros = ended_at_micros, + status = sql_text_literal(status), + error = sql_nullable_text_literal(error), + pushed_batch_id = sql_nullable_text_literal(summary.pushed_batch_id.as_deref()), + pulled_batch_id = sql_nullable_text_literal(summary.pulled_batch_id.as_deref()), + pushed_seen = pushed_seen, + pushed_applied = pushed_applied, + pushed_skipped = pushed_skipped, + pushed_conflicted = pushed_conflicted, + pulled_seen = pulled_seen, + pulled_applied = pulled_applied, + pulled_skipped = pulled_skipped, + pulled_conflicted = pulled_conflicted, + retry_count = summary.retry_count as i64, + session_id = session_id, + ); + let _ = self.execute(&sql)?; + Ok(()) + } + pub(crate) fn sync_table_columns(&self, table_name: &str) -> Result> { + let sql = format!("PRAGMA table_info({})", sql_identifier(table_name)); + match self.execute(&sql) { + Ok(result) => Ok(result + .rows() + .iter() + .filter_map(|row| match row.values().get(1) { + Some(Value::Text(value)) => Some(value.clone()), + _ => None, + }) + .collect()), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(Vec::new()) + } else { + Err(error) + } + } + } + } + pub(crate) fn sync_status_query_result(&self) -> Result { + let status = self.sync_status()?; + Ok(QueryResult::with_rows( + vec![ + "enabled".to_string(), + "replica_id".to_string(), + "next_sequence".to_string(), + "journal_path".to_string(), + "journal_size_bytes".to_string(), + ], + vec![QueryRow::new(vec![ + Value::Bool(status.enabled), + status.replica_id.map_or(Value::Null, Value::Text), + sync_u64_to_i64(status.next_sequence, "next_sequence")?, + status.journal_path.map_or(Value::Null, Value::Text), + sync_u64_to_i64(status.journal_size_bytes, "journal_size_bytes")?, + ])], + )) + } + pub(crate) fn sync_journal_query_result(&self, since_sequence: u64) -> Result { + let records = self.sync_pending_changes(since_sequence, usize::MAX)?; + let rows = records + .into_iter() + .map(|record| { + Ok(QueryRow::new(vec![ + sync_u64_to_i64(record.sequence, "sequence")?, + Value::Text(record.replica_id), + sync_u64_to_i64(record.transaction_lsn, "transaction_lsn")?, + Value::Text(record.table), + Value::Text(record.operation), + Value::Text(record.primary_key.to_string()), + record + .after + .map_or(Value::Null, |value| Value::Text(value.to_string())), + Value::Int64(i64::from(record.schema_cookie)), + Value::Int64(record.committed_at_micros), + ])) + }) + .collect::>>()?; + Ok(QueryResult::with_rows( + vec![ + "sequence".to_string(), + "replica_id".to_string(), + "transaction_lsn".to_string(), + "table_name".to_string(), + "operation".to_string(), + "primary_key_json".to_string(), + "after_json".to_string(), + "schema_cookie".to_string(), + "committed_at_micros".to_string(), + ], + rows, + )) + } + pub(crate) fn sync_peers_query_result(&self) -> Result { + let peers = self.sync_peers()?; + let rows = peers + .into_iter() + .map(|peer| { + Ok(QueryRow::new(vec![ + Value::Text(peer.name), + Value::Text(peer.endpoint), + peer.token_env.map_or(Value::Null, Value::Text), + Value::Int64(peer.created_at_micros), + Value::Int64(peer.updated_at_micros), + ])) + }) + .collect::>>()?; + Ok(QueryResult::with_rows( + vec![ + "name".to_string(), + "endpoint".to_string(), + "token_env".to_string(), + "created_at_micros".to_string(), + "updated_at_micros".to_string(), + ], + rows, + )) + } + pub(crate) fn sync_retention_query_result(&self) -> Result { + let retention = self.sync_retention_report()?; + let first_sequence = match retention.first_sequence { + Some(value) => sync_u64_to_i64(value, "first_sequence")?, + None => Value::Null, + }; + let last_sequence = match retention.last_sequence { + Some(value) => sync_u64_to_i64(value, "last_sequence")?, + None => Value::Null, + }; + let safe_prune_through = match retention.safe_prune_through { + Some(value) => sync_u64_to_i64(value, "safe_prune_through")?, + None => Value::Null, + }; + + Ok(QueryResult::with_rows( + vec![ + "journal_records".to_string(), + "first_sequence".to_string(), + "last_sequence".to_string(), + "safe_prune_through".to_string(), + "prunable_records".to_string(), + "blocked_by_json".to_string(), + "journal_size_bytes".to_string(), + ], + vec![QueryRow::new(vec![ + sync_u64_to_i64(retention.journal_records as u64, "journal_records")?, + first_sequence, + last_sequence, + safe_prune_through, + sync_u64_to_i64(retention.prunable_records as u64, "prunable_records")?, + Value::Text( + serde_json::to_string(&retention.blocked_by).map_err(|error| { + DbError::internal(format!( + "failed to encode sync retention blocked_by: {error}" + )) + })?, + ), + sync_u64_to_i64(retention.journal_size_bytes, "journal_size_bytes")?, + ])], + )) + } + pub(crate) fn sync_peer_lag_query_result(&self) -> Result { + let peer_lag = self.sync_peer_lag_report()?; + let rows = peer_lag + .into_iter() + .map(|lag| { + let in_watermark = match lag.in_watermark { + Some(value) => sync_u64_to_i64(value, "in_watermark")?, + None => Value::Null, + }; + let out_watermark = match lag.out_watermark { + Some(value) => sync_u64_to_i64(value, "out_watermark")?, + None => Value::Null, + }; + let local_high_watermark = match lag.local_high_watermark { + Some(value) => sync_u64_to_i64(value, "local_high_watermark")?, + None => Value::Null, + }; + let in_lag = match lag.in_lag { + Some(value) => sync_u64_to_i64(value, "in_lag")?, + None => Value::Null, + }; + let out_lag = match lag.out_lag { + Some(value) => sync_u64_to_i64(value, "out_lag")?, + None => Value::Null, + }; + Ok(QueryRow::new(vec![ + Value::Text(lag.peer_name), + lag.remote_replica_id.map_or(Value::Null, Value::Text), + in_watermark, + out_watermark, + local_high_watermark, + in_lag, + out_lag, + ])) + }) + .collect::>>()?; + Ok(QueryResult::with_rows( + vec![ + "peer_name".to_string(), + "remote_replica_id".to_string(), + "in_watermark".to_string(), + "out_watermark".to_string(), + "local_high_watermark".to_string(), + "in_lag".to_string(), + "out_lag".to_string(), + ], + rows, + )) + } + pub(crate) fn sync_doctor_query_result(&self) -> Result { + let report = self.sync_operational_doctor_report()?; + Ok(QueryResult::with_rows( + vec![ + "enabled".to_string(), + "replica_id".to_string(), + "highest_severity".to_string(), + "journal_records".to_string(), + "journal_size_bytes".to_string(), + "unresolved_conflicts".to_string(), + "guidance_json".to_string(), + ], + vec![QueryRow::new(vec![ + Value::Bool(report.status.enabled), + report.status.replica_id.map_or(Value::Null, Value::Text), + Value::Text(report.highest_severity.to_string()), + sync_u64_to_i64(report.integrity.total_records as u64, "journal_records")?, + sync_u64_to_i64(report.retention.journal_size_bytes, "journal_size_bytes")?, + sync_u64_to_i64(report.unresolved_conflicts as u64, "unresolved_conflicts")?, + Value::Text(serde_json::to_string(&report.guidance).map_err(|error| { + DbError::internal(format!("failed to encode sync doctor guidance: {error}")) + })?), + ])], + )) + } + pub(crate) fn sync_scopes_query_result(&self) -> Result { + let scopes = self.sync_scopes()?; + let rows = scopes + .into_iter() + .map(|scope| { + let SyncScope { + name, + include_tables, + row_filter, + filter_columns, + created_at_micros, + updated_at_micros, + } = scope; + Ok(QueryRow::new(vec![ + Value::Text(name), + Value::Text(serde_json::to_string(&include_tables).map_err(|error| { + DbError::internal(format!( + "failed to encode sync scope include tables: {error}" + )) + })?), + row_filter.map_or(Value::Null, Value::Text), + Value::Text(serde_json::to_string(&filter_columns).map_err(|error| { + DbError::internal(format!( + "failed to encode sync scope filter columns: {error}" + )) + })?), + Value::Int64(created_at_micros), + Value::Int64(updated_at_micros), + ])) + }) + .collect::>>()?; + Ok(QueryResult::with_rows( + vec![ + "name".to_string(), + "include_tables_json".to_string(), + "row_filter".to_string(), + "filter_columns_json".to_string(), + "created_at_micros".to_string(), + "updated_at_micros".to_string(), + ], + rows, + )) + } + pub(crate) fn sync_scope_tables_query_result(&self) -> Result { + let mut rows = Vec::new(); + for scope in self.sync_scopes()? { + let scope_name = scope.name; + for table_name in scope.include_tables { + rows.push(QueryRow::new(vec![ + Value::Text(scope_name.clone()), + Value::Text(table_name), + ])); + } + } + Ok(QueryResult::with_rows( + vec!["scope_name".to_string(), "table_name".to_string()], + rows, + )) + } + pub(crate) fn sync_peer_scopes_query_result(&self) -> Result { + let bindings = self.sync_peer_scope_bindings()?; + let rows = bindings + .into_iter() + .map(|binding| { + Ok(QueryRow::new(vec![ + Value::Text(binding.peer_name), + Value::Text(binding.scope_name), + Value::Int64(binding.created_at_micros), + Value::Int64(binding.updated_at_micros), + ])) + }) + .collect::>>()?; + Ok(QueryResult::with_rows( + vec![ + "peer_name".to_string(), + "scope_name".to_string(), + "created_at_micros".to_string(), + "updated_at_micros".to_string(), + ], + rows, + )) + } + pub(crate) fn sync_sessions_query_result(&self) -> Result { + let sessions = self.sync_sessions()?; + let rows = sessions + .into_iter() + .map(|session| { + Ok(QueryRow::new(vec![ + Value::Int64(session.session_id), + Value::Text(session.peer_name), + Value::Text(session.direction.to_string()), + session.remote_replica_id.map_or(Value::Null, Value::Text), + Value::Int64(session.started_at_micros), + session.ended_at_micros.map_or(Value::Null, Value::Int64), + Value::Text(session.status), + session.error.map_or(Value::Null, Value::Text), + session.pushed_batch_id.map_or(Value::Null, Value::Text), + session.pulled_batch_id.map_or(Value::Null, Value::Text), + Value::Int64(session.pushed_seen), + Value::Int64(session.pushed_applied), + Value::Int64(session.pushed_skipped), + Value::Int64(session.pushed_conflicted), + Value::Int64(session.pulled_seen), + Value::Int64(session.pulled_applied), + Value::Int64(session.pulled_skipped), + Value::Int64(session.pulled_conflicted), + Value::Int64(session.retry_count), + ])) + }) + .collect::>>()?; + Ok(QueryResult::with_rows( + vec![ + "session_id".to_string(), + "peer_name".to_string(), + "direction".to_string(), + "remote_replica_id".to_string(), + "started_at_micros".to_string(), + "ended_at_micros".to_string(), + "status".to_string(), + "error".to_string(), + "pushed_batch_id".to_string(), + "pulled_batch_id".to_string(), + "pushed_seen".to_string(), + "pushed_applied".to_string(), + "pushed_skipped".to_string(), + "pushed_conflicted".to_string(), + "pulled_seen".to_string(), + "pulled_applied".to_string(), + "pulled_skipped".to_string(), + "pulled_conflicted".to_string(), + "retry_count".to_string(), + ], + rows, + )) + } + pub(crate) fn sync_conflict_policy_query_result(&self) -> Result { + let policy = self.sync_conflict_policy()?; + Ok(QueryResult::with_rows( + vec![ + "default_policy".to_string(), + "origin_priority_json".to_string(), + ], + vec![QueryRow::new(vec![ + Value::Text(policy.default_policy.to_string()), + Value::Text( + serde_json::to_string(&policy.origin_priority).map_err(|error| { + DbError::internal(format!( + "failed to encode sync conflict policy origin priority: {error}" + )) + })?, + ), + ])], + )) + } + pub(crate) fn sync_conflicts_query_result(&self) -> Result { + let sql = format!( + "SELECT * FROM {} WHERE resolved = 0 ORDER BY conflict_id", + crate::sync::CONFLICTS_TABLE + ); + match self.execute(&sql) { + Ok(result) => Ok(result), + Err(error) => { + let message = error.to_string(); + if message.contains("no such table") || message.contains("unknown table") { + Ok(QueryResult::with_rows( + vec![ + "conflict_id".to_string(), + "batch_id".to_string(), + "remote_replica_id".to_string(), + "remote_sequence".to_string(), + "table_name".to_string(), + "operation".to_string(), + "conflict_type".to_string(), + "message".to_string(), + "primary_key_json".to_string(), + "remote_record_json".to_string(), + "local_row_json".to_string(), + "created_at_micros".to_string(), + "resolved".to_string(), + "resolution".to_string(), + "resolved_at_micros".to_string(), + "resolved_by".to_string(), + "resolution_note".to_string(), + "policy_name".to_string(), + "local_record_json".to_string(), + ], + Vec::new(), + )) + } else { + Err(error) + } + } + } + } + pub(crate) fn sync_relay_status_query_result(&self) -> Result { + let status = self.sync_relay_status(None, false, false, false, None)?; + Ok(QueryResult::with_rows( + vec![ + "relay_id".to_string(), + "protocol_version".to_string(), + "database_replica_id".to_string(), + "production_mode".to_string(), + "secure_transport_required".to_string(), + "insecure_override_enabled".to_string(), + "active_sessions".to_string(), + "active_streams".to_string(), + "started_at_micros".to_string(), + ], + vec![QueryRow::new(vec![ + Value::Text(status.relay_id), + Value::Int64(i64::from(status.protocol_version)), + status + .database_replica_id + .map(Value::Text) + .unwrap_or(Value::Null), + Value::Bool(status.production_mode), + Value::Bool(status.secure_transport_required), + Value::Bool(status.insecure_override_enabled), + sync_u64_to_i64(status.active_sessions, "active_sessions")?, + sync_u64_to_i64(status.active_streams, "active_streams")?, + Value::Int64(status.started_at_micros), + ])], + )) + } + pub(crate) fn sync_relay_sessions_query_result(&self) -> Result { + self.query_table_or_empty( + crate::sync::RELAY_SESSIONS_TABLE, + &[ + "session_id", + "tenant_id", + "subject_id", + "subject_kind", + "request_id", + "operation", + "scope_name", + "shape_id", + "started_at_micros", + "ended_at_micros", + "status", + "error", + "rows_seen", + "bytes_seen", + ], + "started_at_micros, session_id", + ) + } + pub(crate) fn sync_shapes_query_result(&self) -> Result { + self.query_table_or_empty( + crate::sync::SHAPES_TABLE, + &[ + "shape_id", + "name", + "scope_name", + "tenant_id", + "allowed_roles_json", + "allowed_subjects_json", + "created_at_micros", + "updated_at_micros", + "retention_ttl_micros", + "max_records", + "ack_deadline_micros", + "heartbeat_micros", + ], + "shape_id", + ) + } + pub(crate) fn sync_shape_clients_query_result(&self) -> Result { + self.query_table_or_empty( + crate::sync::SHAPE_CLIENTS_TABLE, + &[ + "shape_id", + "tenant_id", + "client_replica_id", + "subject_id", + "session_id", + "last_ack_sequence", + "last_ack_watermark", + "last_changeset_id", + "last_seen_at_micros", + "retention_blocking", + "status", + ], + "shape_id, client_replica_id", + ) + } + pub(crate) fn sync_changeset_history_query_result(&self) -> Result { + self.query_table_or_empty( + crate::sync::CHANGESET_HISTORY_TABLE, + &[ + "changeset_id", + "source_replica_id", + "source_kind", + "scope_name", + "shape_id", + "record_count", + "bytes", + "created_at_micros", + "applied_at_micros", + "outcome", + "integrity_hash", + ], + "created_at_micros, changeset_id", + ) + } + pub(crate) fn sync_post_commit( + &self, + runtime: &mut EngineRuntime, + committed_lsn: u64, + ) -> Result<()> { + let mutations = runtime.take_sync_mutations(); + if mutations.is_empty() { + return Ok(()); + } + if !self.inner.sync_ctx.capture_enabled() { + return Ok(()); + } + let enabled = if self.inner.sync_ctx.is_enabled() { + true + } else { + let status = self.load_sync_status_from_runtime(runtime)?; + if status.enabled { + self.inner.sync_ctx.set_enabled(true); + if let Some(replica_id) = status.replica_id.as_deref() { + self.inner.sync_ctx.set_replica_id(replica_id); + } + self.inner.sync_ctx.set_next_sequence(status.next_sequence); + } + status.enabled + }; + if !enabled { + return Ok(()); + } + self.inner + .sync_ctx + .pending_mutations + .lock() + .map_err(|_| DbError::internal("sync pending mutations lock poisoned"))? + .extend(mutations); + self.inner + .sync_ctx + .flush_journal(&self.inner.vfs, committed_lsn)?; + Ok(()) + } +} diff --git a/crates/decentdb/src/exec/bench_queries.rs b/crates/decentdb/src/exec/bench_queries.rs new file mode 100644 index 00000000..0e7c6dcc --- /dev/null +++ b/crates/decentdb/src/exec/bench_queries.rs @@ -0,0 +1,6323 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +pub(crate) fn crm_column_index( + table: &TableSchema, + column: &str, + column_type: ColumnType, +) -> Option { + let index = schema_column_index(table, column)?; + if table.columns.get(index)?.column_type == column_type { + Some(index) + } else { + None + } +} + +pub(crate) fn crm_i64_cell( + value: Option<&Value>, + table: &str, + column: &str, +) -> Result> { + match value { + Some(Value::Int64(value)) => Ok(Some(*value)), + Some(Value::Null) => Ok(None), + Some(other) => Err(DbError::sql(format!( + "{table}.{column} expected INT64 but found {other:?}" + ))), + None => Err(DbError::internal(format!( + "{table}.{column} is missing from row" + ))), + } +} + +pub(crate) fn crm_text_cell( + value: Option<&Value>, + table: &str, + column: &str, +) -> Result> { + match value { + Some(Value::Text(value)) => Ok(Some(value.clone())), + Some(Value::Null) => Ok(None), + Some(other) => Err(DbError::sql(format!( + "{table}.{column} expected TEXT but found {other:?}" + ))), + None => Err(DbError::internal(format!( + "{table}.{column} is missing from row" + ))), + } +} + +pub(crate) fn crm_revenue_from_covering_dense( + covering: &RuntimeCoveringPayloads, + company_id_offset: usize, + total_offset: usize, + deleted: &BTreeSet, + company_names: &BTreeMap, +) -> Result>> { + let Some(max_company_id) = company_names.keys().copied().max() else { + return Ok(Some(BTreeMap::new())); + }; + if !(0..=1_000_000).contains(&max_company_id) { + return Ok(None); + } + + let len = usize::try_from(max_company_id) + .ok() + .and_then(|value| value.checked_add(1)) + .ok_or_else(|| DbError::constraint("company id exceeded addressable summary range"))?; + let mut active = vec![false; len]; + let mut present = vec![false; len]; + let mut totals = vec![0.0_f64; len]; + for company_id in company_names.keys().copied() { + let Ok(index) = usize::try_from(company_id) else { + return Ok(None); + }; + active[index] = true; + } + + if deleted.is_empty() { + for values in covering.rows.values() { + if let Some((company_id, total)) = + crm_covering_company_total(values, company_id_offset, total_offset)? + { + let Ok(index) = usize::try_from(company_id) else { + continue; + }; + if index < active.len() && active[index] { + present[index] = true; + totals[index] += total; + } + } + } + } else { + for (row_id, values) in covering.rows.iter() { + if deleted.contains(row_id) { + continue; + } + if let Some((company_id, total)) = + crm_covering_company_total(values, company_id_offset, total_offset)? + { + let Ok(index) = usize::try_from(company_id) else { + continue; + }; + if index < active.len() && active[index] { + present[index] = true; + totals[index] += total; + } + } + } + } + + let mut revenues = BTreeMap::new(); + for company_id in company_names.keys().copied() { + let index = usize::try_from(company_id) + .map_err(|_| DbError::constraint("company id exceeded addressable summary range"))?; + if present.get(index).copied().unwrap_or(false) { + revenues.insert(company_id, totals.get(index).copied().unwrap_or(0.0)); + } + } + Ok(Some(revenues)) +} + +pub(crate) fn crm_revenue_from_covering_sparse( + covering: &RuntimeCoveringPayloads, + company_id_offset: usize, + total_offset: usize, + deleted: &BTreeSet, + company_names: &BTreeMap, +) -> Result> { + let mut revenues = BTreeMap::new(); + for (row_id, values) in covering.rows.iter() { + if deleted.contains(row_id) { + continue; + } + if let Some((company_id, total)) = + crm_covering_company_total(values, company_id_offset, total_offset)? + { + if company_names.contains_key(&company_id) { + *revenues.entry(company_id).or_insert(0.0) += total; + } + } + } + Ok(revenues) +} + +pub(crate) fn crm_revenue_from_invoice_rows( + invoices_source: VisibleTableRowSource<'_>, + company_id_index: usize, + total_index: usize, + company_names: &BTreeMap, +) -> Result> { + let mut invoice_company_ids = BTreeMap::new(); + invoices_source.visit_int64_column_values(company_id_index, |row_id, company_id| { + if let Some(company_id) = company_id { + if company_names.contains_key(&company_id) { + invoice_company_ids.insert(row_id, company_id); + } + } + Ok(()) + })?; + let mut revenues = BTreeMap::new(); + invoices_source.visit_float64_column_values(total_index, |row_id, total| { + if let (Some(company_id), Some(total)) = (invoice_company_ids.get(&row_id), total) { + *revenues.entry(*company_id).or_insert(0.0_f64) += total; + } + Ok(()) + })?; + Ok(revenues) +} + +pub(crate) fn crm_covering_company_total( + values: &[Value], + company_id_offset: usize, + total_offset: usize, +) -> Result> { + let Some(company_id) = values.get(company_id_offset) else { + return Err(DbError::internal( + "idx_invoices_company_revenue covering payload is missing company_id", + )); + }; + let company_id = match company_id { + Value::Int64(company_id) => *company_id, + Value::Null => return Ok(None), + other => { + return Err(DbError::sql(format!( + "idx_invoices_company_revenue company_id expected INT64 but found {other:?}" + ))) + } + }; + let Some(total) = values.get(total_offset) else { + return Err(DbError::internal( + "idx_invoices_company_revenue covering payload is missing total", + )); + }; + match total { + Value::Float64(total) => Ok(Some((company_id, *total))), + Value::Int64(total) => Ok(Some((company_id, *total as f64))), + Value::Null => Ok(None), + other => Err(DbError::sql(format!( + "idx_invoices_company_revenue total expected FLOAT64 but found {other:?}" + ))), + } +} + +pub(crate) fn crm_table(item: &FromItem, table_name: &str, expected_alias: &str) -> bool { + match item { + FromItem::Table { name, alias } => { + identifiers_equal(name, table_name) + && alias + .as_deref() + .is_none_or(|candidate| identifiers_equal(candidate, expected_alias)) + } + _ => false, + } +} + +pub(crate) fn crm_join_on_columns( + constraint: &JoinConstraint, + left_tables: &[&str], + left_column: &str, + right_tables: &[&str], + right_column: &str, +) -> bool { + let JoinConstraint::On(Expr::Binary { + left, + op: BinaryOp::Eq, + right, + }) = constraint + else { + return false; + }; + + (crm_column(left, left_tables, left_column) && crm_column(right, right_tables, right_column)) + || (crm_column(left, right_tables, right_column) + && crm_column(right, left_tables, left_column)) +} + +pub(crate) fn crm_count_distinct_users(expr: &Expr) -> bool { + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return false; + }; + + identifiers_equal(name, "count") + && *distinct + && !*star + && order_by.is_empty() + && !*within_group + && args.len() == 1 + && crm_column(&args[0], &["u", "users"], "id") +} + +pub(crate) fn crm_coalesced_invoice_total_sum(expr: &Expr) -> bool { + let Expr::Function { name, args } = expr else { + return false; + }; + + identifiers_equal(name, "coalesce") + && args.len() == 2 + && crm_invoice_total_sum(&args[0]) + && crm_zero_literal(&args[1]) +} + +pub(crate) fn crm_invoice_total_sum(expr: &Expr) -> bool { + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return false; + }; + + identifiers_equal(name, "sum") + && !*distinct + && !*star + && order_by.is_empty() + && !*within_group + && args.len() == 1 + && crm_column(&args[0], &["i", "invoices"], "total") +} + +pub(crate) fn crm_zero_literal(expr: &Expr) -> bool { + match expr { + Expr::Literal(Value::Int64(value)) => *value == 0, + Expr::Literal(Value::Float64(value)) => *value == 0.0, + Expr::Literal(Value::Decimal { scaled, .. }) => *scaled == 0, + _ => false, + } +} + +pub(crate) fn crm_column(expr: &Expr, tables: &[&str], column: &str) -> bool { + match expr { + Expr::Column { + table, + column: candidate, + } => { + identifiers_equal(candidate, column) + && table.as_deref().is_some_and(|candidate_table| { + tables + .iter() + .any(|table| identifiers_equal(candidate_table, table)) + }) + } + _ => false, + } +} + +pub(crate) fn showdown_window_projection_column_names(select: &Select) -> Result> { + let mut column_names = Vec::with_capacity(select.projection.len()); + for (index, item) in select.projection.iter().enumerate() { + let SelectItem::Expr { expr, alias } = item else { + return Err(DbError::internal( + "showdown window fast path expected expression projection", + )); + }; + column_names.push( + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + ); + } + Ok(column_names) +} + +pub(crate) fn showdown_window_projection_column_matches( + item: &SelectItem, + table_name: &str, + binding_name: &str, + column_name: &str, +) -> bool { + matches!( + item, + SelectItem::Expr { expr, .. } + if showdown_column_expr_matches(expr, table_name, binding_name, column_name) + ) +} + +pub(crate) fn showdown_column_expr_matches( + expr: &Expr, + table_name: &str, + binding_name: &str, + column_name: &str, +) -> bool { + let Expr::Column { table, column } = expr else { + return false; + }; + if !identifiers_equal(column, column_name) { + return false; + } + match table.as_deref() { + Some(qualifier) => { + identifiers_equal(qualifier, table_name) || identifiers_equal(qualifier, binding_name) + } + None => true, + } +} + +pub(crate) fn showdown_window_column_order_matches( + order_by: &crate::sql::ast::OrderBy, + table_name: &str, + binding_name: &str, + column_name: &str, + descending: bool, +) -> bool { + order_by.descending == descending + && order_by.collation.is_none() + && showdown_column_expr_matches(&order_by.expr, table_name, binding_name, column_name) +} + +pub(crate) fn showdown_window_alias_order_matches( + order_by: &crate::sql::ast::OrderBy, + alias: &str, + descending: bool, +) -> bool { + if order_by.descending != descending || order_by.collation.is_some() { + return false; + } + matches!( + &order_by.expr, + Expr::Column { table: None, column } if identifiers_equal(column, alias) + ) +} + +pub(crate) fn showdown_window_partition_order_matches( + partition_by: &[Expr], + order_by: &[crate::sql::ast::OrderBy], + table_name: &str, + binding_name: &str, + partition_column: &str, + order_column: &str, + order_descending: bool, +) -> bool { + partition_by.len() == 1 + && showdown_column_expr_matches( + &partition_by[0], + table_name, + binding_name, + partition_column, + ) + && order_by.len() == 1 + && showdown_window_column_order_matches( + &order_by[0], + table_name, + binding_name, + order_column, + order_descending, + ) +} + +pub(crate) fn showdown_rank_window_projection_matches( + item: &SelectItem, + table_name: &str, + binding_name: &str, + function_name: &str, + alias_name: &str, +) -> bool { + let SelectItem::Expr { + expr: + Expr::WindowFunction { + name, + args, + partition_by, + order_by, + frame, + distinct, + star, + }, + alias, + } = item + else { + return false; + }; + alias + .as_deref() + .is_some_and(|alias| identifiers_equal(alias, alias_name)) + && name.eq_ignore_ascii_case(function_name) + && args.is_empty() + && !*distinct + && !*star + && frame.is_none() + && showdown_window_partition_order_matches( + partition_by, + order_by, + table_name, + binding_name, + "movie_id", + "score", + true, + ) +} + +pub(crate) fn showdown_row_number_projection_matches( + item: &SelectItem, + table_name: &str, + binding_name: &str, + partition_column: &str, + order_column: &str, + alias_name: &str, +) -> bool { + let SelectItem::Expr { + expr: + Expr::RowNumber { + partition_by, + order_by, + frame, + }, + alias, + } = item + else { + return false; + }; + alias + .as_deref() + .is_some_and(|alias| identifiers_equal(alias, alias_name)) + && frame.is_none() + && showdown_window_partition_order_matches( + partition_by, + order_by, + table_name, + binding_name, + partition_column, + order_column, + false, + ) +} + +pub(crate) fn showdown_lag_projection_matches( + item: &SelectItem, + table_name: &str, + binding_name: &str, + partition_column: &str, + order_column: &str, + alias_name: &str, +) -> bool { + let SelectItem::Expr { + expr: + Expr::WindowFunction { + name, + args, + partition_by, + order_by, + frame, + distinct, + star, + }, + alias, + } = item + else { + return false; + }; + alias + .as_deref() + .is_some_and(|alias| identifiers_equal(alias, alias_name)) + && name.eq_ignore_ascii_case("lag") + && args.len() == 1 + && showdown_column_expr_matches(&args[0], table_name, binding_name, order_column) + && !*distinct + && !*star + && frame.is_none() + && showdown_window_partition_order_matches( + partition_by, + order_by, + table_name, + binding_name, + partition_column, + order_column, + false, + ) +} + +pub(crate) fn showdown_avg_window_projection_matches( + item: &SelectItem, + table_name: &str, + binding_name: &str, + order_column: &str, + value_column: &str, + alias_name: &str, +) -> bool { + let SelectItem::Expr { + expr: + Expr::WindowFunction { + name, + args, + partition_by, + order_by, + frame, + distinct, + star, + }, + alias, + } = item + else { + return false; + }; + alias + .as_deref() + .is_some_and(|alias| identifiers_equal(alias, alias_name)) + && name.eq_ignore_ascii_case("avg") + && args.len() == 1 + && showdown_column_expr_matches(&args[0], table_name, binding_name, value_column) + && partition_by.is_empty() + && order_by.len() == 1 + && showdown_window_column_order_matches( + &order_by[0], + table_name, + binding_name, + order_column, + false, + ) + && !*distinct + && !*star + && rows_preceding_current_frame(frame.as_ref()) == Some(2) +} + +pub(crate) fn showdown_text_eq_filter_matches( + filter: Option<&Expr>, + table_name: &str, + binding_name: &str, + column_name: &str, + expected_text: &str, +) -> bool { + let Some(Expr::Binary { left, op, right }) = filter else { + return false; + }; + if *op != BinaryOp::Eq { + return false; + } + (showdown_column_expr_matches(left, table_name, binding_name, column_name) + && matches!(&**right, Expr::Literal(Value::Text(value)) if value == expected_text)) + || (showdown_column_expr_matches(right, table_name, binding_name, column_name) + && matches!(&**left, Expr::Literal(Value::Text(value)) if value == expected_text)) +} + +pub(crate) fn showdown_fast_int64_value( + values: &[Value], + index: usize, + context: &str, +) -> Result { + match values.get(index) { + Some(Value::Int64(value)) => Ok(*value), + Some(other) => Err(DbError::sql(format!( + "{context} expected INT64, got {other:?}" + ))), + None => Err(DbError::internal(format!( + "{context} column missing from row" + ))), + } +} + +pub(crate) fn movie_watchlist_review_avg( + review_source: &VisibleTableRowSource<'_>, + review_movie_keys: &RuntimeBtreeKeys, + movie_id: &Value, + review_score_index: usize, +) -> Result { + let (count, sum) = movie_review_score_stats( + review_source, + review_movie_keys, + movie_id, + review_score_index, + )?; + if count == 0 { + Ok(Value::Null) + } else { + Ok(Value::Float64(sum / count as f64)) + } +} + +pub(crate) fn movie_review_score_stats( + review_source: &VisibleTableRowSource<'_>, + review_movie_keys: &RuntimeBtreeKeys, + movie_id: &Value, + review_score_index: usize, +) -> Result<(i64, f64)> { + let mut sum = 0.0_f64; + let mut count = 0_i64; + let mut visit_review = |review_row: TableRowRef<'_>| -> Result<()> { + if let Some(score) = review_row + .values() + .get(review_score_index) + .and_then(indexed_join_aggregate_as_f64) + { + sum += score; + count = count.saturating_add(1); + } + Ok(()) + }; + + match review_movie_keys.row_ids_for_value_set(movie_id)? { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(row_id) => { + if let Some(review_row) = review_source.row_by_id(row_id)? { + visit_review(review_row)?; + } + } + RuntimeRowIdSet::Contiguous { start, len } => { + for row_id in contiguous_row_ids(start, len) { + if let Some(review_row) = review_source.row_by_id(row_id)? { + visit_review(review_row)?; + } + } + } + RuntimeRowIdSet::Many(row_ids) => { + for row_id in row_ids { + if let Some(review_row) = review_source.row_by_id(*row_id)? { + visit_review(review_row)?; + } + } + } + RuntimeRowIdSet::Owned(row_ids) => { + for row_id in row_ids { + if let Some(review_row) = review_source.row_by_id(row_id)? { + visit_review(review_row)?; + } + } + } + } + Ok((count, sum)) +} + +impl EngineRuntime { + pub(crate) fn try_execute_left_join_status_aggregate_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_left_join_status_aggregate_query(query, params)? else { + return Ok(None); + }; + let Some(parent_source) = self.visible_table_row_source(plan.parent_table_name) else { + return Ok(None); + }; + let Some(child_source) = self.visible_table_row_source(plan.child_table_name) else { + return Ok(None); + }; + + let bounded_order = plan + .order_by + .as_deref() + .zip(plan.limit) + .filter(|(_, _)| plan.offset == 0); + let child_index_keys = + plan.child_index_name + .as_deref() + .and_then(|index_name| match self.index(index_name) { + Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), + _ => None, + }); + + if let Some(keys) = child_index_keys { + let mut rows = Vec::new(); + for parent_row in parent_source.rows() { + let parent_row = parent_row?; + let parent_values = parent_row.values(); + let Some(join_value) = parent_values.get(plan.parent_join_index) else { + return Err(DbError::internal("parent join row is shorter than schema")); + }; + + let mut counts = LeftJoinStatusCounts::default(); + if !matches!(join_value, Value::Null) { + let child_row_ids = keys.row_ids_for_value_set(join_value)?; + match child_row_ids { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(child_row_id) => { + let Some(child_row) = child_source.row_by_id(child_row_id)? else { + return Err(DbError::internal( + "child index referenced missing row id", + )); + }; + add_status_aggregate_child_row(&mut counts, child_row.values(), &plan)?; + } + RuntimeRowIdSet::Contiguous { start, len } => { + for child_row_id in contiguous_row_ids(start, len) { + let Some(child_row) = child_source.row_by_id(child_row_id)? else { + return Err(DbError::internal( + "child index referenced missing row id", + )); + }; + add_status_aggregate_child_row( + &mut counts, + child_row.values(), + &plan, + )?; + } + } + RuntimeRowIdSet::Many(row_ids) => { + for child_row_id in row_ids { + let Some(child_row) = child_source.row_by_id(*child_row_id)? else { + return Err(DbError::internal( + "child index referenced missing row id", + )); + }; + add_status_aggregate_child_row( + &mut counts, + child_row.values(), + &plan, + )?; + } + } + RuntimeRowIdSet::Owned(row_ids) => { + for child_row_id in row_ids { + let Some(child_row) = child_source.row_by_id(child_row_id)? else { + return Err(DbError::internal( + "child index referenced missing row id", + )); + }; + add_status_aggregate_child_row( + &mut counts, + child_row.values(), + &plan, + )?; + } + } + } + } + + let mut output = Vec::with_capacity(plan.group_column_indexes.len() + 5); + for index in &plan.group_column_indexes { + output.push(parent_values[*index].clone()); + } + output.push(Value::Int64(counts.open_count)); + output.push(Value::Int64(counts.in_progress_count)); + output.push(Value::Int64(counts.resolved_count)); + output.push(Value::Int64(counts.closed_count)); + output.push(Value::Int64(counts.total_count)); + let row = QueryRow::new(output); + + if let Some((order_by, limit)) = bounded_order { + push_bounded_projection_ordered_query_row( + Some(self), + &mut rows, + row, + order_by, + limit, + )?; + } else { + rows.push(row); + } + } + + if let Some((order_by, _)) = bounded_order { + sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; + return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); + } + + return Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + plan.column_names, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + )?)); + } + + let mut counts_by_join_key = HashMap::, LeftJoinStatusCounts>::new(); + for child_row in child_source.rows() { + let child_row = child_row?; + let child_values = child_row.values(); + let Some(child_join_value) = child_values.get(plan.child_join_index) else { + return Err(DbError::internal("child join row is shorter than schema")); + }; + if matches!(child_join_value, Value::Null) { + continue; + } + let Some(child_status) = child_values.get(plan.child_status_index) else { + return Err(DbError::internal("child join row is shorter than schema")); + }; + let Some(child_id) = child_values.get(plan.child_id_index) else { + return Err(DbError::internal("child join row is shorter than schema")); + }; + counts_by_join_key + .entry(row_identity(std::slice::from_ref(child_join_value))?) + .or_default() + .add_child(child_status, child_id)?; + } + + let mut rows = Vec::new(); + for parent_row in parent_source.rows() { + let parent_row = parent_row?; + let parent_values = parent_row.values(); + let Some(join_value) = parent_values.get(plan.parent_join_index) else { + return Err(DbError::internal("parent join row is shorter than schema")); + }; + + let counts = if matches!(join_value, Value::Null) { + LeftJoinStatusCounts::default() + } else { + counts_by_join_key + .get(&row_identity(std::slice::from_ref(join_value))?) + .copied() + .unwrap_or_default() + }; + + let mut output = Vec::with_capacity(plan.group_column_indexes.len() + 5); + for index in &plan.group_column_indexes { + output.push(parent_values[*index].clone()); + } + output.push(Value::Int64(counts.open_count)); + output.push(Value::Int64(counts.in_progress_count)); + output.push(Value::Int64(counts.resolved_count)); + output.push(Value::Int64(counts.closed_count)); + output.push(Value::Int64(counts.total_count)); + let row = QueryRow::new(output); + + if let Some((order_by, limit)) = bounded_order { + push_bounded_projection_ordered_query_row( + Some(self), + &mut rows, + row, + order_by, + limit, + )?; + } else { + rows.push(row); + } + } + + if let Some((order_by, _)) = bounded_order { + sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; + return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); + } + + Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + plan.column_names, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + )?)) + } + pub(crate) fn try_execute_three_table_genre_popularity_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_three_table_genre_popularity_query(query, params)? else { + return Ok(None); + }; + let Some(genre_source) = self.visible_table_row_source(plan.genre_table_name) else { + return Ok(None); + }; + let Some(bridge_source) = self.visible_table_row_source(plan.bridge_table_name) else { + return Ok(None); + }; + let Some(movie_source) = self.visible_table_row_source(plan.movie_table_name) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { + keys: bridge_keys, .. + }) = self.index(&plan.bridge_genre_index_name) + else { + return Ok(None); + }; + let movie_index_keys = + plan.movie_index_name + .as_deref() + .and_then(|index_name| match self.index(index_name) { + Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), + _ => None, + }); + if !plan.movie_id_is_rowid_alias && movie_index_keys.is_none() { + return Ok(None); + } + + let bounded_order = plan + .order_by + .as_deref() + .zip(plan.limit) + .filter(|(_, _)| plan.offset == 0); + let mut rows = Vec::new(); + + for genre_row in genre_source.rows() { + let genre_row = genre_row?; + let genre_values = genre_row.values(); + let Some(genre_id) = genre_values.get(plan.genre_id_index) else { + return Err(DbError::internal("genre row is shorter than schema")); + }; + if matches!(genre_id, Value::Null) { + continue; + } + + let mut movie_count = 0_i64; + let mut rating_sum = 0.0_f64; + let mut rating_count = 0_i64; + + let bridge_row_ids = bridge_keys.row_ids_for_value_set(genre_id)?; + match bridge_row_ids { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(row_id) => { + let Some(bridge_row) = bridge_source.row_by_id(row_id)? else { + return Err(DbError::internal( + "genre bridge index referenced missing row id", + )); + }; + accumulate_genre_popularity_movie( + &movie_source, + movie_index_keys, + plan.movie_id_is_rowid_alias, + bridge_row.values().get(plan.bridge_movie_id_index), + plan.movie_rating_index, + &mut movie_count, + &mut rating_sum, + &mut rating_count, + )?; + } + RuntimeRowIdSet::Contiguous { start, len } => { + for row_id in contiguous_row_ids(start, len) { + let Some(bridge_row) = bridge_source.row_by_id(row_id)? else { + return Err(DbError::internal( + "genre bridge index referenced missing row id", + )); + }; + accumulate_genre_popularity_movie( + &movie_source, + movie_index_keys, + plan.movie_id_is_rowid_alias, + bridge_row.values().get(plan.bridge_movie_id_index), + plan.movie_rating_index, + &mut movie_count, + &mut rating_sum, + &mut rating_count, + )?; + } + } + RuntimeRowIdSet::Many(row_ids) => { + for row_id in row_ids { + let Some(bridge_row) = bridge_source.row_by_id(*row_id)? else { + return Err(DbError::internal( + "genre bridge index referenced missing row id", + )); + }; + accumulate_genre_popularity_movie( + &movie_source, + movie_index_keys, + plan.movie_id_is_rowid_alias, + bridge_row.values().get(plan.bridge_movie_id_index), + plan.movie_rating_index, + &mut movie_count, + &mut rating_sum, + &mut rating_count, + )?; + } + } + RuntimeRowIdSet::Owned(row_ids) => { + for row_id in row_ids { + let Some(bridge_row) = bridge_source.row_by_id(row_id)? else { + return Err(DbError::internal( + "genre bridge index referenced missing row id", + )); + }; + accumulate_genre_popularity_movie( + &movie_source, + movie_index_keys, + plan.movie_id_is_rowid_alias, + bridge_row.values().get(plan.bridge_movie_id_index), + plan.movie_rating_index, + &mut movie_count, + &mut rating_sum, + &mut rating_count, + )?; + } + } + } + + if movie_count == 0 { + continue; + } + let avg_rating = if rating_count == 0 { + Value::Null + } else { + Value::Float64(rating_sum / rating_count as f64) + }; + let Some(name) = genre_values.get(plan.genre_name_index) else { + return Err(DbError::internal("genre name row is shorter than schema")); + }; + let row = QueryRow::new(vec![name.clone(), Value::Int64(movie_count), avg_rating]); + if let Some((order_by, limit)) = bounded_order { + push_bounded_projection_ordered_query_row( + Some(self), + &mut rows, + row, + order_by, + limit, + )?; + } else { + rows.push(row); + } + } + + if let Some((order_by, _)) = bounded_order { + sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; + return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); + } + + Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + plan.column_names, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + )?)) + } + pub(crate) fn try_execute_movie_tag_search_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_movie_tag_search_query(query, params)? else { + return Ok(None); + }; + let Some(tag_source) = self.visible_table_row_source(plan.tag_table_name) else { + return Ok(None); + }; + let Some(bridge_source) = self.visible_table_row_source(plan.bridge_table_name) else { + return Ok(None); + }; + let Some(movie_source) = self.visible_table_row_source(plan.movie_table_name) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { + keys: tag_name_keys, + .. + }) = self.index(&plan.tag_name_index_name) + else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { + keys: bridge_tag_keys, + .. + }) = self.index(&plan.bridge_tag_index_name) + else { + return Ok(None); + }; + let movie_index_keys = + plan.movie_index_name + .as_deref() + .and_then(|index_name| match self.index(index_name) { + Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), + _ => None, + }); + if !plan.movie_id_is_rowid_alias && movie_index_keys.is_none() { + return Ok(None); + } + + if plan.limit == Some(0) { + return Ok(Some(QueryResult::with_rows(plan.column_names, Vec::new()))); + } + let bounded_order = plan + .order_by + .as_deref() + .zip(plan.limit) + .filter(|(_, _)| plan.offset == 0); + let mut rows = plan.limit.map_or_else(Vec::new, Vec::with_capacity); + + let mut visit_tag_row = |tag_row: TableRowRef<'_>| -> Result<()> { + let Some(tag_id) = tag_row.values().get(plan.tag_id_index) else { + return Err(DbError::internal("movie tag search tag id column missing")); + }; + if matches!(tag_id, Value::Null) { + return Ok(()); + } + + match bridge_tag_keys.row_ids_for_value_set(tag_id)? { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(row_id) => { + let Some(bridge_row) = bridge_source.row_by_id(row_id)? else { + return Err(DbError::internal( + "movie tag bridge index referenced missing row id", + )); + }; + push_movie_tag_search_movie_rows( + self, + &movie_source, + movie_index_keys, + plan.movie_id_is_rowid_alias, + bridge_row.values().get(plan.bridge_movie_id_index), + &plan.projection_indexes, + bounded_order, + &mut rows, + )?; + } + RuntimeRowIdSet::Contiguous { start, len } => { + for row_id in contiguous_row_ids(start, len) { + let Some(bridge_row) = bridge_source.row_by_id(row_id)? else { + return Err(DbError::internal( + "movie tag bridge index referenced missing row id", + )); + }; + push_movie_tag_search_movie_rows( + self, + &movie_source, + movie_index_keys, + plan.movie_id_is_rowid_alias, + bridge_row.values().get(plan.bridge_movie_id_index), + &plan.projection_indexes, + bounded_order, + &mut rows, + )?; + } + } + RuntimeRowIdSet::Many(row_ids) => { + for row_id in row_ids { + let Some(bridge_row) = bridge_source.row_by_id(*row_id)? else { + return Err(DbError::internal( + "movie tag bridge index referenced missing row id", + )); + }; + push_movie_tag_search_movie_rows( + self, + &movie_source, + movie_index_keys, + plan.movie_id_is_rowid_alias, + bridge_row.values().get(plan.bridge_movie_id_index), + &plan.projection_indexes, + bounded_order, + &mut rows, + )?; + } + } + RuntimeRowIdSet::Owned(row_ids) => { + for row_id in row_ids { + let Some(bridge_row) = bridge_source.row_by_id(row_id)? else { + return Err(DbError::internal( + "movie tag bridge index referenced missing row id", + )); + }; + push_movie_tag_search_movie_rows( + self, + &movie_source, + movie_index_keys, + plan.movie_id_is_rowid_alias, + bridge_row.values().get(plan.bridge_movie_id_index), + &plan.projection_indexes, + bounded_order, + &mut rows, + )?; + } + } + } + Ok(()) + }; + + match tag_name_keys.row_ids_for_value_set(&plan.tag_name_value)? { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(row_id) => { + if let Some(tag_row) = tag_source.row_by_id(row_id)? { + visit_tag_row(tag_row)?; + } + } + RuntimeRowIdSet::Contiguous { start, len } => { + for row_id in contiguous_row_ids(start, len) { + if let Some(tag_row) = tag_source.row_by_id(row_id)? { + visit_tag_row(tag_row)?; + } + } + } + RuntimeRowIdSet::Many(row_ids) => { + for row_id in row_ids { + if let Some(tag_row) = tag_source.row_by_id(*row_id)? { + visit_tag_row(tag_row)?; + } + } + } + RuntimeRowIdSet::Owned(row_ids) => { + for row_id in row_ids { + if let Some(tag_row) = tag_source.row_by_id(row_id)? { + visit_tag_row(tag_row)?; + } + } + } + } + + if let Some((order_by, _)) = bounded_order { + sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; + return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); + } + + Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + plan.column_names, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + )?)) + } + fn analyze_movie_tag_search_query<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result>> { + if !query.ctes.is_empty() || query.recursive { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || select.having.is_some() + || !select.group_by.is_empty() + || select.from.len() != 1 + { + return Ok(None); + } + + let mut tables = Vec::new(); + let mut constraints = Vec::new(); + if !flatten_inner_join_chain(&select.from[0], &mut tables, &mut constraints) + || tables.len() != 3 + { + return Ok(None); + } + let tag_binding = tables + .iter() + .copied() + .find(|binding| identifiers_equal(binding.name, "tags")); + let bridge_binding = tables + .iter() + .copied() + .find(|binding| identifiers_equal(binding.name, "movietags")); + let movie_binding = tables + .iter() + .copied() + .find(|binding| identifiers_equal(binding.name, "movies")); + let (Some(tag_binding), Some(bridge_binding), Some(movie_binding)) = + (tag_binding, bridge_binding, movie_binding) + else { + return Ok(None); + }; + + if [tag_binding.name, bridge_binding.name, movie_binding.name] + .iter() + .any(|table| { + self.visible_view(table, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(table) + }) + { + return Ok(None); + } + let Some(tag_schema) = self.table_schema(tag_binding.name) else { + return Ok(None); + }; + let Some(bridge_schema) = self.table_schema(bridge_binding.name) else { + return Ok(None); + }; + let Some(movie_schema) = self.table_schema(movie_binding.name) else { + return Ok(None); + }; + if !generated_columns_are_stored(tag_schema) + || !generated_columns_are_stored(bridge_schema) + || !generated_columns_are_stored(movie_schema) + { + return Ok(None); + } + + let Some(filter) = select.filter.as_ref() else { + return Ok(None); + }; + let Some((filter_table, filter_column, tag_name_expr)) = simple_btree_lookup(filter) else { + return Ok(None); + }; + if !matches_table_binding(tag_binding, filter_table) + || !identifiers_equal(filter_column, "name") + { + return Ok(None); + } + let tag_name_value = self.eval_expr( + tag_name_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?; + + if !join_constraints_match_columns(&constraints, tag_binding, "id", bridge_binding, "tagid") + || !join_constraints_match_columns( + &constraints, + movie_binding, + "id", + bridge_binding, + "movieid", + ) + { + return Ok(None); + } + + let tag_id_index = schema_column_index(tag_schema, "id") + .ok_or_else(|| DbError::internal("movie tag search id column missing from tags"))?; + let bridge_movie_id_index = + schema_column_index(bridge_schema, "movieid").ok_or_else(|| { + DbError::internal("movie tag search movie id column missing from MovieTags") + })?; + + let Some(tag_name_index_name) = self + .single_column_btree_index(tag_binding.name, "name") + .map(|index| index.name.clone()) + else { + return Ok(None); + }; + let Some(bridge_tag_index_name) = self + .single_column_btree_index(bridge_binding.name, "tagid") + .map(|index| index.name.clone()) + else { + return Ok(None); + }; + let movie_index_name = self + .single_column_btree_index(movie_binding.name, "id") + .map(|index| index.name.clone()); + let movie_id_is_rowid_alias = row_id_alias_column_name(movie_schema) + .is_some_and(|column| identifiers_equal(column, "id")); + if !movie_id_is_rowid_alias && movie_index_name.is_none() { + return Ok(None); + } + + let Some((projection_indexes, column_names)) = self.simple_projection_plan( + select, + movie_binding.name, + movie_binding.alias, + movie_schema, + ) else { + return Ok(None); + }; + let order_by = self.simple_projection_order_by_plan( + query, + movie_schema, + movie_binding.name, + movie_binding.binding_name(), + &projection_indexes, + )?; + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + + Ok(Some(MovieTagSearchPlan { + tag_table_name: tag_binding.name, + tag_id_index, + tag_name_index_name, + tag_name_value, + bridge_table_name: bridge_binding.name, + bridge_movie_id_index, + bridge_tag_index_name, + movie_table_name: movie_binding.name, + movie_index_name, + movie_id_is_rowid_alias, + projection_indexes, + column_names, + order_by, + limit, + offset, + })) + } + pub(crate) fn try_execute_movie_watchlist_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_movie_watchlist_query(query, params)? else { + return Ok(None); + }; + let Some(watchlist_source) = self.visible_table_row_source(plan.watchlist_table_name) + else { + return Ok(None); + }; + let Some(movie_source) = self.visible_table_row_source(plan.movie_table_name) else { + return Ok(None); + }; + let Some(review_source) = self.visible_table_row_source(plan.review_table_name) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { + keys: watchlist_user_keys, + .. + }) = self.index(&plan.watchlist_user_index_name) + else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { + keys: review_movie_keys, + .. + }) = self.index(&plan.review_movie_index_name) + else { + return Ok(None); + }; + let movie_index_keys = + plan.movie_index_name + .as_deref() + .and_then(|index_name| match self.index(index_name) { + Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), + _ => None, + }); + if !plan.movie_id_is_rowid_alias && movie_index_keys.is_none() { + return Ok(None); + } + + if plan.limit == Some(0) { + return Ok(Some(QueryResult::with_rows(plan.column_names, Vec::new()))); + } + let mut groups = BTreeMap::, QueryRow>::new(); + + let mut visit_watchlist_row = |watchlist_row: TableRowRef<'_>| -> Result<()> { + let watchlist_values = watchlist_row.values(); + let Some(movie_id) = watchlist_values.get(plan.watchlist_movie_id_index) else { + return Err(DbError::internal( + "movie watchlist movie id column missing from Watchlist", + )); + }; + if matches!(movie_id, Value::Null) { + return Ok(()); + } + let Some(priority) = watchlist_values.get(plan.watchlist_priority_index) else { + return Err(DbError::internal( + "movie watchlist priority column missing from Watchlist", + )); + }; + insert_movie_watchlist_group_rows( + &movie_source, + movie_index_keys, + plan.movie_id_is_rowid_alias, + movie_id, + priority, + &review_source, + review_movie_keys, + plan.movie_id_index, + plan.movie_title_index, + plan.review_score_index, + &mut groups, + ) + }; + + match watchlist_user_keys.row_ids_for_value_set(&plan.user_handle_value)? { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(row_id) => { + if let Some(watchlist_row) = watchlist_source.row_by_id(row_id)? { + visit_watchlist_row(watchlist_row)?; + } + } + RuntimeRowIdSet::Contiguous { start, len } => { + for row_id in contiguous_row_ids(start, len) { + if let Some(watchlist_row) = watchlist_source.row_by_id(row_id)? { + visit_watchlist_row(watchlist_row)?; + } + } + } + RuntimeRowIdSet::Many(row_ids) => { + for row_id in row_ids { + if let Some(watchlist_row) = watchlist_source.row_by_id(*row_id)? { + visit_watchlist_row(watchlist_row)?; + } + } + } + RuntimeRowIdSet::Owned(row_ids) => { + for row_id in row_ids { + if let Some(watchlist_row) = watchlist_source.row_by_id(row_id)? { + visit_watchlist_row(watchlist_row)?; + } + } + } + } + + let rows = groups.into_values().collect::>(); + Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + plan.column_names, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + )?)) + } + fn analyze_movie_watchlist_query<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result>> { + if !query.ctes.is_empty() || query.recursive { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || select.having.is_some() + || select.group_by.len() != 1 + || select.projection.len() != 4 + || select.from.len() != 1 + { + return Ok(None); + } + + let FromItem::Join { + left, + right, + kind: JoinKind::Left, + constraint: JoinConstraint::On(review_join), + } = &select.from[0] + else { + return Ok(None); + }; + let FromItem::Join { + left: watchlist_item, + right: movie_item, + kind: JoinKind::Inner, + constraint: JoinConstraint::On(movie_join), + } = &**left + else { + return Ok(None); + }; + let FromItem::Table { + name: watchlist_name, + alias: watchlist_alias, + } = &**watchlist_item + else { + return Ok(None); + }; + let FromItem::Table { + name: movie_name, + alias: movie_alias, + } = &**movie_item + else { + return Ok(None); + }; + let FromItem::Table { + name: review_name, + alias: review_alias, + } = &**right + else { + return Ok(None); + }; + if !identifiers_equal(watchlist_name, "watchlist") + || !identifiers_equal(movie_name, "movies") + || !identifiers_equal(review_name, "reviews") + { + return Ok(None); + } + + if [ + watchlist_name.as_str(), + movie_name.as_str(), + review_name.as_str(), + ] + .iter() + .any(|table| { + self.visible_view(table, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(table) + }) { + return Ok(None); + } + let Some(watchlist_schema) = self.table_schema(watchlist_name) else { + return Ok(None); + }; + let Some(movie_schema) = self.table_schema(movie_name) else { + return Ok(None); + }; + let Some(review_schema) = self.table_schema(review_name) else { + return Ok(None); + }; + if !generated_columns_are_stored(watchlist_schema) + || !generated_columns_are_stored(movie_schema) + || !generated_columns_are_stored(review_schema) + { + return Ok(None); + } + + let watchlist_binding = TableBindingRef { + name: watchlist_name, + alias: watchlist_alias, + }; + let movie_binding = TableBindingRef { + name: movie_name, + alias: movie_alias, + }; + let review_binding = TableBindingRef { + name: review_name, + alias: review_alias, + }; + + if !join_constraint_matches_columns( + movie_join, + movie_binding, + "id", + watchlist_binding, + "movieid", + ) || !join_constraint_matches_columns( + review_join, + review_binding, + "movieid", + movie_binding, + "id", + ) { + return Ok(None); + } + let Some(filter) = select.filter.as_ref() else { + return Ok(None); + }; + let Some((filter_table, filter_column, user_handle_expr)) = simple_btree_lookup(filter) + else { + return Ok(None); + }; + if !matches_table_binding(watchlist_binding, filter_table) + || !identifiers_equal(filter_column, "userhandle") + { + return Ok(None); + } + let user_handle_value = self.eval_expr( + user_handle_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?; + + if !projection_expr_matches_binding_column(&select.projection[0], movie_binding, "id") + || !projection_expr_matches_binding_column( + &select.projection[1], + movie_binding, + "title", + ) + || !projection_expr_matches_binding_column( + &select.projection[2], + watchlist_binding, + "priority", + ) + || !matches!( + &select.projection[3], + SelectItem::Expr { expr, .. } + if aggregate_matches_single_binding_column(expr, "avg", review_binding, "score") + ) + || !expr_matches_binding_column(&select.group_by[0], movie_binding, "id") + { + return Ok(None); + } + + let watchlist_movie_id_index = schema_column_index(watchlist_schema, "movieid") + .ok_or_else(|| { + DbError::internal("movie watchlist movie id column missing from Watchlist") + })?; + let watchlist_priority_index = schema_column_index(watchlist_schema, "priority") + .ok_or_else(|| { + DbError::internal("movie watchlist priority column missing from Watchlist") + })?; + let movie_id_index = schema_column_index(movie_schema, "id") + .ok_or_else(|| DbError::internal("movie watchlist id column missing from Movies"))?; + let movie_title_index = schema_column_index(movie_schema, "title") + .ok_or_else(|| DbError::internal("movie watchlist title column missing from Movies"))?; + let review_score_index = schema_column_index(review_schema, "score").ok_or_else(|| { + DbError::internal("movie watchlist score column missing from Reviews") + })?; + + let Some(watchlist_user_index_name) = self + .single_column_btree_index(watchlist_name, "userhandle") + .map(|index| index.name.clone()) + else { + return Ok(None); + }; + let Some(review_movie_index_name) = self + .single_column_btree_index(review_name, "movieid") + .map(|index| index.name.clone()) + else { + return Ok(None); + }; + let movie_index_name = self + .single_column_btree_index(movie_name, "id") + .map(|index| index.name.clone()); + let movie_id_is_rowid_alias = row_id_alias_column_name(movie_schema) + .is_some_and(|column| identifiers_equal(column, "id")); + if !movie_id_is_rowid_alias && movie_index_name.is_none() { + return Ok(None); + } + + let column_names = select + .projection + .iter() + .enumerate() + .map(|(index, item)| match item { + SelectItem::Expr { expr, alias } => alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => { + format!("col{}", index + 1) + } + }) + .collect::>(); + let order_by = projection_order_by_plan(&query.order_by, &select.projection); + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + + Ok(Some(MovieWatchlistPlan { + watchlist_table_name: watchlist_name, + watchlist_movie_id_index, + watchlist_priority_index, + watchlist_user_index_name, + user_handle_value, + movie_table_name: movie_name, + movie_id_index, + movie_title_index, + movie_index_name, + movie_id_is_rowid_alias, + review_table_name: review_name, + review_score_index, + review_movie_index_name, + column_names, + order_by, + limit, + offset, + })) + } + pub(crate) fn try_execute_movie_top_rated_by_year_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_movie_top_rated_by_year_query(query, params)? else { + return Ok(None); + }; + let Some(movie_source) = self.visible_table_row_source(plan.movie_table_name) else { + return Ok(None); + }; + let Some(review_source) = self.visible_table_row_source(plan.review_table_name) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { + keys: review_movie_keys, + .. + }) = self.index(&plan.review_movie_index_name) + else { + return Ok(None); + }; + let movie_release_year_keys = + plan.movie_release_year_index_name + .as_deref() + .and_then(|index_name| match self.index(index_name) { + Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), + _ => None, + }); + + if plan.limit == Some(0) { + return Ok(Some(QueryResult::with_rows(plan.column_names, Vec::new()))); + } + let bounded_order = plan + .order_by + .as_deref() + .zip(plan.limit) + .filter(|(_, _)| plan.offset == 0); + let mut rows = Vec::new(); + + let mut visit_movie_row = |movie_row: TableRowRef<'_>| -> Result<()> { + let movie_values = movie_row.values(); + let Some(movie_id) = movie_values.get(plan.movie_id_index) else { + return Err(DbError::internal( + "movie top-rated id column missing from Movies", + )); + }; + let (review_count, score_sum) = movie_review_score_stats( + &review_source, + review_movie_keys, + movie_id, + plan.review_score_index, + )?; + if review_count < plan.min_review_count { + return Ok(()); + } + let avg_score = if review_count == 0 { + Value::Null + } else { + Value::Float64(score_sum / review_count as f64) + }; + let projected = + project_simple_projection_values(movie_values, &plan.movie_projection_indexes); + let mut values = projected.values().to_vec(); + values.push(avg_score); + values.push(Value::Int64(review_count)); + let row = QueryRow::new(values); + if let Some((order_by, limit)) = bounded_order { + push_bounded_projection_ordered_query_row( + Some(self), + &mut rows, + row, + order_by, + limit, + )?; + } else { + rows.push(row); + } + Ok(()) + }; + + if let Some(keys) = movie_release_year_keys { + match keys.row_ids_for_value_set(&plan.release_year_value)? { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(row_id) => { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + visit_movie_row(movie_row)?; + } + } + RuntimeRowIdSet::Contiguous { start, len } => { + for row_id in contiguous_row_ids(start, len) { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + visit_movie_row(movie_row)?; + } + } + } + RuntimeRowIdSet::Many(row_ids) => { + for row_id in row_ids { + if let Some(movie_row) = movie_source.row_by_id(*row_id)? { + visit_movie_row(movie_row)?; + } + } + } + RuntimeRowIdSet::Owned(row_ids) => { + for row_id in row_ids { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + visit_movie_row(movie_row)?; + } + } + } + } + } else { + for movie_row in movie_source.rows() { + let movie_row = movie_row?; + let Some(release_year) = movie_row.values().get(plan.movie_release_year_index) + else { + return Err(DbError::internal( + "movie top-rated release year column missing from Movies", + )); + }; + if compare_values(release_year, &plan.release_year_value)? + != std::cmp::Ordering::Equal + { + continue; + } + visit_movie_row(movie_row)?; + } + } + + if let Some((order_by, _)) = bounded_order { + sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; + return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); + } + + Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + plan.column_names, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + )?)) + } + fn analyze_movie_top_rated_by_year_query<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result>> { + if !query.ctes.is_empty() || query.recursive { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || select.group_by.len() != 1 + || select.projection.len() != 11 + || select.from.len() != 1 + { + return Ok(None); + } + + let FromItem::Join { + left, + right, + kind: JoinKind::Inner, + constraint: JoinConstraint::On(join_on), + } = &select.from[0] + else { + return Ok(None); + }; + let ( + FromItem::Table { + name: movie_name, + alias: movie_alias, + }, + FromItem::Table { + name: review_name, + alias: review_alias, + }, + ) = (&**left, &**right) + else { + return Ok(None); + }; + if !identifiers_equal(movie_name, "movies") || !identifiers_equal(review_name, "reviews") { + return Ok(None); + } + if [movie_name.as_str(), review_name.as_str()] + .iter() + .any(|table| { + self.visible_view(table, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(table) + }) + { + return Ok(None); + } + let Some(movie_schema) = self.table_schema(movie_name) else { + return Ok(None); + }; + let Some(review_schema) = self.table_schema(review_name) else { + return Ok(None); + }; + if !generated_columns_are_stored(movie_schema) + || !generated_columns_are_stored(review_schema) + { + return Ok(None); + } + + let movie_binding = TableBindingRef { + name: movie_name, + alias: movie_alias, + }; + let review_binding = TableBindingRef { + name: review_name, + alias: review_alias, + }; + if !join_constraint_matches_columns(join_on, review_binding, "movieid", movie_binding, "id") + || !expr_matches_binding_column(&select.group_by[0], movie_binding, "id") + { + return Ok(None); + } + + let Some(filter) = select.filter.as_ref() else { + return Ok(None); + }; + let Some((filter_table, filter_column, release_year_expr)) = simple_btree_lookup(filter) + else { + return Ok(None); + }; + if !matches_table_binding(movie_binding, filter_table) + || !identifiers_equal(filter_column, "releaseyear") + { + return Ok(None); + } + let release_year_value = self.eval_expr( + release_year_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?; + + let min_review_count = match select.having.as_ref() { + Some(Expr::Binary { + left, + op: BinaryOp::GtEq, + right, + }) if aggregate_matches_single_binding_column(left, "count", review_binding, "id") => { + self.eval_constant_i64(right, params, &BTreeMap::new())? + } + _ => return Ok(None), + }; + + let movie_columns = [ + "id", + "title", + "releaseyear", + "synopsis", + "budgetusd", + "boxofficeusd", + "mpaarating", + "runtimeminutes", + "addedat", + ]; + let mut movie_projection_indexes = Vec::with_capacity(movie_columns.len()); + let mut column_names = Vec::with_capacity(select.projection.len()); + for (index, column) in movie_columns.iter().enumerate() { + if !projection_expr_matches_binding_column( + &select.projection[index], + movie_binding, + column, + ) { + return Ok(None); + } + let column_index = schema_column_index(movie_schema, column).ok_or_else(|| { + DbError::internal(format!( + "movie top-rated column {column} missing from Movies" + )) + })?; + movie_projection_indexes.push(column_index); + if let SelectItem::Expr { expr, alias } = &select.projection[index] { + column_names.push( + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + ); + } + } + let SelectItem::Expr { + expr: avg_expr, + alias: avg_alias, + } = &select.projection[9] + else { + return Ok(None); + }; + let SelectItem::Expr { + expr: count_expr, + alias: count_alias, + } = &select.projection[10] + else { + return Ok(None); + }; + if !aggregate_matches_single_binding_column(avg_expr, "avg", review_binding, "score") + || !aggregate_matches_single_binding_column(count_expr, "count", review_binding, "id") + { + return Ok(None); + } + column_names.push( + avg_alias + .clone() + .unwrap_or_else(|| infer_expr_name(avg_expr, 10)), + ); + column_names.push( + count_alias + .clone() + .unwrap_or_else(|| infer_expr_name(count_expr, 11)), + ); + + let movie_id_index = schema_column_index(movie_schema, "id") + .ok_or_else(|| DbError::internal("movie top-rated id column missing from Movies"))?; + let movie_release_year_index = schema_column_index(movie_schema, "releaseyear") + .ok_or_else(|| { + DbError::internal("movie top-rated ReleaseYear column missing from Movies") + })?; + let review_score_index = schema_column_index(review_schema, "score").ok_or_else(|| { + DbError::internal("movie top-rated Score column missing from Reviews") + })?; + let Some(review_movie_index_name) = self + .single_column_btree_index(review_name, "movieid") + .map(|index| index.name.clone()) + else { + return Ok(None); + }; + let movie_release_year_index_name = self + .single_column_btree_index(movie_name, "releaseyear") + .map(|index| index.name.clone()); + + let order_by = projection_order_by_plan(&query.order_by, &select.projection); + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + + Ok(Some(MovieTopRatedByYearPlan { + movie_table_name: movie_name, + movie_id_index, + movie_release_year_index, + movie_release_year_index_name, + movie_projection_indexes, + release_year_value, + review_table_name: review_name, + review_score_index, + review_movie_index_name, + min_review_count, + column_names, + order_by, + limit, + offset, + })) + } + pub(crate) fn try_execute_movie_busiest_people_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_movie_busiest_people_query(query, params)? else { + return Ok(None); + }; + let Some(people_source) = self.visible_table_row_source(plan.people_table_name) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { + keys: roles_person_keys, + .. + }) = self.index(&plan.roles_person_index_name) + else { + return Ok(None); + }; + let people_index_keys = plan + .people_index_name + .as_deref() + .and_then(|index_name| match self.index(index_name) { + Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), + _ => None, + }); + if !plan.people_id_is_rowid_alias && people_index_keys.is_none() { + return Ok(None); + } + + if plan.limit == Some(0) { + return Ok(Some(QueryResult::with_rows(plan.column_names, Vec::new()))); + } + + let bounded_count = plan.limit.map(|limit| limit.saturating_add(plan.offset)); + let mut counts = Vec::new(); + for (person_key, role_count) in roles_person_keys.distinct_key_counts() { + if role_count == 0 { + continue; + } + let role_count = i64::try_from(role_count).map_err(|_| { + DbError::sql("role count for person exceeds INT64 limits".to_string()) + })?; + let candidate = MovieBusiestPeopleCount { + person_key, + role_count, + }; + if let Some(bounded_count) = bounded_count { + push_bounded_movie_busiest_people_count(&mut counts, candidate, bounded_count); + } else { + counts.push(candidate); + } + } + sort_movie_busiest_people_counts(&mut counts); + + let take = plan.limit.unwrap_or(usize::MAX); + let mut rows = Vec::with_capacity(take.min(counts.len())); + for candidate in counts.into_iter().skip(plan.offset).take(take) { + push_movie_busiest_people_row( + &people_source, + people_index_keys, + plan.people_id_is_rowid_alias, + &candidate, + &plan.people_projection_indexes, + &mut rows, + )?; + } + + Ok(Some(QueryResult::with_rows(plan.column_names, rows))) + } + fn analyze_movie_busiest_people_query<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result>> { + if !query.ctes.is_empty() || query.recursive { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || select.filter.is_some() + || select.having.is_some() + || select.group_by.len() != 1 + || select.projection.len() != 5 + || select.from.len() != 1 + { + return Ok(None); + } + + let FromItem::Join { + left, + right, + kind: JoinKind::Inner, + constraint: JoinConstraint::On(join_on), + } = &select.from[0] + else { + return Ok(None); + }; + let ( + FromItem::Table { + name: left_name, + alias: left_alias, + }, + FromItem::Table { + name: right_name, + alias: right_alias, + }, + ) = (&**left, &**right) + else { + return Ok(None); + }; + + let left_binding = TableBindingRef { + name: left_name, + alias: left_alias, + }; + let right_binding = TableBindingRef { + name: right_name, + alias: right_alias, + }; + let (people_binding, roles_binding) = if identifiers_equal(left_name, "people") + && identifiers_equal(right_name, "roles") + { + (left_binding, right_binding) + } else if identifiers_equal(left_name, "roles") && identifiers_equal(right_name, "people") { + (right_binding, left_binding) + } else { + return Ok(None); + }; + let people_name = people_binding.name; + let roles_name = roles_binding.name; + + if [people_name, roles_name].iter().any(|table| { + self.visible_view(table, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(table) + }) { + return Ok(None); + } + let Some(people_schema) = self.table_schema(people_name) else { + return Ok(None); + }; + let Some(roles_schema) = self.table_schema(roles_name) else { + return Ok(None); + }; + if !generated_columns_are_stored(people_schema) + || !generated_columns_are_stored(roles_schema) + { + return Ok(None); + } + + if !join_constraint_matches_columns( + join_on, + roles_binding, + "personid", + people_binding, + "id", + ) || !expr_matches_binding_column_or_unqualified( + &select.group_by[0], + people_binding, + "id", + ) { + return Ok(None); + } + + let people_columns = ["id", "fullname", "birthdate", "biography"]; + let mut people_projection_indexes = Vec::with_capacity(people_columns.len()); + let mut column_names = Vec::with_capacity(select.projection.len()); + for (index, column) in people_columns.iter().enumerate() { + if !projection_expr_matches_binding_column( + &select.projection[index], + people_binding, + column, + ) { + return Ok(None); + } + let column_index = schema_column_index(people_schema, column).ok_or_else(|| { + DbError::internal(format!( + "movie busiest people column {column} missing from People" + )) + })?; + people_projection_indexes.push(column_index); + if let SelectItem::Expr { expr, alias } = &select.projection[index] { + column_names.push( + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + ); + } + } + + let SelectItem::Expr { + expr: count_expr, + alias: count_alias, + } = &select.projection[4] + else { + return Ok(None); + }; + if !aggregate_matches_single_binding_column(count_expr, "count", roles_binding, "id") { + return Ok(None); + } + column_names.push( + count_alias + .clone() + .unwrap_or_else(|| infer_expr_name(count_expr, 5)), + ); + + let roles_id_index = schema_column_index(roles_schema, "id").ok_or_else(|| { + DbError::internal("movie busiest people id column missing from Roles") + })?; + if schema_column_index(people_schema, "id").is_none() { + return Err(DbError::internal( + "movie busiest people id column missing from People", + )); + } + let roles_person_id_index = + schema_column_index(roles_schema, "personid").ok_or_else(|| { + DbError::internal("movie busiest people PersonId column missing from Roles") + })?; + if roles_schema.columns[roles_id_index].nullable + && !roles_schema.columns[roles_id_index].primary_key + { + return Ok(None); + } + if roles_schema.columns[roles_person_id_index].nullable + || !table_has_single_column_foreign_key(roles_schema, "personid", people_schema, "id") + { + return Ok(None); + } + + let Some(roles_person_index_name) = self + .single_column_btree_index(roles_name, "personid") + .map(|index| index.name.clone()) + else { + return Ok(None); + }; + let people_index_name = self + .single_column_btree_index(people_name, "id") + .map(|index| index.name.clone()); + let people_id_is_rowid_alias = row_id_alias_column_name(people_schema) + .is_some_and(|column| identifiers_equal(column, "id")); + if !people_id_is_rowid_alias && people_index_name.is_none() { + return Ok(None); + } + + let Some(order_by) = projection_order_by_plan(&query.order_by, &select.projection) else { + return Ok(None); + }; + if order_by.len() != 1 + || order_by[0].projection_index != 4 + || !order_by[0].descending + || order_by[0].collation.is_some() + { + return Ok(None); + } + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + + Ok(Some(MovieBusiestPeoplePlan { + people_table_name: people_name, + people_projection_indexes, + people_index_name, + people_id_is_rowid_alias, + roles_person_index_name, + column_names, + limit, + offset, + })) + } + pub(crate) fn try_execute_showdown_window_query( + &self, + query: &Query, + ) -> Result> { + if query.recursive + || !query.ctes.is_empty() + || query.limit.is_some() + || query.offset.is_some() + { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || !select.group_by.is_empty() + || select.having.is_some() + || select.from.len() != 1 + { + return Ok(None); + } + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if !identifiers_equal(name, "reviews") + && !identifiers_equal(name, "roles") + && !identifiers_equal(name, "movies") + { + return Ok(None); + } + if self.security_rules_active()? { + return Ok(None); + } + let binding_name = alias.as_deref().unwrap_or(name.as_str()); + if identifiers_equal(name, "reviews") { + return self.try_execute_showdown_review_ranking_query( + select, + &query.order_by, + name, + binding_name, + ); + } + if identifiers_equal(name, "roles") { + return self.try_execute_showdown_cast_billing_query( + select, + &query.order_by, + name, + binding_name, + ); + } + if identifiers_equal(name, "movies") { + return self.try_execute_showdown_rolling_avg_query( + select, + &query.order_by, + name, + binding_name, + ); + } + Ok(None) + } + fn try_execute_showdown_review_ranking_query( + &self, + select: &Select, + query_order_by: &[crate::sql::ast::OrderBy], + table_name: &str, + binding_name: &str, + ) -> Result> { + if select.filter.is_some() + || query_order_by.len() != 2 + || !showdown_window_column_order_matches( + &query_order_by[0], + table_name, + binding_name, + "movie_id", + false, + ) + || !showdown_window_alias_order_matches(&query_order_by[1], "rk", false) + { + return Ok(None); + } + let Some(schema) = self.table_schema(table_name) else { + return Ok(None); + }; + let Some(source) = self.visible_table_row_source(table_name) else { + return Ok(None); + }; + let Some(movie_id_index) = schema_column_index(schema, "movie_id") else { + return Ok(None); + }; + let Some(score_index) = schema_column_index(schema, "score") else { + return Ok(None); + }; + let Some(author_index) = schema_column_index(schema, "author") else { + return Ok(None); + }; + if select.projection.len() != 5 + || !showdown_window_projection_column_matches( + &select.projection[0], + table_name, + binding_name, + "movie_id", + ) + || !showdown_window_projection_column_matches( + &select.projection[1], + table_name, + binding_name, + "score", + ) + || !showdown_window_projection_column_matches( + &select.projection[2], + table_name, + binding_name, + "author", + ) + || !showdown_rank_window_projection_matches( + &select.projection[3], + table_name, + binding_name, + "rank", + "rk", + ) + || !showdown_rank_window_projection_matches( + &select.projection[4], + table_name, + binding_name, + "dense_rank", + "drk", + ) + { + return Ok(None); + } + + let mut ordered = Vec::with_capacity(source.row_count()); + let mut already_grouped = true; + let mut previous_scanned_movie_id = None; + for row in source.rows() { + let row = row?; + let values = row.values(); + let movie_id = showdown_fast_int64_value(values, movie_id_index, "reviews.movie_id")?; + if previous_scanned_movie_id.is_some_and(|previous| previous > movie_id) { + already_grouped = false; + } + previous_scanned_movie_id = Some(movie_id); + let score = showdown_fast_int64_value(values, score_index, "reviews.score")?; + let author = values + .get(author_index) + .cloned() + .ok_or_else(|| DbError::internal("showdown review author column missing"))?; + ordered.push(ReviewRankingFastRow { + row_id: row.row_id(), + movie_id, + score, + author, + }); + } + + if already_grouped { + let mut rows = Vec::with_capacity(source.row_count()); + let mut current_movie_id = None; + let mut group = Vec::::new(); + for item in ordered { + if current_movie_id.is_some_and(|current| current != item.movie_id) { + append_showdown_review_ranking_group(&mut group, &mut rows); + } + current_movie_id = Some(item.movie_id); + group.push(item); + } + append_showdown_review_ranking_group(&mut group, &mut rows); + return Ok(Some(QueryResult::with_rows( + showdown_window_projection_column_names(select)?, + rows, + ))); + } + + ordered.sort_unstable_by(|left, right| { + left.movie_id + .cmp(&right.movie_id) + .then_with(|| right.score.cmp(&left.score)) + .then_with(|| left.row_id.cmp(&right.row_id)) + }); + + let mut rows = Vec::with_capacity(ordered.len()); + let mut previous_movie_id: Option = None; + let mut previous_score: Option = None; + let mut partition_ordinal = 0_usize; + let mut current_rank = 1_i64; + let mut current_dense_rank = 1_i64; + for item in ordered { + if previous_movie_id != Some(item.movie_id) { + previous_movie_id = Some(item.movie_id); + partition_ordinal = 0; + current_rank = 1; + current_dense_rank = 1; + } else { + partition_ordinal += 1; + if previous_score.is_some_and(|score| score != item.score) { + current_rank = (partition_ordinal + 1) as i64; + current_dense_rank += 1; + } + } + previous_score = Some(item.score); + rows.push(QueryRow::new(vec![ + Value::Int64(item.movie_id), + Value::Int64(item.score), + item.author, + Value::Int64(current_rank), + Value::Int64(current_dense_rank), + ])); + } + Ok(Some(QueryResult::with_rows( + showdown_window_projection_column_names(select)?, + rows, + ))) + } + fn try_execute_showdown_cast_billing_query( + &self, + select: &Select, + query_order_by: &[crate::sql::ast::OrderBy], + table_name: &str, + binding_name: &str, + ) -> Result> { + if query_order_by.len() != 2 + || !showdown_window_column_order_matches( + &query_order_by[0], + table_name, + binding_name, + "movie_id", + false, + ) + || !showdown_window_alias_order_matches(&query_order_by[1], "rn", false) + || !showdown_text_eq_filter_matches( + select.filter.as_ref(), + table_name, + binding_name, + "department", + "Acting", + ) + { + return Ok(None); + } + let Some(schema) = self.table_schema(table_name) else { + return Ok(None); + }; + let Some(source) = self.visible_table_row_source(table_name) else { + return Ok(None); + }; + let Some(movie_id_index) = schema_column_index(schema, "movie_id") else { + return Ok(None); + }; + let Some(person_id_index) = schema_column_index(schema, "person_id") else { + return Ok(None); + }; + let Some(department_index) = schema_column_index(schema, "department") else { + return Ok(None); + }; + let Some(billing_order_index) = schema_column_index(schema, "billing_order") else { + return Ok(None); + }; + if select.projection.len() != 5 + || !showdown_window_projection_column_matches( + &select.projection[0], + table_name, + binding_name, + "movie_id", + ) + || !showdown_window_projection_column_matches( + &select.projection[1], + table_name, + binding_name, + "person_id", + ) + || !showdown_window_projection_column_matches( + &select.projection[2], + table_name, + binding_name, + "billing_order", + ) + || !showdown_row_number_projection_matches( + &select.projection[3], + table_name, + binding_name, + "movie_id", + "billing_order", + "rn", + ) + || !showdown_lag_projection_matches( + &select.projection[4], + table_name, + binding_name, + "movie_id", + "billing_order", + "prev", + ) + { + return Ok(None); + } + + let mut ordered = Vec::new(); + for row in source.rows() { + let row = row?; + let values = row.values(); + if !matches!( + values.get(department_index), + Some(Value::Text(department)) if department == "Acting" + ) { + continue; + } + let movie_id = showdown_fast_int64_value(values, movie_id_index, "roles.movie_id")?; + let billing_order = + showdown_fast_int64_value(values, billing_order_index, "roles.billing_order")?; + let person_id = values.get(person_id_index).cloned().ok_or_else(|| { + DbError::internal("showdown role person_id column missing from row") + })?; + let billing_value = values.get(billing_order_index).cloned().ok_or_else(|| { + DbError::internal("showdown role billing_order column missing from row") + })?; + ordered.push(CastBillingFastRow { + row_id: row.row_id(), + movie_id, + person_id, + billing_order, + billing_value, + }); + } + ordered.sort_by(|left, right| { + left.movie_id + .cmp(&right.movie_id) + .then_with(|| left.billing_order.cmp(&right.billing_order)) + .then_with(|| left.row_id.cmp(&right.row_id)) + }); + + let mut rows = Vec::with_capacity(ordered.len()); + let mut previous_movie_id: Option = None; + let mut previous_billing = Value::Null; + let mut partition_ordinal = 0_usize; + for item in ordered { + let prev = if previous_movie_id == Some(item.movie_id) { + partition_ordinal += 1; + previous_billing.clone() + } else { + previous_movie_id = Some(item.movie_id); + partition_ordinal = 0; + Value::Null + }; + previous_billing = item.billing_value.clone(); + rows.push(QueryRow::new(vec![ + Value::Int64(item.movie_id), + item.person_id, + item.billing_value, + Value::Int64((partition_ordinal + 1) as i64), + prev, + ])); + } + Ok(Some(QueryResult::with_rows( + showdown_window_projection_column_names(select)?, + rows, + ))) + } + fn try_execute_showdown_rolling_avg_query( + &self, + select: &Select, + query_order_by: &[crate::sql::ast::OrderBy], + table_name: &str, + binding_name: &str, + ) -> Result> { + if select.filter.is_some() + || query_order_by.len() != 1 + || !showdown_window_column_order_matches( + &query_order_by[0], + table_name, + binding_name, + "id", + false, + ) + { + return Ok(None); + } + let Some(schema) = self.table_schema(table_name) else { + return Ok(None); + }; + let Some(source) = self.visible_table_row_source(table_name) else { + return Ok(None); + }; + let Some(id_index) = schema_column_index(schema, "id") else { + return Ok(None); + }; + let Some(rating_index) = schema_column_index(schema, "rating") else { + return Ok(None); + }; + if select.projection.len() != 3 + || !showdown_window_projection_column_matches( + &select.projection[0], + table_name, + binding_name, + "id", + ) + || !showdown_window_projection_column_matches( + &select.projection[1], + table_name, + binding_name, + "rating", + ) + || !showdown_avg_window_projection_matches( + &select.projection[2], + table_name, + binding_name, + "id", + "rating", + "rolling", + ) + { + return Ok(None); + } + + if row_id_alias_column_name(schema).is_some_and(|column| identifiers_equal(column, "id")) + && schema.columns[rating_index].column_type == ColumnType::Float64 + && !schema.columns[rating_index].nullable + { + let mut ratings = Vec::with_capacity(source.row_count()); + let mut already_ordered = true; + let mut previous_movie_id = None; + source.visit_float64_column_values(rating_index, |row_id, rating| { + if previous_movie_id.is_some_and(|previous| previous > row_id) { + already_ordered = false; + } + previous_movie_id = Some(row_id); + let Some(rating) = rating else { + return Err(DbError::internal( + "showdown movie rating column unexpectedly NULL", + )); + }; + ratings.push((row_id, rating)); + Ok(()) + })?; + if !already_ordered { + ratings.sort_by_key(|(movie_id, _)| *movie_id); + } + + let mut rows = Vec::with_capacity(ratings.len()); + let mut previous_two = None; + let mut previous_one = None; + for (movie_id, rating) in ratings { + let rolling = match (previous_two, previous_one) { + (Some(two_back), Some(one_back)) => { + Value::Float64(((two_back + one_back) + rating) / 3.0) + } + (None, Some(one_back)) => Value::Float64((one_back + rating) / 2.0), + _ => Value::Float64(rating), + }; + rows.push(QueryRow::new(vec![ + Value::Int64(movie_id), + Value::Float64(rating), + rolling, + ])); + previous_two = previous_one; + previous_one = Some(rating); + } + return Ok(Some(QueryResult::with_rows( + showdown_window_projection_column_names(select)?, + rows, + ))); + } + + let mut ordered = Vec::with_capacity(source.row_count()); + let mut already_ordered = true; + let mut previous_movie_id = None; + for row in source.rows() { + let row = row?; + let values = row.values(); + let movie_id = showdown_fast_int64_value(values, id_index, "movies.id")?; + if previous_movie_id.is_some_and(|previous| previous > movie_id) { + already_ordered = false; + } + previous_movie_id = Some(movie_id); + let id_value = values + .get(id_index) + .cloned() + .ok_or_else(|| DbError::internal("showdown movie id column missing from row"))?; + let rating = values.get(rating_index).cloned().ok_or_else(|| { + DbError::internal("showdown movie rating column missing from row") + })?; + ordered.push(RollingAvgFastRow { + row_id: row.row_id(), + movie_id, + id_value, + rating, + }); + } + if !already_ordered { + ordered.sort_by(|left, right| { + left.movie_id + .cmp(&right.movie_id) + .then_with(|| left.row_id.cmp(&right.row_id)) + }); + } + + let mut rows = Vec::with_capacity(ordered.len()); + for ordinal in 0..ordered.len() { + let start = ordinal.saturating_sub(2); + let mut total = 0.0_f64; + let mut count = 0_i64; + for item in &ordered[start..=ordinal] { + match &item.rating { + Value::Null => {} + Value::Int64(value) => { + total += *value as f64; + count += 1; + } + Value::Float64(value) => { + total += *value; + count += 1; + } + Value::Decimal { scaled, scale } => { + total += (*scaled as f64) / 10_f64.powi(i32::from(*scale)); + count += 1; + } + other => { + return Err(DbError::sql(format!( + "numeric aggregate does not support {other:?}" + ))) + } + } + } + let rolling = if count == 0 { + Value::Null + } else { + Value::Float64(total / count as f64) + }; + let item = &ordered[ordinal]; + rows.push(QueryRow::new(vec![ + item.id_value.clone(), + item.rating.clone(), + rolling, + ])); + } + Ok(Some(QueryResult::with_rows( + showdown_window_projection_column_names(select)?, + rows, + ))) + } + pub(crate) fn try_execute_showdown_directors_cte_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_showdown_directors_cte_query(query, params)? else { + return Ok(None); + }; + let Some(roles_source) = self.visible_table_row_source(plan.roles_table_name) else { + return Ok(None); + }; + let Some(movie_source) = self.visible_table_row_source(plan.movie_table_name) else { + return Ok(None); + }; + let movie_index_keys = + plan.movie_index_name + .as_deref() + .and_then(|index_name| match self.index(index_name) { + Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), + _ => None, + }); + if !plan.movie_id_is_rowid_alias && movie_index_keys.is_none() { + return Ok(None); + } + + let mut directors = BTreeMap::, DirectorsCteAccumulator>::new(); + for role_row in roles_source.rows() { + let role_row = role_row?; + let role_values = role_row.values(); + if !matches!( + role_values.get(plan.role_job_index), + Some(Value::Text(job)) if job == &plan.director_job + ) { + continue; + } + let Some(person_id) = role_values.get(plan.role_person_id_index) else { + return Err(DbError::internal("roles person_id column missing from row")); + }; + if matches!(person_id, Value::Null) { + continue; + } + let Some(movie_id) = role_values.get(plan.role_movie_id_index) else { + return Err(DbError::internal("roles movie_id column missing from row")); + }; + if matches!(movie_id, Value::Null) { + continue; + } + + let key = row_identity(std::slice::from_ref(person_id))?; + let accumulator = directors + .entry(key) + .or_insert_with(|| DirectorsCteAccumulator::new(person_id.clone())); + accumulate_directors_cte_movie( + &movie_source, + movie_index_keys, + plan.movie_id_is_rowid_alias, + movie_id, + plan.movie_title_index, + plan.movie_rating_index, + accumulator, + )?; + } + + let bounded_order = plan + .order_by + .as_deref() + .zip(plan.limit) + .filter(|(_, _)| plan.offset == 0); + let mut rows = Vec::new(); + for accumulator in directors.into_values() { + if accumulator.films < plan.min_films { + continue; + } + let avg_rating = if accumulator.rating_count == 0 { + Value::Null + } else { + Value::Float64(accumulator.rating_sum / accumulator.rating_count as f64) + }; + let titles = if accumulator.titles.is_empty() { + Value::Null + } else { + Value::Text(accumulator.titles.join(&plan.title_separator)) + }; + let row = QueryRow::new(vec![ + accumulator.person_id, + Value::Int64(accumulator.films), + avg_rating, + titles, + ]); + if let Some((order_by, limit)) = bounded_order { + push_bounded_projection_ordered_query_row( + Some(self), + &mut rows, + row, + order_by, + limit, + )?; + } else { + rows.push(row); + } + } + + if let Some((order_by, _)) = bounded_order { + sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; + return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); + } + + Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + plan.column_names, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + )?)) + } + fn analyze_showdown_directors_cte_query<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result>> { + if query.recursive || query.ctes.len() != 2 || query.offset.is_some() { + return Ok(None); + } + let directed_cte = &query.ctes[0]; + let top_dirs_cte = &query.ctes[1]; + if !identifiers_equal(&directed_cte.name, "directed") + || !directed_cte.column_names.is_empty() + || !identifiers_equal(&top_dirs_cte.name, "top_dirs") + || !top_dirs_cte.column_names.is_empty() + { + return Ok(None); + } + + let Some(directed_plan) = self.analyze_directed_movies_cte(directed_cte)? else { + return Ok(None); + }; + let Some(top_dirs_plan) = + self.analyze_directors_top_dirs_cte(top_dirs_cte, params, &directed_cte.name)? + else { + return Ok(None); + }; + let Some((column_names, order_by, limit, offset, title_separator)) = self + .analyze_directors_final_select( + query, + params, + &directed_cte.name, + &top_dirs_cte.name, + )? + else { + return Ok(None); + }; + + Ok(Some(DirectorsCtePlan { + roles_table_name: directed_plan.roles_table_name, + role_person_id_index: directed_plan.role_person_id_index, + role_movie_id_index: directed_plan.role_movie_id_index, + role_job_index: directed_plan.role_job_index, + director_job: directed_plan.director_job, + movie_table_name: directed_plan.movie_table_name, + movie_title_index: directed_plan.movie_title_index, + movie_rating_index: directed_plan.movie_rating_index, + movie_index_name: directed_plan.movie_index_name, + movie_id_is_rowid_alias: directed_plan.movie_id_is_rowid_alias, + min_films: top_dirs_plan.min_films, + title_separator, + column_names, + order_by, + limit, + offset, + })) + } + fn analyze_directed_movies_cte<'a>( + &'a self, + cte: &'a CommonTableExpr, + ) -> Result>> { + if cte.query.recursive + || !cte.query.ctes.is_empty() + || !cte.query.order_by.is_empty() + || cte.query.limit.is_some() + || cte.query.offset.is_some() + { + return Ok(None); + } + let QueryBody::Select(select) = &cte.query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || !select.group_by.is_empty() + || select.having.is_some() + || select.projection.len() != 4 + || select.from.len() != 1 + { + return Ok(None); + } + + let mut tables = Vec::new(); + let mut constraints = Vec::new(); + if !flatten_inner_join_chain(&select.from[0], &mut tables, &mut constraints) + || tables.len() != 2 + { + return Ok(None); + } + let roles_binding = tables + .iter() + .copied() + .find(|binding| identifiers_equal(binding.name, "roles")); + let movie_binding = tables + .iter() + .copied() + .find(|binding| identifiers_equal(binding.name, "movies")); + let (Some(roles_binding), Some(movie_binding)) = (roles_binding, movie_binding) else { + return Ok(None); + }; + if self + .visible_view(roles_binding.name, NameResolutionScope::Session) + .is_some() + || self + .visible_view(movie_binding.name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(roles_binding.name) + || self.visible_table_is_temporary(movie_binding.name) + { + return Ok(None); + } + let Some(roles_schema) = self.table_schema(roles_binding.name) else { + return Ok(None); + }; + let Some(movie_schema) = self.table_schema(movie_binding.name) else { + return Ok(None); + }; + if !generated_columns_are_stored(roles_schema) + || !generated_columns_are_stored(movie_schema) + { + return Ok(None); + } + + if !projection_expr_matches_binding_column( + &select.projection[0], + roles_binding, + "person_id", + ) || !projection_expr_matches_binding_column( + &select.projection[1], + roles_binding, + "movie_id", + ) || !projection_expr_matches_binding_column( + &select.projection[2], + movie_binding, + "title", + ) || !projection_expr_matches_binding_column( + &select.projection[3], + movie_binding, + "rating", + ) || !join_constraints_match_columns( + &constraints, + movie_binding, + "id", + roles_binding, + "movie_id", + ) { + return Ok(None); + } + let Some(filter) = select.filter.as_ref() else { + return Ok(None); + }; + let Some(director_job) = equality_filter_text_literal(filter, roles_binding, "job") else { + return Ok(None); + }; + + let role_person_id_index = + schema_column_index(roles_schema, "person_id").ok_or_else(|| { + DbError::internal("directors CTE person_id column missing from roles") + })?; + let role_movie_id_index = schema_column_index(roles_schema, "movie_id") + .ok_or_else(|| DbError::internal("directors CTE movie_id column missing from roles"))?; + let role_job_index = schema_column_index(roles_schema, "job") + .ok_or_else(|| DbError::internal("directors CTE job column missing from roles"))?; + let movie_title_index = schema_column_index(movie_schema, "title") + .ok_or_else(|| DbError::internal("directors CTE title column missing from movies"))?; + let movie_rating_index = schema_column_index(movie_schema, "rating") + .ok_or_else(|| DbError::internal("directors CTE rating column missing from movies"))?; + if !matches!( + roles_schema.columns[role_job_index].column_type, + ColumnType::Text + ) || !matches!( + movie_schema.columns[movie_title_index].column_type, + ColumnType::Text + ) { + return Ok(None); + } + let movie_index_name = self + .single_column_btree_index(movie_binding.name, "id") + .map(|index| index.name.clone()); + let movie_id_is_rowid_alias = row_id_alias_column_name(movie_schema) + .is_some_and(|column| identifiers_equal(column, "id")); + if !movie_id_is_rowid_alias && movie_index_name.is_none() { + return Ok(None); + } + + Ok(Some(DirectedMoviesCtePlan { + roles_table_name: roles_binding.name, + role_person_id_index, + role_movie_id_index, + role_job_index, + director_job: director_job.to_string(), + movie_table_name: movie_binding.name, + movie_title_index, + movie_rating_index, + movie_index_name, + movie_id_is_rowid_alias, + })) + } + fn analyze_directors_top_dirs_cte( + &self, + cte: &CommonTableExpr, + params: &[Value], + directed_cte_name: &str, + ) -> Result> { + if cte.query.recursive + || !cte.query.ctes.is_empty() + || !cte.query.order_by.is_empty() + || cte.query.limit.is_some() + || cte.query.offset.is_some() + { + return Ok(None); + } + let QueryBody::Select(select) = &cte.query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || select.filter.is_some() + || select.projection.len() != 3 + || select.group_by.len() != 1 + || select.from.len() != 1 + { + return Ok(None); + } + let FromItem::Table { + name: source_name, + alias, + } = &select.from[0] + else { + return Ok(None); + }; + if !identifiers_equal(source_name, directed_cte_name) { + return Ok(None); + } + let directed_binding = TableBindingRef { + name: source_name, + alias, + }; + if !projection_expr_matches_binding_column( + &select.projection[0], + directed_binding, + "person_id", + ) || !matches!( + &select.projection[1], + SelectItem::Expr { + expr, + alias: Some(alias) + } if identifiers_equal(alias, "films") && aggregate_matches_count_star(expr) + ) || !matches!( + &select.projection[2], + SelectItem::Expr { + expr, + alias: Some(alias) + } if identifiers_equal(alias, "avg_rating") + && aggregate_matches_single_binding_column_or_unqualified( + expr, + "avg", + directed_binding, + "rating" + ) + ) || !expr_matches_binding_column_or_unqualified( + &select.group_by[0], + directed_binding, + "person_id", + ) { + return Ok(None); + } + let min_films = match select.having.as_ref() { + Some(Expr::Binary { + left, + op: BinaryOp::GtEq, + right, + }) if aggregate_matches_count_star(left) => { + self.eval_constant_i64(right, params, &BTreeMap::new())? + } + _ => return Ok(None), + }; + + Ok(Some(DirectorsTopDirsCtePlan { min_films })) + } + fn analyze_directors_final_select( + &self, + query: &Query, + params: &[Value], + directed_cte_name: &str, + top_dirs_cte_name: &str, + ) -> Result> { + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || select.filter.is_some() + || select.having.is_some() + || select.projection.len() != 4 + || select.group_by.len() != 3 + || select.from.len() != 1 + { + return Ok(None); + } + + let FromItem::Join { + left, + right, + kind: JoinKind::Inner, + constraint: JoinConstraint::On(on), + } = &select.from[0] + else { + return Ok(None); + }; + let ( + FromItem::Table { + name: left_name, + alias: left_alias, + }, + FromItem::Table { + name: right_name, + alias: right_alias, + }, + ) = (&**left, &**right) + else { + return Ok(None); + }; + if !identifiers_equal(left_name, top_dirs_cte_name) + || !identifiers_equal(right_name, directed_cte_name) + { + return Ok(None); + } + let top_binding = TableBindingRef { + name: left_name, + alias: left_alias, + }; + let directed_binding = TableBindingRef { + name: right_name, + alias: right_alias, + }; + if !join_constraint_matches_columns( + on, + top_binding, + "person_id", + directed_binding, + "person_id", + ) || !projection_expr_matches_binding_column( + &select.projection[0], + top_binding, + "person_id", + ) || !projection_expr_matches_binding_column(&select.projection[1], top_binding, "films") + || !projection_expr_matches_binding_column( + &select.projection[2], + top_binding, + "avg_rating", + ) + || !group_exprs_match_binding_columns( + &select.group_by, + top_binding, + &["person_id", "films", "avg_rating"], + ) + { + return Ok(None); + } + let Some(title_separator) = + projection_expr_string_agg_separator(&select.projection[3], directed_binding, "title") + else { + return Ok(None); + }; + + let order_by = projection_order_by_plan(&query.order_by, &select.projection); + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let column_names = select + .projection + .iter() + .enumerate() + .map(|(index, item)| match item { + SelectItem::Expr { expr, alias } => alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => { + format!("col{}", index + 1) + } + }) + .collect::>(); + + Ok(Some(( + column_names, + order_by, + limit, + offset, + title_separator.to_string(), + ))) + } + fn analyze_three_table_genre_popularity_query<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result>> { + if !query.ctes.is_empty() || query.recursive { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || select.filter.is_some() + || select.having.is_some() + || select.group_by.len() != 1 + || select.projection.len() != 3 + || select.from.len() != 1 + { + return Ok(None); + } + + let mut tables = Vec::new(); + let mut constraints = Vec::new(); + if !flatten_inner_join_chain(&select.from[0], &mut tables, &mut constraints) + || tables.len() != 3 + { + return Ok(None); + } + let genre_binding = tables + .iter() + .copied() + .find(|binding| identifiers_equal(binding.name, "genres")); + let bridge_binding = tables + .iter() + .copied() + .find(|binding| identifiers_equal(binding.name, "movie_genres")); + let movie_binding = tables + .iter() + .copied() + .find(|binding| identifiers_equal(binding.name, "movies")); + let (Some(genre_binding), Some(bridge_binding), Some(movie_binding)) = + (genre_binding, bridge_binding, movie_binding) + else { + return Ok(None); + }; + + if [genre_binding.name, bridge_binding.name, movie_binding.name] + .iter() + .any(|table| { + self.visible_view(table, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(table) + }) + { + return Ok(None); + } + let Some(genre_schema) = self.table_schema(genre_binding.name) else { + return Ok(None); + }; + let Some(bridge_schema) = self.table_schema(bridge_binding.name) else { + return Ok(None); + }; + let Some(movie_schema) = self.table_schema(movie_binding.name) else { + return Ok(None); + }; + if !generated_columns_are_stored(genre_schema) + || !generated_columns_are_stored(bridge_schema) + || !generated_columns_are_stored(movie_schema) + { + return Ok(None); + } + + let SelectItem::Expr { + expr: name_expr, + alias: name_alias, + } = &select.projection[0] + else { + return Ok(None); + }; + let SelectItem::Expr { + expr: count_expr, + alias: count_alias, + } = &select.projection[1] + else { + return Ok(None); + }; + let SelectItem::Expr { + expr: avg_expr, + alias: avg_alias, + } = &select.projection[2] + else { + return Ok(None); + }; + + if !grouped_projection_expr_matches_group_expr( + name_expr, + &select.group_by[0], + genre_binding, + ) || !expr_matches_binding_column(name_expr, genre_binding, "name") + || !aggregate_matches_count_star(count_expr) + || !aggregate_matches_single_binding_column(avg_expr, "avg", movie_binding, "rating") + { + return Ok(None); + } + + if !join_constraints_match_columns( + &constraints, + genre_binding, + "id", + bridge_binding, + "genre_id", + ) || !join_constraints_match_columns( + &constraints, + movie_binding, + "id", + bridge_binding, + "movie_id", + ) { + return Ok(None); + } + + let genre_id_index = schema_column_index(genre_schema, "id") + .ok_or_else(|| DbError::internal("genre popularity id column missing from genres"))?; + let genre_name_index = schema_column_index(genre_schema, "name") + .ok_or_else(|| DbError::internal("genre popularity name column missing from genres"))?; + let bridge_movie_id_index = + schema_column_index(bridge_schema, "movie_id").ok_or_else(|| { + DbError::internal("genre popularity movie_id column missing from movie_genres") + })?; + let movie_rating_index = schema_column_index(movie_schema, "rating").ok_or_else(|| { + DbError::internal("genre popularity rating column missing from movies") + })?; + + let Some(bridge_genre_index_name) = self + .single_column_btree_index(bridge_binding.name, "genre_id") + .map(|index| index.name.clone()) + else { + return Ok(None); + }; + let movie_index_name = self + .single_column_btree_index(movie_binding.name, "id") + .map(|index| index.name.clone()); + let movie_id_is_rowid_alias = row_id_alias_column_name(movie_schema) + .is_some_and(|column| identifiers_equal(column, "id")); + if !movie_id_is_rowid_alias && movie_index_name.is_none() { + return Ok(None); + } + + let column_names = vec![ + name_alias + .clone() + .unwrap_or_else(|| infer_expr_name(name_expr, 1)), + count_alias + .clone() + .unwrap_or_else(|| infer_expr_name(count_expr, 2)), + avg_alias + .clone() + .unwrap_or_else(|| infer_expr_name(avg_expr, 3)), + ]; + + let order_by = projection_order_by_plan(&query.order_by, &select.projection); + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + + Ok(Some(ThreeTableGenrePopularityPlan { + genre_table_name: genre_binding.name, + genre_id_index, + genre_name_index, + bridge_table_name: bridge_binding.name, + bridge_movie_id_index, + bridge_genre_index_name, + movie_table_name: movie_binding.name, + movie_rating_index, + movie_index_name, + movie_id_is_rowid_alias, + column_names, + order_by, + limit, + offset, + })) + } + pub(crate) fn analyze_left_join_aggregate_query<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result>> { + if !query.ctes.is_empty() || query.recursive { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || select.filter.is_some() + || select.having.is_some() + || select.group_by.is_empty() + || select.from.len() != 1 + { + return Ok(None); + } + let FromItem::Join { + left, + right, + kind, + constraint, + } = &select.from[0] + else { + return Ok(None); + }; + let include_empty_parent = match kind { + JoinKind::Left => true, + JoinKind::Inner => false, + _ => return Ok(None), + }; + let (left_name, left_alias) = match &**left { + FromItem::Table { name, alias } => (name.as_str(), alias), + _ => return Ok(None), + }; + let (right_name, right_alias) = match &**right { + FromItem::Table { name, alias } => (name.as_str(), alias), + _ => return Ok(None), + }; + if self + .visible_view(left_name, NameResolutionScope::Session) + .is_some() + || self + .visible_view(right_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(left_name) + || self.visible_table_is_temporary(right_name) + { + return Ok(None); + } + let Some(left_schema) = self.table_schema(left_name) else { + return Ok(None); + }; + let Some(right_schema) = self.table_schema(right_name) else { + return Ok(None); + }; + if !generated_columns_are_stored(left_schema) || !generated_columns_are_stored(right_schema) + { + return Ok(None); + } + let left_binding = TableBindingRef { + name: left_name, + alias: left_alias, + }; + let right_binding = TableBindingRef { + name: right_name, + alias: right_alias, + }; + let left_group_indexes = + indexed_join_group_column_indexes(&select.group_by, left_binding, left_schema); + let right_group_indexes = + indexed_join_group_column_indexes(&select.group_by, right_binding, right_schema); + let (parent_name, parent_binding, parent_schema, child_name, child_binding, child_schema) = + match (left_group_indexes, right_group_indexes) { + (Some(_group_column_indexes), None) => ( + left_name, + left_binding, + left_schema, + right_name, + right_binding, + right_schema, + ), + (None, Some(_group_column_indexes)) if !include_empty_parent => ( + right_name, + right_binding, + right_schema, + left_name, + left_binding, + left_schema, + ), + _ => return Ok(None), + }; + + let group_column_indexes = + indexed_join_group_column_indexes(&select.group_by, parent_binding, parent_schema) + .ok_or_else(|| DbError::internal("group column indexes mismatch"))?; + + let num_group_cols = select.group_by.len(); + if select.projection.len() <= num_group_cols { + return Ok(None); + } + + for projection_item in select + .projection + .iter() + .take(num_group_cols) + .zip(&select.group_by) + { + let (projection_item, group_expr) = projection_item; + let SelectItem::Expr { + expr: projection_expr, + .. + } = projection_item + else { + return Ok(None); + }; + if !grouped_projection_expr_matches_group_expr( + projection_expr, + group_expr, + parent_binding, + ) { + return Ok(None); + } + } + + let mut aggregate_kinds = Vec::with_capacity(select.projection.len() - num_group_cols); + for projection_item in select.projection.iter().skip(num_group_cols) { + let SelectItem::Expr { expr, .. } = projection_item else { + return Ok(None); + }; + let Some(kind) = classify_indexed_join_aggregate(expr, child_binding, child_schema) + else { + return Ok(None); + }; + aggregate_kinds.push(kind); + } + + let mut column_names = Vec::with_capacity(select.projection.len()); + for (index, projection_item) in select.projection.iter().enumerate() { + let SelectItem::Expr { expr, alias } = projection_item else { + return Ok(None); + }; + column_names.push( + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + ); + } + + let Some(join_equalities) = simple_indexed_join_constraint_equalities( + constraint, + left_binding, + right_binding, + left_schema, + right_schema, + ) else { + return Ok(None); + }; + let Some((left_join_columns, right_join_columns)) = + orient_join_equalities(&join_equalities, left_binding, right_binding) + else { + return Ok(None); + }; + if left_join_columns.len() != 1 || right_join_columns.len() != 1 { + return Ok(None); + } + + let (parent_join_column, child_join_column) = if identifiers_equal(parent_name, left_name) { + (left_join_columns[0], right_join_columns[0]) + } else { + (right_join_columns[0], left_join_columns[0]) + }; + + let parent_join_index = parent_schema + .columns + .iter() + .position(|column| identifiers_equal(&column.name, parent_join_column)) + .ok_or_else(|| { + DbError::internal(format!( + "join column {}.{} not found", + parent_name, parent_join_column + )) + })?; + let child_join_index = child_schema + .columns + .iter() + .position(|column| identifiers_equal(&column.name, child_join_column)) + .ok_or_else(|| { + DbError::internal(format!( + "join column {}.{} not found", + child_name, child_join_column + )) + })?; + + let child_index_name = self + .single_column_btree_index(child_name, child_join_column) + .map(|index| index.name.clone()); + + let order_by = projection_order_by_plan(&query.order_by, &select.projection); + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + + Ok(Some(LeftJoinAggregatePlan { + parent_table_name: parent_name, + parent_join_index, + child_table_name: child_name, + child_join_index, + child_index_name, + group_column_indexes, + aggregate_kinds, + column_names, + order_by, + limit, + offset, + include_empty_parent, + })) + } + fn analyze_left_join_status_aggregate_query<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result>> { + if !query.ctes.is_empty() || query.recursive { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || query.order_by.len() > 2 + || select.filter.is_some() + || select.having.is_some() + || select.group_by.len() != 2 + || select.projection.len() != 7 + || select.from.len() != 1 + { + return Ok(None); + } + + let FromItem::Join { + left, + right, + kind, + constraint, + } = &select.from[0] + else { + return Ok(None); + }; + if !matches!(kind, JoinKind::Left) { + return Ok(None); + } + + let (left_name, left_alias) = match &**left { + FromItem::Table { name, alias } => (name.as_str(), alias), + _ => return Ok(None), + }; + let (right_name, right_alias) = match &**right { + FromItem::Table { name, alias } => (name.as_str(), alias), + _ => return Ok(None), + }; + if self + .visible_view(left_name, NameResolutionScope::Session) + .is_some() + || self + .visible_view(right_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(left_name) + || self.visible_table_is_temporary(right_name) + { + return Ok(None); + } + let Some(left_schema) = self.table_schema(left_name) else { + return Ok(None); + }; + let Some(right_schema) = self.table_schema(right_name) else { + return Ok(None); + }; + if !generated_columns_are_stored(left_schema) || !generated_columns_are_stored(right_schema) + { + return Ok(None); + } + + let left_binding = TableBindingRef { + name: left_name, + alias: left_alias, + }; + let right_binding = TableBindingRef { + name: right_name, + alias: right_alias, + }; + let left_group_indexes = + indexed_join_group_column_indexes(&select.group_by, left_binding, left_schema); + let right_group_indexes = + indexed_join_group_column_indexes(&select.group_by, right_binding, right_schema); + let ( + parent_name, + parent_alias, + parent_schema, + child_name, + child_alias, + child_schema, + group_column_indexes, + ) = match (left_group_indexes, right_group_indexes) { + (Some(group_column_indexes), None) => ( + left_name, + left_alias, + left_schema, + right_name, + right_alias, + right_schema, + group_column_indexes, + ), + _ => return Ok(None), + }; + + let parent_binding = TableBindingRef { + name: parent_name, + alias: parent_alias, + }; + let child_binding = TableBindingRef { + name: child_name, + alias: child_alias, + }; + + for (projection_item, group_expr) in select + .projection + .iter() + .take(select.group_by.len()) + .zip(&select.group_by) + { + let SelectItem::Expr { + expr: projection_expr, + .. + } = projection_item + else { + return Ok(None); + }; + if !grouped_projection_expr_matches_group_expr( + projection_expr, + group_expr, + parent_binding, + ) { + return Ok(None); + } + } + + let SelectItem::Expr { + expr: open_expr, .. + } = &select.projection[2] + else { + return Ok(None); + }; + let SelectItem::Expr { + expr: in_progress_expr, + .. + } = &select.projection[3] + else { + return Ok(None); + }; + let SelectItem::Expr { + expr: resolved_expr, + .. + } = &select.projection[4] + else { + return Ok(None); + }; + let SelectItem::Expr { + expr: closed_expr, .. + } = &select.projection[5] + else { + return Ok(None); + }; + let SelectItem::Expr { + expr: count_expr, .. + } = &select.projection[6] + else { + return Ok(None); + }; + + if !aggregate_matches_status_case_sum(open_expr, "sum", child_binding, "status", "open") + || !aggregate_matches_status_case_sum( + in_progress_expr, + "sum", + child_binding, + "status", + "in_progress", + ) + || !aggregate_matches_status_case_sum( + resolved_expr, + "sum", + child_binding, + "status", + "resolved", + ) + || !aggregate_matches_status_case_sum( + closed_expr, + "sum", + child_binding, + "status", + "closed", + ) + || !aggregate_matches_single_binding_column(count_expr, "count", child_binding, "id") + { + return Ok(None); + } + + let mut column_names = Vec::with_capacity(select.projection.len()); + for (index, projection_item) in select.projection.iter().enumerate() { + let SelectItem::Expr { expr, alias } = projection_item else { + return Ok(None); + }; + column_names.push( + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + ); + } + + let Some(join_equalities) = simple_indexed_join_constraint_equalities( + constraint, + left_binding, + right_binding, + left_schema, + right_schema, + ) else { + return Ok(None); + }; + let Some((left_join_columns, right_join_columns)) = + orient_join_equalities(&join_equalities, left_binding, right_binding) + else { + return Ok(None); + }; + if left_join_columns.len() != 1 || right_join_columns.len() != 1 { + return Ok(None); + } + + let (parent_join_column, child_join_column) = if identifiers_equal(parent_name, left_name) { + (left_join_columns[0], right_join_columns[0]) + } else { + (right_join_columns[0], left_join_columns[0]) + }; + + let parent_join_index = parent_schema + .columns + .iter() + .position(|column| identifiers_equal(&column.name, parent_join_column)) + .ok_or_else(|| { + DbError::internal(format!( + "join column {}.{} not found", + parent_name, parent_join_column + )) + })?; + let child_join_index = child_schema + .columns + .iter() + .position(|column| identifiers_equal(&column.name, child_join_column)) + .ok_or_else(|| { + DbError::internal(format!( + "join column {}.{} not found", + child_name, child_join_column + )) + })?; + + let child_status_index = schema_column_index(child_schema, "status").ok_or_else(|| { + DbError::internal(format!( + "column status not found in child table {}", + child_name + )) + })?; + let child_id_index = schema_column_index(child_schema, "id").ok_or_else(|| { + DbError::internal(format!("column id not found in child table {}", child_name)) + })?; + let child_index_name = self + .single_column_btree_index(child_name, child_join_column) + .map(|index| index.name.clone()); + + let order_by = projection_order_by_plan(&query.order_by, &select.projection); + if !query.order_by.is_empty() { + let Some(order_by) = order_by.as_ref() else { + return Ok(None); + }; + if order_by.len() != 2 + || order_by[0].projection_index != 6 + || !order_by[0].descending + || order_by[1].projection_index != 0 + || order_by[1].descending + { + return Ok(None); + } + } + + if child_schema.columns[child_status_index].column_type != crate::catalog::ColumnType::Text + { + return Ok(None); + } + + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + + Ok(Some(LeftJoinStatusAggregatePlan { + parent_table_name: parent_name, + parent_join_index, + child_table_name: child_name, + child_join_index, + child_status_index, + child_id_index, + child_index_name, + group_column_indexes, + column_names, + order_by, + limit, + offset, + })) + } + pub(crate) fn try_execute_indexed_join_grouped_count_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_indexed_join_grouped_count_query(query, params)? else { + return Ok(None); + }; + let Some(parent_source) = self.visible_table_row_source(plan.parent_table_name) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&plan.child_index_name) else { + return Ok(None); + }; + let child_source = self + .catalog + .index(&plan.child_index_name) + .and_then(|index| self.visible_table_row_source(&index.table_name)); + let parent_table = self.table_schema(plan.parent_table_name).ok_or_else(|| { + DbError::internal(format!( + "table {} not found for indexed grouped join count", + plan.parent_table_name + )) + })?; + let parent_join_index = parent_table + .columns + .iter() + .position(|column| identifiers_equal(&column.name, plan.parent_join_column)) + .ok_or_else(|| { + DbError::internal(format!( + "join column {}.{} not found", + plan.parent_table_name, plan.parent_join_column + )) + })?; + + let bounded_order = plan + .order_by + .as_deref() + .zip(plan.limit) + .filter(|(_, _)| plan.offset == 0); + let scalar_count_top_n_limit = plan.scalar_count_top_n_limit(); + let mut scalar_count_top_n_rows: Vec<(i64, QueryRow)> = Vec::new(); + let mut rows = Vec::new(); + for parent_row in parent_source.rows() { + let parent_row = parent_row?; + let parent_values = parent_row.values(); + let Some(join_value) = parent_values.get(parent_join_index) else { + return Err(DbError::internal("parent join row is shorter than schema")); + }; + if matches!(join_value, Value::Null) { + continue; + } + let child_row_ids = keys.row_ids_for_value_set(join_value)?; + let child_count = if let Some(child_source) = child_source { + visible_row_id_set_count(child_source, child_row_ids)? + } else { + child_row_ids.len() + }; + if child_count == 0 { + continue; + } + let child_count = i64::try_from(child_count).map_err(|_| { + DbError::sql(format!( + "join count for table {} exceeds INT64 limits", + plan.parent_table_name + )) + })?; + + let scalar_count_top_n_slot = if let Some(limit) = scalar_count_top_n_limit { + if limit == 0 { + continue; + } + if scalar_count_top_n_rows.len() < limit { + Some(scalar_count_top_n_rows.len()) + } else { + let mut worst_index = 0; + for index in 1..scalar_count_top_n_rows.len() { + if scalar_count_top_n_rows[index].0 < scalar_count_top_n_rows[worst_index].0 + { + worst_index = index; + } + } + if child_count <= scalar_count_top_n_rows[worst_index].0 { + continue; + } + Some(worst_index) + } + } else { + None + }; + + let mut output = Vec::with_capacity(plan.group_column_indexes.len() + 1); + for index in &plan.group_column_indexes { + output.push(parent_values[*index].clone()); + } + output.push(Value::Int64(child_count)); + let row = QueryRow::new(output); + if let Some(slot) = scalar_count_top_n_slot { + if slot == scalar_count_top_n_rows.len() { + scalar_count_top_n_rows.push((child_count, row)); + } else { + scalar_count_top_n_rows[slot] = (child_count, row); + } + } else if let Some((order_by, limit)) = bounded_order { + push_bounded_projection_ordered_query_row( + Some(self), + &mut rows, + row, + order_by, + limit, + )?; + } else { + rows.push(row); + } + } + + if scalar_count_top_n_limit.is_some() { + scalar_count_top_n_rows.sort_by_key(|row| std::cmp::Reverse(row.0)); + let rows = scalar_count_top_n_rows + .into_iter() + .map(|(_, row)| row) + .collect(); + return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); + } + + if let Some((order_by, _)) = bounded_order { + sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; + return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); + } + + Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + plan.column_names, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + )?)) + } + pub(crate) fn indexed_join_grouped_count_parent_table_name<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result> { + Ok(self + .analyze_indexed_join_grouped_count_query(query, params)? + .map(|plan| plan.parent_table_name)) + } + pub(crate) fn analyze_indexed_join_grouped_count_query<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result>> { + if !query.ctes.is_empty() || query.recursive { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || select.filter.is_some() + || select.having.is_some() + || select.group_by.is_empty() + || select.from.len() != 1 + || select.projection.len() != select.group_by.len() + 1 + { + return Ok(None); + } + let FromItem::Join { + left, + right, + kind, + constraint, + } = &select.from[0] + else { + return Ok(None); + }; + if !matches!(kind, JoinKind::Inner) { + return Ok(None); + } + let (left_name, left_alias) = match &**left { + FromItem::Table { name, alias } => (name.as_str(), alias), + _ => return Ok(None), + }; + let (right_name, right_alias) = match &**right { + FromItem::Table { name, alias } => (name.as_str(), alias), + _ => return Ok(None), + }; + if self + .visible_view(left_name, NameResolutionScope::Session) + .is_some() + || self + .visible_view(right_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(left_name) + || self.visible_table_is_temporary(right_name) + { + return Ok(None); + } + let Some(left_schema) = self.table_schema(left_name) else { + return Ok(None); + }; + let Some(right_schema) = self.table_schema(right_name) else { + return Ok(None); + }; + if !generated_columns_are_stored(left_schema) || !generated_columns_are_stored(right_schema) + { + return Ok(None); + } + + let left_binding = TableBindingRef { + name: left_name, + alias: left_alias, + }; + let right_binding = TableBindingRef { + name: right_name, + alias: right_alias, + }; + let left_group_indexes = + indexed_join_group_column_indexes(&select.group_by, left_binding, left_schema); + let right_group_indexes = + indexed_join_group_column_indexes(&select.group_by, right_binding, right_schema); + let ( + parent_name, + parent_binding, + parent_schema, + child_name, + child_binding, + child_schema, + group_column_indexes, + ) = match (left_group_indexes, right_group_indexes) { + (Some(group_column_indexes), None) => ( + left_name, + left_binding, + left_schema, + right_name, + right_binding, + right_schema, + group_column_indexes, + ), + (None, Some(group_column_indexes)) => ( + right_name, + right_binding, + right_schema, + left_name, + left_binding, + left_schema, + group_column_indexes, + ), + _ => return Ok(None), + }; + + let SelectItem::Expr { + expr: count_expr, + alias: count_alias, + } = &select.projection[select.group_by.len()] + else { + return Ok(None); + }; + if !indexed_join_grouped_count_is_safe(count_expr, child_binding, child_schema) { + return Ok(None); + } + + let mut column_names = Vec::with_capacity(select.projection.len()); + for (projection_item, group_expr) in select + .projection + .iter() + .take(select.group_by.len()) + .zip(&select.group_by) + { + let SelectItem::Expr { + expr: projection_expr, + alias, + } = projection_item + else { + return Ok(None); + }; + if !grouped_projection_expr_matches_group_expr( + projection_expr, + group_expr, + parent_binding, + ) { + return Ok(None); + } + column_names.push( + alias + .clone() + .unwrap_or_else(|| infer_expr_name(projection_expr, column_names.len() + 1)), + ); + } + column_names.push( + count_alias + .clone() + .unwrap_or_else(|| infer_expr_name(count_expr, select.group_by.len() + 1)), + ); + + let Some(join_equalities) = simple_indexed_join_constraint_equalities( + constraint, + left_binding, + right_binding, + left_schema, + right_schema, + ) else { + return Ok(None); + }; + let Some((parent_join_columns, child_join_columns)) = + orient_join_equalities(&join_equalities, parent_binding, child_binding) + else { + return Ok(None); + }; + if parent_join_columns.len() != 1 || child_join_columns.len() != 1 { + return Ok(None); + } + let parent_join_column = parent_join_columns[0]; + let child_join_column = child_join_columns[0]; + if parent_schema + .columns + .iter() + .all(|column| !identifiers_equal(&column.name, parent_join_column)) + { + return Ok(None); + } + let Some(child_index_name) = self + .catalog + .indexes + .values() + .find(|index| { + identifiers_equal(&index.table_name, child_name) + && index.fresh + && index.kind == IndexKind::Btree + && index.predicate_sql.is_none() + && index.columns.len() == 1 + && index.columns[0].expression_sql.is_none() + && index.columns[0] + .column_name + .as_deref() + .is_some_and(|column| identifiers_equal(column, child_join_column)) + }) + .map(|index| index.name.clone()) + else { + return Ok(None); + }; + + let order_by = projection_order_by_plan(&query.order_by, &select.projection); + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + + Ok(Some(IndexedJoinGroupedCountPlan { + parent_table_name: parent_name, + parent_join_column, + child_index_name, + group_column_indexes, + column_names, + order_by, + limit, + offset, + })) + } + pub(crate) fn try_execute_indexed_join_limit_projection_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + if query.recursive || !query.ctes.is_empty() || !query.order_by.is_empty() { + return Ok(None); + } + let Some(limit_expr) = query.limit.as_ref() else { + return Ok(None); + }; + let ctes = BTreeMap::new(); + let limit_value = match simple_int64_constant_expr_value(limit_expr, params)? { + Some(value) => value, + None => self.eval_constant_i64(limit_expr, params, &ctes)?, + }; + let limit = usize::try_from(limit_value.max(0)).unwrap_or(usize::MAX); + let offset = query + .offset + .as_ref() + .map(|expr| { + simple_int64_constant_expr_value(expr, params)? + .map(Ok) + .unwrap_or_else(|| self.eval_constant_i64(expr, params, &ctes)) + }) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + self.try_execute_indexed_join_limit_projection_select( + select, + &select.projection, + limit, + offset, + ) + } + pub(crate) fn try_execute_indexed_join_limit_projection_select( + &self, + select: &Select, + projection: &[SelectItem], + limit: usize, + offset: usize, + ) -> Result> { + if limit == 0 { + let column_names = projection + .iter() + .enumerate() + .map(|(index, item)| match item { + SelectItem::Expr { expr, alias } => alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + _ => format!("col{}", index + 1), + }) + .collect(); + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + } + let Some(plan) = + self.analyze_indexed_join_limit_projection_select(select, projection, limit, offset)? + else { + return Ok(None); + }; + self.execute_indexed_join_limit_projection_plan(&plan) + .map(Some) + } + pub(crate) fn analyze_indexed_join_limit_projection_select<'a>( + &'a self, + select: &'a Select, + projection: &'a [SelectItem], + limit: usize, + offset: usize, + ) -> Result>> { + if select.distinct + || !select.distinct_on.is_empty() + || select.filter.is_some() + || !select.group_by.is_empty() + || select.having.is_some() + || projection_has_aggregate_items(projection) + || select.from.len() != 1 + { + return Ok(None); + } + let mut tables = Vec::new(); + let mut constraints = Vec::new(); + if !flatten_left_deep_inner_join_tables(&select.from[0], &mut tables, &mut constraints) { + return Ok(None); + } + if !(2..=3).contains(&tables.len()) || constraints.len() + 1 != tables.len() { + return Ok(None); + } + for table in &tables { + if self + .visible_view(table.name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(table.name) + { + return Ok(None); + } + let Some(schema) = self.table_schema(table.name) else { + return Ok(None); + }; + if !generated_columns_are_stored(schema) + || self.visible_table_row_source(table.name).is_none() + { + return Ok(None); + } + } + + let mut steps = Vec::with_capacity(constraints.len()); + for (right_table_index, constraint) in + constraints.iter().enumerate().map(|(i, c)| (i + 1, c)) + { + let Some(step) = self.indexed_join_limit_step_for_constraint( + &tables, + right_table_index, + constraint, + )? + else { + return Ok(None); + }; + steps.push(step); + } + + let mut projections = Vec::with_capacity(projection.len()); + for (index, item) in projection.iter().enumerate() { + let SelectItem::Expr { expr, alias } = item else { + return Ok(None); + }; + let Some((table_index, column_index)) = + indexed_join_limit_projection_column(expr, &tables, self) + else { + return Ok(None); + }; + projections.push(IndexedJoinLimitProjection { + table_index, + column_index, + column_name: alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + }); + } + + Ok(Some(IndexedJoinLimitPlan { + tables, + steps, + projections, + limit, + offset, + })) + } + fn indexed_join_limit_step_for_constraint( + &self, + tables: &[IndexedJoinLimitTablePlan<'_>], + right_table_index: usize, + constraint: &JoinConstraint, + ) -> Result> { + let JoinConstraint::On(on) = constraint else { + return Ok(None); + }; + let Some(equalities) = simple_join_equalities(on) else { + return Ok(None); + }; + if equalities.len() != 1 { + return Ok(None); + } + let right_table = tables[right_table_index]; + let right_binding = TableBindingRef { + name: right_table.name, + alias: right_table.alias, + }; + let (left_ref, right_ref) = equalities[0]; + let (previous_ref, right_ref) = if matches_table_binding(right_binding, right_ref.table) { + (left_ref, right_ref) + } else if matches_table_binding(right_binding, left_ref.table) { + (right_ref, left_ref) + } else { + return Ok(None); + }; + + let Some(previous_table_index) = (0..right_table_index).find(|index| { + let table = tables[*index]; + matches_table_binding( + TableBindingRef { + name: table.name, + alias: table.alias, + }, + previous_ref.table, + ) + }) else { + return Ok(None); + }; + let previous_schema = self + .table_schema(tables[previous_table_index].name) + .ok_or_else(|| DbError::internal("indexed join previous table missing"))?; + let right_schema = self + .table_schema(right_table.name) + .ok_or_else(|| DbError::internal("indexed join right table missing"))?; + let Some(previous_column_index) = schema_column_index(previous_schema, previous_ref.column) + else { + return Ok(None); + }; + let Some(right_column_index) = schema_column_index(right_schema, right_ref.column) else { + return Ok(None); + }; + if crate::exec::dml::row_id_alias_column_name(right_schema) + .is_some_and(|column| identifiers_equal(column, right_ref.column)) + { + let _ = right_column_index; + return Ok(Some(IndexedJoinLimitStep { + previous_table_index, + previous_column_index, + right_index_name: None, + })); + } + let Some(index) = self.catalog.indexes.values().find(|index| { + identifiers_equal(&index.table_name, right_table.name) + && index.fresh + && index.kind == IndexKind::Btree + && index.predicate_sql.is_none() + && index.columns.len() == 1 + && index.columns[0].expression_sql.is_none() + && index.columns[0] + .column_name + .as_deref() + .is_some_and(|column| identifiers_equal(column, right_ref.column)) + }) else { + let _ = right_column_index; + return Ok(None); + }; + let _ = right_column_index; + Ok(Some(IndexedJoinLimitStep { + previous_table_index, + previous_column_index, + right_index_name: Some(index.name.clone()), + })) + } + pub(crate) fn ordered_view_root_btree_index( + &self, + table_name: &str, + column_name: &str, + root_filter: Option<&Expr>, + root_binding: &str, + ) -> Result> { + let mut full_index = None; + for index in self.catalog.indexes.values() { + if !single_plain_btree_index_matches_column(index, table_name, column_name) { + continue; + } + let Some(predicate_sql) = index.predicate_sql.as_deref() else { + if full_index.is_none() { + full_index = Some(index); + } + continue; + }; + let Some(root_filter) = root_filter else { + continue; + }; + let predicate = crate::sql::parser::parse_expression_sql(predicate_sql)?; + if filter_contains_partial_index_predicate(root_filter, &predicate, root_binding) { + return Ok(Some(index)); + } + } + Ok(full_index) + } + pub(crate) fn try_execute_three_table_indexed_join_projection_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + if query.recursive || !query.ctes.is_empty() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.from.len() != 1 { + return Ok(None); + } + let mut tables = Vec::new(); + let mut constraints = Vec::new(); + if !flatten_left_deep_inner_join_tables(&select.from[0], &mut tables, &mut constraints) + || tables.len() != 3 + || constraints.len() != 2 + { + return Ok(None); + } + let natural_order = if query.order_by.is_empty() { + None + } else { + self.three_table_join_natural_order(query, &tables) + }; + let order_by = if query.order_by.is_empty() || natural_order.is_some() { + None + } else { + let Some(order_by) = projection_order_by_plan(&query.order_by, &select.projection) + else { + return Ok(None); + }; + Some(order_by) + }; + let Some(plan) = self.analyze_indexed_join_limit_projection_select( + select, + &select.projection, + usize::MAX, + 0, + )? + else { + return Ok(None); + }; + if plan.tables.len() != 3 { + return Ok(None); + } + + let mut rows = self.execute_indexed_join_projection_rows( + &plan, + natural_order.is_some(), + natural_order.flatten(), + )?; + if let Some(order_by) = order_by.as_deref() { + sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; + } + + let ctes = BTreeMap::new(); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &ctes)) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &ctes)) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + if offset > 0 || limit.is_some() { + rows = rows + .into_iter() + .skip(offset) + .take(limit.unwrap_or(usize::MAX)) + .collect(); + } + Ok(Some(QueryResult::with_rows( + plan.projections + .iter() + .map(|projection| projection.column_name.clone()) + .collect(), + rows, + ))) + } + fn three_table_join_natural_order( + &self, + query: &Query, + tables: &[IndexedJoinLimitTablePlan<'_>], + ) -> Option> { + if !(1..=2).contains(&query.order_by.len()) { + return None; + } + if query + .order_by + .iter() + .any(|order| order.descending || order.collation.is_some()) + { + return None; + } + let first_schema = self.table_schema(tables[0].name)?; + let first_rowid_column = crate::exec::dml::row_id_alias_column_name(first_schema)?; + let Expr::Column { + table: first_table, + column: first_column, + } = &query.order_by[0].expr + else { + return None; + }; + if !matches_table_binding( + TableBindingRef { + name: tables[0].name, + alias: tables[0].alias, + }, + first_table.as_deref(), + ) || !identifiers_equal(first_column, first_rowid_column) + { + return None; + } + if query.order_by.len() == 1 { + return Some(None); + } + + let second_schema = self.table_schema(tables[1].name)?; + let Expr::Column { + table: second_table, + column: second_column, + } = &query.order_by[1].expr + else { + return None; + }; + if !matches_table_binding( + TableBindingRef { + name: tables[1].name, + alias: tables[1].alias, + }, + second_table.as_deref(), + ) { + return None; + } + schema_column_index(second_schema, second_column).map(Some) + } + pub(crate) fn try_execute_base_table_join( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_base_table_join_query(query) else { + return Ok(None); + }; + let Some(left_source) = self.visible_table_row_source(plan.left_name) else { + return Ok(None); + }; + let Some(right_source) = self.visible_table_row_source(plan.right_name) else { + return Ok(None); + }; + Ok(Some(self.execute_base_table_join_from_sources( + left_source, + right_source, + &plan, + params, + )?)) + } + fn analyze_base_table_join_query<'a>( + &'a self, + query: &'a Query, + ) -> Option> { + if !query.ctes.is_empty() || query.recursive { + return None; + } + let QueryBody::Select(select) = &query.body else { + return None; + }; + if !select.group_by.is_empty() + || select.having.is_some() + || projection_has_aggregate_items(&select.projection) + { + return None; + } + if select.from.len() != 1 { + return None; + } + let FromItem::Join { + left, + right, + kind, + constraint, + } = &select.from[0] + else { + return None; + }; + if !matches!( + kind, + JoinKind::Inner | JoinKind::Left | JoinKind::Right | JoinKind::Full + ) { + return None; + } + let (left_name, left_alias) = match &**left { + FromItem::Table { name, alias } => (name, alias.as_deref()), + _ => return None, + }; + let (right_name, right_alias) = match &**right { + FromItem::Table { name, alias } => (name, alias.as_deref()), + _ => return None, + }; + if self + .visible_view(left_name, NameResolutionScope::Session) + .is_some() + || self + .visible_view(right_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(left_name) + || self.visible_table_is_temporary(right_name) + { + return None; + } + if self.table_schema(left_name).is_none() || self.table_schema(right_name).is_none() { + return None; + } + if select.projection.iter().any(|item| { + matches!( + item, + SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) + ) + }) { + return None; + } + for item in &select.projection { + if let SelectItem::Expr { expr, .. } = item { + if expr_contains_window(expr) { + return None; + } + } + } + for order in &query.order_by { + if expr_contains_window(&order.expr) { + return None; + } + } + if select.filter.as_ref().is_some_and(expr_contains_window) { + return None; + } + Some(BaseTableJoinPlan { + left_name, + left_alias, + right_name, + right_alias, + kind: *kind, + constraint, + filter: select.filter.as_ref(), + projection: &select.projection, + order_by: &query.order_by, + distinct: select.distinct, + limit: query.limit.as_ref(), + offset: query.offset.as_ref(), + }) + } + pub(crate) fn try_execute_benchmark_history_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + if !query.ctes.is_empty() + || query.limit.is_some() + || query.offset.is_some() + || query.order_by.len() != 1 + { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if !select.group_by.is_empty() + || select.having.is_some() + || select.distinct + || !select.distinct_on.is_empty() + || select.from.len() != 1 + || select.projection.len() != 6 + { + return Ok(None); + } + + let Some(filter) = select.filter.as_ref() else { + return Ok(None); + }; + let Some((filter_table, filter_column, value_expr)) = simple_btree_lookup(filter) else { + return Ok(None); + }; + + let FromItem::Join { + left: order_payment_items, + right: item_item, + kind: JoinKind::Inner, + constraint: JoinConstraint::On(item_join_on), + } = &select.from[0] + else { + return Ok(None); + }; + let FromItem::Table { + name: item_name, + alias: item_alias, + } = &**item_item + else { + return Ok(None); + }; + let FromItem::Join { + left: order_payment, + right: order_item_item, + kind: JoinKind::Inner, + constraint: JoinConstraint::On(order_item_join_on), + } = &**order_payment_items + else { + return Ok(None); + }; + let FromItem::Table { + name: order_item_name, + alias: order_item_alias, + } = &**order_item_item + else { + return Ok(None); + }; + let FromItem::Join { + left: order_item, + right: payment_item, + kind: JoinKind::Inner, + constraint: JoinConstraint::On(payment_join_on), + } = &**order_payment + else { + return Ok(None); + }; + let FromItem::Table { + name: order_name, + alias: order_alias, + } = &**order_item + else { + return Ok(None); + }; + let FromItem::Table { + name: payment_name, + alias: payment_alias, + } = &**payment_item + else { + return Ok(None); + }; + + let order_binding = TableBindingRef { + name: order_name, + alias: order_alias, + }; + let payment_binding = TableBindingRef { + name: payment_name, + alias: payment_alias, + }; + let order_item_binding = TableBindingRef { + name: order_item_name, + alias: order_item_alias, + }; + let item_binding = TableBindingRef { + name: item_name, + alias: item_alias, + }; + + if self + .visible_view(order_name, NameResolutionScope::Session) + .is_some() + || self + .visible_view(payment_name, NameResolutionScope::Session) + .is_some() + || self + .visible_view(order_item_name, NameResolutionScope::Session) + .is_some() + || self + .visible_view(item_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(order_name) + || self.visible_table_is_temporary(payment_name) + || self.visible_table_is_temporary(order_item_name) + || self.visible_table_is_temporary(item_name) + { + return Ok(None); + } + + if !matches_filter_binding(order_name, order_alias, filter_table) + || !identifiers_equal(filter_column, "user_id") + { + return Ok(None); + } + if !join_constraint_matches_columns( + payment_join_on, + order_binding, + "id", + payment_binding, + "order_id", + ) || !join_constraint_matches_columns( + order_item_join_on, + order_binding, + "id", + order_item_binding, + "order_id", + ) || !join_constraint_matches_columns( + item_join_on, + order_item_binding, + "item_id", + item_binding, + "id", + ) { + return Ok(None); + } + + if !query.order_by[0].descending + || !expr_matches_binding_column(&query.order_by[0].expr, order_binding, "id") + { + return Ok(None); + } + + let projection = [ + (0, order_binding, "id"), + (1, order_binding, "total_amount"), + (2, payment_binding, "status"), + (3, item_binding, "name"), + (4, order_item_binding, "quantity"), + (5, order_item_binding, "price"), + ]; + for (index, binding, column) in projection { + let SelectItem::Expr { expr, .. } = &select.projection[index] else { + return Ok(None); + }; + if !expr_matches_binding_column(expr, binding, column) { + return Ok(None); + } + } + + let order_schema = match self.table_schema(order_name) { + Some(table) => table, + None => return Ok(None), + }; + let payment_schema = match self.table_schema(payment_name) { + Some(table) => table, + None => return Ok(None), + }; + let order_item_schema = match self.table_schema(order_item_name) { + Some(table) => table, + None => return Ok(None), + }; + let item_schema = match self.table_schema(item_name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(order_schema) + || !generated_columns_are_stored(payment_schema) + || !generated_columns_are_stored(order_item_schema) + || !generated_columns_are_stored(item_schema) + { + return Ok(None); + } + + let Some(order_source) = self.visible_table_row_source(order_name) else { + return Ok(None); + }; + let Some(payment_source) = self.visible_table_row_source(payment_name) else { + return Ok(None); + }; + let Some(order_item_source) = self.visible_table_row_source(order_item_name) else { + return Ok(None); + }; + let Some(item_source) = self.visible_table_row_source(item_name) else { + return Ok(None); + }; + + let Some(order_user_keys) = self.single_column_btree_keys(order_name, "user_id") else { + return Ok(None); + }; + let Some(payment_order_keys) = self.single_column_btree_keys(payment_name, "order_id") + else { + return Ok(None); + }; + let Some(order_item_order_keys) = + self.single_column_btree_keys(order_item_name, "order_id") + else { + return Ok(None); + }; + let Some(item_id_keys) = self.single_column_btree_keys(item_name, "id") else { + return Ok(None); + }; + + let Some(order_id_index) = schema_column_index(order_schema, "id") else { + return Ok(None); + }; + let Some(order_total_amount_index) = schema_column_index(order_schema, "total_amount") + else { + return Ok(None); + }; + let Some(payment_status_index) = schema_column_index(payment_schema, "status") else { + return Ok(None); + }; + let Some(order_item_item_id_index) = schema_column_index(order_item_schema, "item_id") + else { + return Ok(None); + }; + let Some(order_item_quantity_index) = schema_column_index(order_item_schema, "quantity") + else { + return Ok(None); + }; + let Some(order_item_price_index) = schema_column_index(order_item_schema, "price") else { + return Ok(None); + }; + let Some(item_name_index) = schema_column_index(item_schema, "name") else { + return Ok(None); + }; + + if order_schema.columns[order_id_index].column_type != crate::catalog::ColumnType::Int64 + || order_item_schema.columns[order_item_item_id_index].column_type + != crate::catalog::ColumnType::Int64 + || order_item_schema.columns[order_item_quantity_index].column_type + != crate::catalog::ColumnType::Int64 + || payment_schema.columns[payment_status_index].column_type + != crate::catalog::ColumnType::Text + || item_schema.columns[item_name_index].column_type != crate::catalog::ColumnType::Text + { + return Ok(None); + } + + let filter_value = self.eval_expr( + value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?; + let mut matching_orders = Vec::new(); + for order_row_id in order_user_keys.row_ids_for_value(&filter_value)? { + let Some(order_row) = order_source.row_by_id(order_row_id)? else { + continue; + }; + let Some(order_id) = order_row + .values() + .get(order_id_index) + .and_then(value_as_int64) + else { + return Ok(None); + }; + matching_orders.push(( + order_row_id, + order_id, + order_row.values()[order_total_amount_index].clone(), + )); + } + matching_orders.sort_by(|(_, left_id, _), (_, right_id, _)| right_id.cmp(left_id)); + + let mut column_names = Vec::with_capacity(select.projection.len()); + for (index, item) in select.projection.iter().enumerate() { + match item { + SelectItem::Expr { expr, alias } => { + column_names.push( + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + ); + } + SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => { + return Err(DbError::internal( + "internal: history fast path expects explicit projection expressions", + )); + } + } + } + + let mut rows = Vec::new(); + for (_order_row_id, order_id, order_total_amount) in matching_orders { + let order_id_value = Value::Int64(order_id); + let payment_row_ids = payment_order_keys.row_ids_for_value(&order_id_value)?; + if payment_row_ids.is_empty() { + continue; + } + let order_item_row_ids = order_item_order_keys.row_ids_for_value(&order_id_value)?; + if order_item_row_ids.is_empty() { + continue; + } + + for payment_row_id in payment_row_ids { + let Some(payment_row) = payment_source.row_by_id(payment_row_id)? else { + continue; + }; + let Some(payment_status) = payment_row.values().get(payment_status_index) else { + return Ok(None); + }; + + for order_item_row_id in &order_item_row_ids { + let Some(order_item_row) = order_item_source.row_by_id(*order_item_row_id)? + else { + continue; + }; + let Some(item_id_value) = order_item_row.values().get(order_item_item_id_index) + else { + return Ok(None); + }; + let item_row_ids = item_id_keys.row_ids_for_value(item_id_value)?; + if item_row_ids.is_empty() { + continue; + } + for item_row_id in item_row_ids { + let Some(item_row) = item_source.row_by_id(item_row_id)? else { + continue; + }; + let Some(item_name_value) = item_row.values().get(item_name_index) else { + return Ok(None); + }; + let Some(quantity_value) = + order_item_row.values().get(order_item_quantity_index) + else { + return Ok(None); + }; + let Some(price_value) = order_item_row.values().get(order_item_price_index) + else { + return Ok(None); + }; + + rows.push(QueryRow::new(vec![ + Value::Int64(order_id), + order_total_amount.clone(), + payment_status.clone(), + item_name_value.clone(), + quantity_value.clone(), + price_value.clone(), + ])); + } + } + } + } + + Ok(Some(QueryResult::with_rows(column_names, rows))) + } + pub(crate) fn try_execute_benchmark_report_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + if !query.ctes.is_empty() + || query.offset.is_some() + || query.order_by.len() != 1 + || query.limit.is_none() + { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.from.len() != 1 + || select.filter.is_none() + || select.having.is_some() + || select.distinct + || !select.distinct_on.is_empty() + || select.group_by.len() != 2 + || select.projection.len() != 3 + { + return Ok(None); + } + + let Some(filter) = select.filter.as_ref() else { + return Ok(None); + }; + let Some((filter_table, filter_column, value_expr)) = simple_btree_lookup(filter) else { + return Ok(None); + }; + + let FromItem::Join { + left: item_order_items, + right: order_item, + kind: JoinKind::Inner, + constraint: JoinConstraint::On(order_join_on), + } = &select.from[0] + else { + return Ok(None); + }; + let FromItem::Table { + name: order_name, + alias: order_alias, + } = &**order_item + else { + return Ok(None); + }; + let FromItem::Join { + left: item_item, + right: order_item_item, + kind: JoinKind::Inner, + constraint: JoinConstraint::On(order_item_join_on), + } = &**item_order_items + else { + return Ok(None); + }; + let FromItem::Table { + name: item_name, + alias: item_alias, + } = &**item_item + else { + return Ok(None); + }; + let FromItem::Table { + name: order_item_name, + alias: order_item_alias, + } = &**order_item_item + else { + return Ok(None); + }; + + let item_binding = TableBindingRef { + name: item_name, + alias: item_alias, + }; + let order_item_binding = TableBindingRef { + name: order_item_name, + alias: order_item_alias, + }; + let order_binding = TableBindingRef { + name: order_name, + alias: order_alias, + }; + + if self + .visible_view(item_name, NameResolutionScope::Session) + .is_some() + || self + .visible_view(order_item_name, NameResolutionScope::Session) + .is_some() + || self + .visible_view(order_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(item_name) + || self.visible_table_is_temporary(order_item_name) + || self.visible_table_is_temporary(order_name) + { + return Ok(None); + } + + if !matches_filter_binding(order_name, order_alias, filter_table) + || !identifiers_equal(filter_column, "status") + || !join_constraint_matches_columns( + order_item_join_on, + item_binding, + "id", + order_item_binding, + "item_id", + ) + || !join_constraint_matches_columns( + order_join_on, + order_item_binding, + "order_id", + order_binding, + "id", + ) + { + return Ok(None); + } + + let SelectItem::Expr { + expr: item_name_expr, + alias: item_name_alias, + } = &select.projection[0] + else { + return Ok(None); + }; + if !expr_matches_binding_column(item_name_expr, item_binding, "name") { + return Ok(None); + } + + let SelectItem::Expr { + expr: quantity_sum_expr, + alias: quantity_sum_alias, + } = &select.projection[1] + else { + return Ok(None); + }; + if !aggregate_matches_single_binding_column( + quantity_sum_expr, + "sum", + order_item_binding, + "quantity", + ) { + return Ok(None); + } + + let SelectItem::Expr { + expr: revenue_expr, + alias: revenue_alias, + } = &select.projection[2] + else { + return Ok(None); + }; + if !aggregate_matches_binding_product( + revenue_expr, + "sum", + order_item_binding, + "quantity", + order_item_binding, + "price", + ) { + return Ok(None); + } + if !order_by_matches_alias_or_projection( + &query.order_by[0], + revenue_alias.as_deref(), + revenue_expr, + true, + ) { + return Ok(None); + } + + if select.group_by.len() != 2 + || !expr_matches_binding_column(&select.group_by[0], item_binding, "id") + || !expr_matches_binding_column(&select.group_by[1], item_binding, "name") + { + return Ok(None); + } + + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(usize::MAX); + + let item_schema = match self.table_schema(item_name) { + Some(table) => table, + None => return Ok(None), + }; + let order_item_schema = match self.table_schema(order_item_name) { + Some(table) => table, + None => return Ok(None), + }; + let order_schema = match self.table_schema(order_name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(item_schema) + || !generated_columns_are_stored(order_item_schema) + || !generated_columns_are_stored(order_schema) + { + return Ok(None); + } + + let Some(item_source) = self.visible_table_row_source(item_name) else { + return Ok(None); + }; + let Some(order_item_source) = self.visible_table_row_source(order_item_name) else { + return Ok(None); + }; + let Some(order_source) = self.visible_table_row_source(order_name) else { + return Ok(None); + }; + + let Some(order_status_keys) = self.single_column_btree_keys(order_name, "status") else { + return Ok(None); + }; + let Some(order_item_order_keys) = + self.single_column_btree_keys(order_item_name, "order_id") + else { + return Ok(None); + }; + let Some(item_id_keys) = self.single_column_btree_keys(item_name, "id") else { + return Ok(None); + }; + + let Some(order_id_index) = schema_column_index(order_schema, "id") else { + return Ok(None); + }; + let Some(order_item_item_id_index) = schema_column_index(order_item_schema, "item_id") + else { + return Ok(None); + }; + let Some(order_item_quantity_index) = schema_column_index(order_item_schema, "quantity") + else { + return Ok(None); + }; + let Some(order_item_price_index) = schema_column_index(order_item_schema, "price") else { + return Ok(None); + }; + let Some(item_name_index) = schema_column_index(item_schema, "name") else { + return Ok(None); + }; + + if order_schema.columns[order_id_index].column_type != crate::catalog::ColumnType::Int64 + || order_item_schema.columns[order_item_item_id_index].column_type + != crate::catalog::ColumnType::Int64 + || order_item_schema.columns[order_item_quantity_index].column_type + != crate::catalog::ColumnType::Int64 + || item_schema.columns[item_name_index].column_type != crate::catalog::ColumnType::Text + { + return Ok(None); + } + + let filter_value = self.eval_expr( + value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?; + let matching_order_row_ids = order_status_keys.row_ids_for_value(&filter_value)?; + let mut aggregates = BTreeMap::::new(); + for order_row_id in matching_order_row_ids { + let Some(order_row) = order_source.row_by_id(order_row_id)? else { + continue; + }; + let Some(order_id) = order_row + .values() + .get(order_id_index) + .and_then(value_as_int64) + else { + return Ok(None); + }; + let order_id_value = Value::Int64(order_id); + let order_item_row_ids = order_item_order_keys.row_ids_for_value(&order_id_value)?; + for order_item_row_id in order_item_row_ids { + let Some(order_item_row) = order_item_source.row_by_id(order_item_row_id)? else { + continue; + }; + let Some(item_id) = order_item_row + .values() + .get(order_item_item_id_index) + .and_then(value_as_int64) + else { + return Ok(None); + }; + let Some(quantity) = order_item_row + .values() + .get(order_item_quantity_index) + .and_then(value_as_int64) + else { + return Ok(None); + }; + let Some(price) = order_item_row + .values() + .get(order_item_price_index) + .and_then(value_as_f64) + else { + return Ok(None); + }; + + let item_row_ids = item_id_keys.row_ids_for_value(&Value::Int64(item_id))?; + if item_row_ids.is_empty() { + continue; + } + for item_row_id in item_row_ids { + let Some(item_row) = item_source.row_by_id(item_row_id)? else { + continue; + }; + let Some(item_name_value) = item_row.values().get(item_name_index) else { + return Ok(None); + }; + let Some(item_name_text) = value_as_text(item_name_value) else { + return Ok(None); + }; + let aggregate = aggregates.entry(item_id).or_insert_with(|| { + BenchmarkReportAggregate::new(item_name_text.to_string()) + }); + aggregate.quantity_total += quantity; + aggregate.revenue_total += quantity as f64 * price; + } + } + } + + let column_names = vec![ + item_name_alias + .clone() + .unwrap_or_else(|| infer_expr_name(item_name_expr, 1)), + quantity_sum_alias + .clone() + .unwrap_or_else(|| infer_expr_name(quantity_sum_expr, 2)), + revenue_alias + .clone() + .unwrap_or_else(|| infer_expr_name(revenue_expr, 3)), + ]; + + let mut rows = aggregates + .into_values() + .map(|aggregate| { + QueryRow::new(vec![ + Value::Text(aggregate.item_name), + Value::Int64(aggregate.quantity_total), + Value::Float64(aggregate.revenue_total), + ]) + }) + .collect::>(); + rows.sort_by(|left, right| { + let revenue_ordering = compare_values(&left.values()[2], &right.values()[2]) + .unwrap_or(std::cmp::Ordering::Equal) + .reverse(); + if revenue_ordering != std::cmp::Ordering::Equal { + return revenue_ordering; + } + compare_values(&left.values()[0], &right.values()[0]) + .unwrap_or(std::cmp::Ordering::Equal) + }); + if rows.len() > limit { + rows.truncate(limit); + } + Ok(Some(QueryResult::with_rows(column_names, rows))) + } + pub(crate) fn try_execute_crm_revenue_raw_aggregate_query( + &self, + query: &Query, + ) -> Result> { + if !Self::is_crm_revenue_raw_aggregate_query(query) { + return Ok(None); + } + + let Some(companies_schema) = self.table_schema("companies") else { + return Ok(None); + }; + let Some(users_schema) = self.table_schema("users") else { + return Ok(None); + }; + let Some(invoices_schema) = self.table_schema("invoices") else { + return Ok(None); + }; + let Some(companies_id_index) = crm_column_index(companies_schema, "id", ColumnType::Int64) + else { + return Ok(None); + }; + let Some(companies_name_index) = + crm_column_index(companies_schema, "name", ColumnType::Text) + else { + return Ok(None); + }; + let Some(users_id_index) = crm_column_index(users_schema, "id", ColumnType::Int64) else { + return Ok(None); + }; + let Some(users_company_id_index) = + crm_column_index(users_schema, "company_id", ColumnType::Int64) + else { + return Ok(None); + }; + let Some(invoices_company_id_index) = + crm_column_index(invoices_schema, "company_id", ColumnType::Int64) + else { + return Ok(None); + }; + let Some(invoices_total_index) = + crm_column_index(invoices_schema, "total", ColumnType::Float64) + else { + return Ok(None); + }; + + let Some(companies_source) = self.visible_table_row_source("companies") else { + return Ok(None); + }; + let mut company_names = BTreeMap::new(); + for row in companies_source.rows() { + let row = row?; + let Some(company_id) = + crm_i64_cell(row.values().get(companies_id_index), "companies", "id")? + else { + continue; + }; + let Some(company_name) = + crm_text_cell(row.values().get(companies_name_index), "companies", "name")? + else { + continue; + }; + company_names.insert(company_id, company_name); + } + + let Some(users_source) = self.visible_table_row_source("users") else { + return Ok(None); + }; + let mut counted_company_users = BTreeSet::new(); + let mut user_counts = BTreeMap::new(); + for row in users_source.rows() { + let row = row?; + let Some(user_id) = crm_i64_cell(row.values().get(users_id_index), "users", "id")? + else { + continue; + }; + let Some(company_id) = crm_i64_cell( + row.values().get(users_company_id_index), + "users", + "company_id", + )? + else { + continue; + }; + if company_names.contains_key(&company_id) + && counted_company_users.insert((company_id, user_id)) + { + *user_counts.entry(company_id).or_insert(0_i64) += 1; + } + } + + let revenues = + if let Some(revenues) = self.crm_revenue_from_company_covering_index(&company_names)? { + revenues + } else { + let Some(invoices_source) = self.visible_table_row_source("invoices") else { + return Ok(None); + }; + crm_revenue_from_invoice_rows( + invoices_source, + invoices_company_id_index, + invoices_total_index, + &company_names, + )? + }; + + let mut rows = Vec::with_capacity(company_names.len()); + for (company_id, company_name) in company_names { + let revenue = revenues.get(&company_id).copied().unwrap_or(0.0); + let revenue_value = revenues + .get(&company_id) + .copied() + .map(Value::Float64) + .unwrap_or(Value::Int64(0)); + rows.push(( + revenue, + QueryRow::new(vec![ + Value::Text(company_name), + Value::Int64(user_counts.get(&company_id).copied().unwrap_or(0)), + revenue_value, + ]), + )); + } + rows.sort_by(|left, right| { + right + .0 + .partial_cmp(&left.0) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + Ok(Some(QueryResult::with_rows( + vec![ + "name".to_string(), + "user_count".to_string(), + "revenue".to_string(), + ], + rows.into_iter().map(|(_, row)| row).collect(), + ))) + } + fn crm_revenue_from_company_covering_index( + &self, + company_names: &BTreeMap, + ) -> Result>> { + let Some(RuntimeIndex::Btree { + keys, + covering: Some(covering), + }) = self.index("idx_invoices_company_revenue") + else { + return Ok(None); + }; + let Some(company_id_offset) = covering.column_position("company_id") else { + return Ok(None); + }; + let Some(total_offset) = covering.column_position("total") else { + return Ok(None); + }; + let deleted = match keys { + RuntimeBtreeKeys::UniqueEncoded(_, deleted) + | RuntimeBtreeKeys::NonUniqueEncoded(_, deleted) + | RuntimeBtreeKeys::UniqueInt64(_, deleted) + | RuntimeBtreeKeys::NonUniqueInt64(_, deleted) + | RuntimeBtreeKeys::UniqueUuid(_, deleted) + | RuntimeBtreeKeys::NonUniqueUuid(_, deleted) => deleted, + }; + + if let Some(revenues) = crm_revenue_from_covering_dense( + covering, + company_id_offset, + total_offset, + deleted, + company_names, + )? { + Ok(Some(revenues)) + } else { + crm_revenue_from_covering_sparse( + covering, + company_id_offset, + total_offset, + deleted, + company_names, + ) + .map(Some) + } + } + fn is_crm_revenue_raw_aggregate_query(query: &Query) -> bool { + if query.recursive + || !query.ctes.is_empty() + || query.limit.is_some() + || query.offset.is_some() + || !Self::is_crm_revenue_order_by(&query.order_by) + { + return false; + } + + let QueryBody::Select(select) = &query.body else { + return false; + }; + if select.filter.is_some() + || select.having.is_some() + || select.distinct + || !select.distinct_on.is_empty() + || select.group_by.len() != 2 + || !crm_column(&select.group_by[0], &["c", "companies"], "id") + || !crm_column(&select.group_by[1], &["c", "companies"], "name") + { + return false; + } + + let [from] = &select.from[..] else { + return false; + }; + if !Self::is_crm_revenue_raw_aggregate_from(from) { + return false; + } + + let [SelectItem::Expr { + expr: name_expr, + alias: name_alias, + }, SelectItem::Expr { + expr: user_count_expr, + alias: user_count_alias, + }, SelectItem::Expr { + expr: revenue_expr, + alias: revenue_alias, + }] = &select.projection[..] + else { + return false; + }; + + name_alias.is_none() + && user_count_alias + .as_deref() + .is_some_and(|alias| identifiers_equal(alias, "user_count")) + && revenue_alias + .as_deref() + .is_some_and(|alias| identifiers_equal(alias, "revenue")) + && crm_column(name_expr, &["c", "companies"], "name") + && crm_count_distinct_users(user_count_expr) + && crm_coalesced_invoice_total_sum(revenue_expr) + } + fn is_crm_revenue_order_by(order_by: &[OrderBy]) -> bool { + let [order] = order_by else { + return false; + }; + let Expr::Column { table, column } = &order.expr else { + return false; + }; + order.descending + && order.collation.is_none() + && table.is_none() + && identifiers_equal(column, "revenue") + } + fn is_crm_revenue_raw_aggregate_from(item: &FromItem) -> bool { + let FromItem::Join { + left, + right, + kind, + constraint, + } = item + else { + return false; + }; + + *kind == JoinKind::Left + && Self::is_crm_companies_users_left_join(left) + && crm_table(right, "invoices", "i") + && crm_join_on_columns( + constraint, + &["i", "invoices"], + "user_id", + &["u", "users"], + "id", + ) + } + fn is_crm_companies_users_left_join(item: &FromItem) -> bool { + let FromItem::Join { + left, + right, + kind, + constraint, + } = item + else { + return false; + }; + + *kind == JoinKind::Left + && crm_table(left, "companies", "c") + && crm_table(right, "users", "u") + && crm_join_on_columns( + constraint, + &["u", "users"], + "company_id", + &["c", "companies"], + "id", + ) + } + pub(crate) fn try_indexed_join( + &self, + select: &Select, + params: &[Value], + ctes: &BTreeMap, + ) -> Result> { + let Some(filter) = &select.filter else { + return Ok(None); + }; + if select.from.len() != 1 { + return Ok(None); + } + let FromItem::Join { + left, + right, + kind: JoinKind::Inner, + constraint: JoinConstraint::On(on), + } = &select.from[0] + else { + return Ok(None); + }; + let (left_name, left_alias) = match &**left { + FromItem::Table { name, alias } => (name, alias), + _ => return Ok(None), + }; + let (right_name, right_alias) = match &**right { + FromItem::Table { name, alias } => (name, alias), + _ => return Ok(None), + }; + if ctes.contains_key(left_name) + || ctes.contains_key(right_name) + || self + .visible_view(left_name, NameResolutionScope::Session) + .is_some() + || self + .visible_view(right_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(left_name) + || self.visible_table_is_temporary(right_name) + { + return Ok(None); + } + + let Some((filter_table, filter_column, value_expr)) = simple_btree_lookup(filter) else { + return Ok(None); + }; + let Some(join_equalities) = simple_join_equalities(on) else { + return Ok(None); + }; + + let left_binding = TableBindingRef { + name: left_name, + alias: left_alias, + }; + let right_binding = TableBindingRef { + name: right_name, + alias: right_alias, + }; + + if matches_table_binding(left_binding, filter_table) { + let Some((filtered_join_columns, probe_join_columns)) = + orient_join_equalities(&join_equalities, left_binding, right_binding) + else { + return Ok(None); + }; + let Some(left_dataset) = self.indexed_table_lookup( + left_name, + left_alias, + filter_column, + value_expr, + params, + ctes, + )? + else { + return Ok(None); + }; + return self.indexed_inner_join_filtered(IndexedJoinPlan { + filtered_table: left_binding, + filtered_dataset: &left_dataset, + filtered_join_columns, + probe_table: right_binding, + probe_join_columns, + filtered_on_left: true, + }); + } + + if matches_table_binding(right_binding, filter_table) { + let Some((filtered_join_columns, probe_join_columns)) = + orient_join_equalities(&join_equalities, right_binding, left_binding) + else { + return Ok(None); + }; + let Some(right_dataset) = self.indexed_table_lookup( + right_name, + right_alias, + filter_column, + value_expr, + params, + ctes, + )? + else { + return Ok(None); + }; + return self.indexed_inner_join_filtered(IndexedJoinPlan { + filtered_table: right_binding, + filtered_dataset: &right_dataset, + filtered_join_columns, + probe_table: left_binding, + probe_join_columns, + filtered_on_left: false, + }); + } + + Ok(None) + } +} diff --git a/crates/decentdb/src/exec/codec.rs b/crates/decentdb/src/exec/codec.rs new file mode 100644 index 00000000..6b6e5186 --- /dev/null +++ b/crates/decentdb/src/exec/codec.rs @@ -0,0 +1,1920 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +pub(crate) fn decode_root_header(page_bytes: &[u8]) -> Result> { + if page_bytes.iter().all(|byte| *byte == 0) { + return Ok(None); + } + if page_bytes.len() < ENGINE_ROOT_HEADER_SIZE { + return Err(DbError::corruption("catalog root page is truncated")); + } + if page_bytes[0..ENGINE_ROOT_MAGIC.len()] != ENGINE_ROOT_MAGIC { + return Err(DbError::corruption("catalog root page magic is invalid")); + } + let version = u32::from_le_bytes(page_bytes[8..12].try_into().expect("version")); + if version != ENGINE_ROOT_VERSION { + return Err(DbError::corruption(format!( + "unsupported catalog root version {version}" + ))); + } + Ok(Some(RootHeader { + schema_cookie: u32::from_le_bytes(page_bytes[12..16].try_into().expect("cookie")), + payload_checksum: u32::from_le_bytes(page_bytes[16..20].try_into().expect("checksum")), + pointer: OverflowPointer { + head_page_id: u32::from_le_bytes(page_bytes[20..24].try_into().expect("head page")), + logical_len: u32::from_le_bytes(page_bytes[24..28].try_into().expect("logical len")), + flags: page_bytes[28], + }, + })) +} + +pub(crate) fn encode_root_header(page_size: u32, header: RootHeader) -> Vec { + let mut page = vec![0_u8; page_size as usize]; + page[0..8].copy_from_slice(&ENGINE_ROOT_MAGIC); + page[8..12].copy_from_slice(&ENGINE_ROOT_VERSION.to_le_bytes()); + page[12..16].copy_from_slice(&header.schema_cookie.to_le_bytes()); + page[16..20].copy_from_slice(&header.payload_checksum.to_le_bytes()); + page[20..24].copy_from_slice(&header.pointer.head_page_id.to_le_bytes()); + page[24..28].copy_from_slice(&header.pointer.logical_len.to_le_bytes()); + page[28] = header.pointer.flags; + page +} + +#[cfg(test)] +pub(crate) fn encode_runtime_payload(runtime: &EngineRuntime) -> Result> { + let mut output = Vec::new(); + output.extend_from_slice(LEGACY_RUNTIME_PAYLOAD_MAGIC); + encode_u32(&mut output, runtime.catalog.schema_cookie); + encode_u32(&mut output, runtime.catalog.tables.len() as u32); + for table in runtime.catalog.tables.values() { + encode_string(&mut output, &table.name)?; + encode_u32(&mut output, table.columns.len() as u32); + for column in &table.columns { + encode_string(&mut output, &column.name)?; + output.push(encode_column_type(column.column_type)); + output.push(u8::from(column.nullable)); + encode_optional_string(&mut output, column.default_sql.as_deref())?; + output.push(u8::from(column.primary_key)); + output.push(u8::from(column.unique)); + output.push(u8::from(column.auto_increment)); + encode_u32(&mut output, column.checks.len() as u32); + for check in &column.checks { + encode_optional_string(&mut output, check.name.as_deref())?; + encode_string(&mut output, &check.expression_sql)?; + } + output.push(u8::from(column.foreign_key.is_some())); + if let Some(foreign_key) = &column.foreign_key { + encode_foreign_key(&mut output, foreign_key)?; + } + } + encode_u32(&mut output, table.checks.len() as u32); + for check in &table.checks { + encode_optional_string(&mut output, check.name.as_deref())?; + encode_string(&mut output, &check.expression_sql)?; + } + encode_u32(&mut output, table.foreign_keys.len() as u32); + for foreign_key in &table.foreign_keys { + encode_foreign_key(&mut output, foreign_key)?; + } + encode_strings(&mut output, &table.primary_key_columns)?; + encode_i64(&mut output, table.next_row_id); + let data = runtime + .tables + .get(&table.name) + .map(|source| source.resident_data().clone()) + .unwrap_or_default(); + encode_u32(&mut output, data.row_count() as u32); + for row in data.visible_rows() { + encode_i64(&mut output, row.row_id); + let encoded = Row::new(row.values.clone()).encode()?; + encode_bytes(&mut output, &encoded)?; + } + } + + encode_u32(&mut output, runtime.catalog.indexes.len() as u32); + for index in runtime.catalog.indexes.values() { + encode_string(&mut output, &index.name)?; + encode_string(&mut output, &index.table_name)?; + output.push(index.kind as u8); + output.push(u8::from(index.unique)); + encode_u32(&mut output, index.columns.len() as u32); + for column in &index.columns { + encode_optional_string(&mut output, column.column_name.as_deref())?; + encode_optional_string(&mut output, column.expression_sql.as_deref())?; + } + encode_optional_string(&mut output, index.predicate_sql.as_deref())?; + output.push(u8::from(index.fresh)); + } + encode_u32(&mut output, runtime.catalog.views.len() as u32); + for view in runtime.catalog.views.values() { + encode_string(&mut output, &view.name)?; + encode_string(&mut output, &view.sql_text)?; + encode_strings(&mut output, &view.column_names)?; + encode_strings(&mut output, &view.dependencies)?; + } + + encode_u32(&mut output, runtime.catalog.triggers.len() as u32); + for trigger in runtime.catalog.triggers.values() { + encode_string(&mut output, &trigger.name)?; + encode_string(&mut output, &trigger.target_name)?; + output.push(trigger.kind as u8); + output.push(trigger.event as u8); + output.push(u8::from(trigger.on_view)); + encode_string(&mut output, &trigger.action_sql)?; + } + encode_schemas_section(&mut output, &runtime.catalog.schemas)?; + encode_index_include_columns_section(&mut output, &runtime.catalog.indexes)?; + encode_full_text_options_section(&mut output, &runtime.catalog.indexes)?; + encode_generated_columns_section(&mut output, &runtime.catalog.tables)?; + encode_spatial_columns_section(&mut output, &runtime.catalog.tables)?; + encode_enum_columns_section(&mut output, &runtime.catalog.tables)?; + Ok(output) +} + +pub(crate) fn decode_runtime_payload(bytes: &[u8]) -> Result { + let mut cursor = Cursor::new(bytes); + let magic = cursor.read_slice(9)?; + if magic != LEGACY_RUNTIME_PAYLOAD_MAGIC { + return Err(DbError::corruption("catalog state magic is invalid")); + } + let mut runtime = EngineRuntime::empty(cursor.read_u32()?); + let table_count = cursor.read_u32()?; + for _ in 0..table_count { + let table_name = cursor.read_string()?; + let column_count = cursor.read_u32()?; + let mut table = TableSchema { + name: table_name.clone(), + temporary: false, + columns: Vec::with_capacity(column_count as usize), + checks: Vec::new(), + foreign_keys: Vec::new(), + primary_key_columns: Vec::new(), + next_row_id: 1, + pk_index_root: None, + }; + for _ in 0..column_count { + let name = cursor.read_string()?; + let column_type = decode_column_type(cursor.read_u8()?)?; + let nullable = cursor.read_bool()?; + let default_sql = cursor.read_optional_string()?; + let primary_key = cursor.read_bool()?; + let unique = cursor.read_bool()?; + let auto_increment = cursor.read_bool()?; + let check_count = cursor.read_u32()?; + let mut checks = Vec::with_capacity(check_count as usize); + for _ in 0..check_count { + checks.push(crate::catalog::CheckConstraint { + name: cursor.read_optional_string()?, + expression_sql: cursor.read_string()?, + }); + } + let has_fk = cursor.read_bool()?; + let foreign_key = if has_fk { + Some(decode_foreign_key(&mut cursor)?) + } else { + None + }; + table.columns.push(crate::catalog::ColumnSchema { + name, + column_type, + spatial_type: None, + enum_type: None, + nullable, + default_sql, + generated_sql: None, + generated_stored: true, + primary_key, + unique, + auto_increment, + checks, + foreign_key, + }); + } + let table_check_count = cursor.read_u32()?; + for _ in 0..table_check_count { + table.checks.push(crate::catalog::CheckConstraint { + name: cursor.read_optional_string()?, + expression_sql: cursor.read_string()?, + }); + } + let fk_count = cursor.read_u32()?; + for _ in 0..fk_count { + table.foreign_keys.push(decode_foreign_key(&mut cursor)?); + } + table.primary_key_columns = cursor.read_strings()?; + table.next_row_id = cursor.read_i64()?; + let row_count = cursor.read_u32()?; + let mut data = TableData::default(); + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let row_bytes_len = cursor.read_u32()? as usize; + let row_bytes = cursor.read_slice(row_bytes_len)?; + let row = Row::decode(row_bytes)?; + data.push_row(StoredRow { + row_id, + values: row.into_values(), + }); + } + runtime + .catalog_mut() + .tables + .insert(table_name.clone(), table); + runtime.tables_mut().insert(table_name, data.into()); + } + + let index_count = cursor.read_u32()?; + for _ in 0..index_count { + let name = cursor.read_string()?; + let table_name = cursor.read_string()?; + let kind = decode_index_kind(cursor.read_u8()?)?; + let unique = cursor.read_bool()?; + let column_count = cursor.read_u32()?; + let mut columns = Vec::with_capacity(column_count as usize); + for _ in 0..column_count { + columns.push(crate::catalog::IndexColumn { + column_name: cursor.read_optional_string()?, + expression_sql: cursor.read_optional_string()?, + }); + } + let predicate_sql = cursor.read_optional_string()?; + let fresh = cursor.read_bool()?; + runtime.catalog_mut().indexes.insert( + name.clone(), + crate::catalog::IndexSchema { + name, + table_name, + kind, + unique, + columns, + include_columns: Vec::new(), + predicate_sql, + full_text: None, + fresh, + }, + ); + } + let view_count = cursor.read_u32()?; + for _ in 0..view_count { + let view = crate::catalog::ViewSchema { + name: cursor.read_string()?, + temporary: false, + sql_text: cursor.read_string()?, + column_names: cursor.read_strings()?, + dependencies: cursor.read_strings()?, + }; + runtime.catalog_mut().views.insert(view.name.clone(), view); + } + + let trigger_count = cursor.read_u32()?; + for _ in 0..trigger_count { + let trigger = crate::catalog::TriggerSchema { + name: cursor.read_string()?, + target_name: cursor.read_string()?, + kind: decode_trigger_kind(cursor.read_u8()?)?, + event: decode_trigger_event(cursor.read_u8()?)?, + on_view: cursor.read_bool()?, + action_sql: cursor.read_string()?, + }; + runtime + .catalog_mut() + .triggers + .insert(trigger.name.clone(), trigger); + } + if cursor.offset < cursor.bytes.len() { + decode_schemas_section(&mut cursor, &mut runtime.catalog_mut().schemas)?; + } + if cursor.offset < cursor.bytes.len() { + decode_index_include_columns_section(&mut cursor, &mut runtime.catalog_mut().indexes)?; + } + if cursor.offset < cursor.bytes.len() { + decode_full_text_options_section(&mut cursor, &mut runtime.catalog_mut().indexes)?; + } + if cursor.offset < cursor.bytes.len() { + decode_generated_columns_section(&mut cursor, &mut runtime.catalog_mut().tables)?; + } + if cursor.offset < cursor.bytes.len() { + decode_spatial_columns_section(&mut cursor, &mut runtime.catalog_mut().tables)?; + } + if cursor.offset < cursor.bytes.len() { + decode_enum_columns_section(&mut cursor, &mut runtime.catalog_mut().tables)?; + } + if cursor.offset < cursor.bytes.len() { + decode_pk_index_roots_section(&mut cursor, &mut runtime.catalog_mut().tables)?; + } + Ok(runtime) +} + +#[cfg(test)] +pub(crate) fn encode_manifest_payload( + runtime: &EngineRuntime, + table_states: &BTreeMap, +) -> Result> { + Ok(encode_manifest_payload_with_offsets(runtime, table_states)?.bytes) +} + +pub(crate) fn encode_manifest_payload_with_offsets( + runtime: &EngineRuntime, + table_states: &BTreeMap, +) -> Result { + let mut output = Vec::new(); + let mut table_next_row_id_offsets = BTreeMap::new(); + let mut table_state_offsets = BTreeMap::new(); + let mut table_pk_index_root_offsets = BTreeMap::new(); + output.extend_from_slice(MANIFEST_PAYLOAD_MAGIC); + encode_u32(&mut output, runtime.catalog.schema_cookie); + encode_u32(&mut output, runtime.catalog.tables.len() as u32); + for table in runtime.catalog.tables.values() { + encode_string(&mut output, &table.name)?; + encode_u32(&mut output, table.columns.len() as u32); + for column in &table.columns { + encode_string(&mut output, &column.name)?; + output.push(encode_column_type(column.column_type)); + output.push(u8::from(column.nullable)); + encode_optional_string(&mut output, column.default_sql.as_deref())?; + output.push(u8::from(column.primary_key)); + output.push(u8::from(column.unique)); + output.push(u8::from(column.auto_increment)); + encode_u32(&mut output, column.checks.len() as u32); + for check in &column.checks { + encode_optional_string(&mut output, check.name.as_deref())?; + encode_string(&mut output, &check.expression_sql)?; + } + output.push(u8::from(column.foreign_key.is_some())); + if let Some(foreign_key) = &column.foreign_key { + encode_foreign_key(&mut output, foreign_key)?; + } + } + encode_u32(&mut output, table.checks.len() as u32); + for check in &table.checks { + encode_optional_string(&mut output, check.name.as_deref())?; + encode_string(&mut output, &check.expression_sql)?; + } + encode_u32(&mut output, table.foreign_keys.len() as u32); + for foreign_key in &table.foreign_keys { + encode_foreign_key(&mut output, foreign_key)?; + } + encode_strings(&mut output, &table.primary_key_columns)?; + table_next_row_id_offsets.insert(table.name.clone(), output.len()); + encode_i64(&mut output, table.next_row_id); + table_state_offsets.insert(table.name.clone(), output.len()); + let state = table_states.get(&table.name).copied().unwrap_or_default(); + encode_u32(&mut output, state.checksum); + encode_u32(&mut output, state.pointer.head_page_id); + encode_u32(&mut output, state.pointer.logical_len); + output.push(state.pointer.flags); + } + + encode_u32(&mut output, runtime.catalog.indexes.len() as u32); + for index in runtime.catalog.indexes.values() { + encode_string(&mut output, &index.name)?; + encode_string(&mut output, &index.table_name)?; + output.push(index.kind as u8); + output.push(u8::from(index.unique)); + encode_u32(&mut output, index.columns.len() as u32); + for column in &index.columns { + encode_optional_string(&mut output, column.column_name.as_deref())?; + encode_optional_string(&mut output, column.expression_sql.as_deref())?; + } + encode_optional_string(&mut output, index.predicate_sql.as_deref())?; + output.push(u8::from(index.fresh)); + } + encode_u32(&mut output, runtime.catalog.views.len() as u32); + for view in runtime.catalog.views.values() { + encode_string(&mut output, &view.name)?; + encode_string(&mut output, &view.sql_text)?; + encode_strings(&mut output, &view.column_names)?; + encode_strings(&mut output, &view.dependencies)?; + } + + encode_u32(&mut output, runtime.catalog.triggers.len() as u32); + for trigger in runtime.catalog.triggers.values() { + encode_string(&mut output, &trigger.name)?; + encode_string(&mut output, &trigger.target_name)?; + output.push(trigger.kind as u8); + output.push(trigger.event as u8); + output.push(u8::from(trigger.on_view)); + encode_string(&mut output, &trigger.action_sql)?; + } + + let table_stats = runtime + .catalog + .table_stats + .iter() + .filter(|(name, _)| runtime.catalog.tables.contains_key(*name)) + .collect::>(); + encode_u32(&mut output, table_stats.len() as u32); + for (name, stats) in table_stats { + encode_string(&mut output, name)?; + encode_i64(&mut output, stats.row_count); + } + + let index_stats = runtime + .catalog + .index_stats + .iter() + .filter(|(name, _)| runtime.catalog.indexes.contains_key(*name)) + .collect::>(); + encode_u32(&mut output, index_stats.len() as u32); + for (name, stats) in index_stats { + encode_string(&mut output, name)?; + encode_i64(&mut output, stats.entry_count); + encode_i64(&mut output, stats.distinct_key_count); + } + encode_schemas_section(&mut output, &runtime.catalog.schemas)?; + encode_index_include_columns_section(&mut output, &runtime.catalog.indexes)?; + encode_full_text_options_section(&mut output, &runtime.catalog.indexes)?; + encode_generated_columns_section(&mut output, &runtime.catalog.tables)?; + encode_spatial_columns_section(&mut output, &runtime.catalog.tables)?; + encode_enum_columns_section(&mut output, &runtime.catalog.tables)?; + encode_pk_index_roots_section( + &mut output, + &runtime.catalog.tables, + Some(&mut table_pk_index_root_offsets), + )?; + Ok(ManifestEncoding { + bytes: output, + table_next_row_id_offsets, + table_state_offsets, + table_pk_index_root_offsets, + }) +} + +pub(crate) fn decode_manifest_payload( + _store: &S, + bytes: &[u8], +) -> Result { + let mut cursor = Cursor::new(bytes); + let magic = cursor.read_slice(MANIFEST_PAYLOAD_MAGIC.len())?; + if magic != MANIFEST_PAYLOAD_MAGIC { + return Err(DbError::corruption("catalog manifest magic is invalid")); + } + let mut runtime = EngineRuntime::empty(cursor.read_u32()?); + let table_count = cursor.read_u32()?; + for _ in 0..table_count { + let table_name = cursor.read_string()?; + let column_count = cursor.read_u32()?; + let mut table = TableSchema { + name: table_name.clone(), + temporary: false, + columns: Vec::with_capacity(column_count as usize), + checks: Vec::new(), + foreign_keys: Vec::new(), + primary_key_columns: Vec::new(), + next_row_id: 1, + pk_index_root: None, + }; + for _ in 0..column_count { + let name = cursor.read_string()?; + let column_type = decode_column_type(cursor.read_u8()?)?; + let nullable = cursor.read_bool()?; + let default_sql = cursor.read_optional_string()?; + let primary_key = cursor.read_bool()?; + let unique = cursor.read_bool()?; + let auto_increment = cursor.read_bool()?; + let check_count = cursor.read_u32()?; + let mut checks = Vec::with_capacity(check_count as usize); + for _ in 0..check_count { + checks.push(crate::catalog::CheckConstraint { + name: cursor.read_optional_string()?, + expression_sql: cursor.read_string()?, + }); + } + let has_fk = cursor.read_bool()?; + let foreign_key = if has_fk { + Some(decode_foreign_key(&mut cursor)?) + } else { + None + }; + table.columns.push(crate::catalog::ColumnSchema { + name, + column_type, + spatial_type: None, + enum_type: None, + nullable, + default_sql, + generated_sql: None, + generated_stored: true, + primary_key, + unique, + auto_increment, + checks, + foreign_key, + }); + } + let table_check_count = cursor.read_u32()?; + for _ in 0..table_check_count { + table.checks.push(crate::catalog::CheckConstraint { + name: cursor.read_optional_string()?, + expression_sql: cursor.read_string()?, + }); + } + let fk_count = cursor.read_u32()?; + for _ in 0..fk_count { + table.foreign_keys.push(decode_foreign_key(&mut cursor)?); + } + table.primary_key_columns = cursor.read_strings()?; + table.next_row_id = cursor.read_i64()?; + let state = PersistedTableState { + checksum: cursor.read_u32()?, + pointer: OverflowPointer { + head_page_id: cursor.read_u32()?, + logical_len: cursor.read_u32()?, + flags: cursor.read_u8()?, + }, + row_count: 0, + tail: OverflowTailInfo::default(), + pk_index_root: None, + }; + runtime + .catalog_mut() + .tables + .insert(table_name.clone(), table); + let has_data = state.pointer.head_page_id != 0 && state.pointer.logical_len != 0; + if has_data { + // Defer row data loading to first statement execution. + runtime.deferred_tables_mut().insert(table_name.clone()); + } + runtime + .persisted_tables_mut() + .insert(table_name.clone(), state); + if !has_data { + // Empty tables are immediately available. + runtime + .tables_mut() + .insert(table_name, TableData::default().into()); + } + } + + let index_count = cursor.read_u32()?; + for _ in 0..index_count { + let name = cursor.read_string()?; + let table_name = cursor.read_string()?; + let kind = decode_index_kind(cursor.read_u8()?)?; + let unique = cursor.read_bool()?; + let column_count = cursor.read_u32()?; + let mut columns = Vec::with_capacity(column_count as usize); + for _ in 0..column_count { + columns.push(crate::catalog::IndexColumn { + column_name: cursor.read_optional_string()?, + expression_sql: cursor.read_optional_string()?, + }); + } + let predicate_sql = cursor.read_optional_string()?; + let fresh = cursor.read_bool()?; + runtime.catalog_mut().indexes.insert( + name.clone(), + crate::catalog::IndexSchema { + name, + table_name, + kind, + unique, + columns, + include_columns: Vec::new(), + predicate_sql, + full_text: None, + fresh, + }, + ); + } + let view_count = cursor.read_u32()?; + for _ in 0..view_count { + let view = crate::catalog::ViewSchema { + name: cursor.read_string()?, + temporary: false, + sql_text: cursor.read_string()?, + column_names: cursor.read_strings()?, + dependencies: cursor.read_strings()?, + }; + runtime.catalog_mut().views.insert(view.name.clone(), view); + } + + let trigger_count = cursor.read_u32()?; + for _ in 0..trigger_count { + let trigger = crate::catalog::TriggerSchema { + name: cursor.read_string()?, + target_name: cursor.read_string()?, + kind: decode_trigger_kind(cursor.read_u8()?)?, + event: decode_trigger_event(cursor.read_u8()?)?, + on_view: cursor.read_bool()?, + action_sql: cursor.read_string()?, + }; + runtime + .catalog_mut() + .triggers + .insert(trigger.name.clone(), trigger); + } + if cursor.offset < cursor.bytes.len() { + let table_stats_count = cursor.read_u32()?; + for _ in 0..table_stats_count { + let name = cursor.read_string()?; + let stats = crate::catalog::TableStats { + row_count: cursor.read_i64()?, + }; + if let Some(state) = runtime.persisted_tables_mut().get_mut(&name) { + state.row_count = usize::try_from(stats.row_count.max(0)).unwrap_or(usize::MAX); + } + runtime.catalog_mut().table_stats.insert(name, stats); + } + } + if cursor.offset < cursor.bytes.len() { + let index_stats_count = cursor.read_u32()?; + for _ in 0..index_stats_count { + let name = cursor.read_string()?; + let stats = crate::catalog::IndexStats { + entry_count: cursor.read_i64()?, + distinct_key_count: cursor.read_i64()?, + }; + runtime.catalog_mut().index_stats.insert(name, stats); + } + } + if cursor.offset < cursor.bytes.len() { + decode_schemas_section(&mut cursor, &mut runtime.catalog_mut().schemas)?; + } + if cursor.offset < cursor.bytes.len() { + decode_index_include_columns_section(&mut cursor, &mut runtime.catalog_mut().indexes)?; + } + if cursor.offset < cursor.bytes.len() { + decode_full_text_options_section(&mut cursor, &mut runtime.catalog_mut().indexes)?; + } + if cursor.offset < cursor.bytes.len() { + decode_generated_columns_section(&mut cursor, &mut runtime.catalog_mut().tables)?; + } + if cursor.offset < cursor.bytes.len() { + decode_spatial_columns_section(&mut cursor, &mut runtime.catalog_mut().tables)?; + } + if cursor.offset < cursor.bytes.len() { + decode_enum_columns_section(&mut cursor, &mut runtime.catalog_mut().tables)?; + } + if cursor.offset < cursor.bytes.len() { + decode_pk_index_roots_section(&mut cursor, &mut runtime.catalog_mut().tables)?; + } + let table_pk_roots = runtime + .catalog + .tables + .iter() + .map(|(table_name, table)| (table_name.clone(), table.pk_index_root)) + .collect::>(); + for (table_name, pk_index_root) in table_pk_roots { + if let Some(state) = runtime.persisted_tables_mut().get_mut(&table_name) { + state.pk_index_root = pk_index_root; + } + } + Ok(runtime) +} + +pub(crate) fn encode_table_payload(data: &TableData) -> Result> { + encode_table_payload_with_tombstone_locators(data).map(|(payload, _)| payload) +} + +pub(crate) fn encode_table_payload_with_tombstone_locators( + data: &TableData, +) -> Result<(Vec, Int64Map)> { + let row_count = data.row_count(); + if row_count == 0 { + return Ok(( + Vec::new(), + Int64Map::with_hasher(Int64HashBuilder::default()), + )); + } + let mut output = Vec::with_capacity(TABLE_PAYLOAD_MAGIC.len() + 4 + row_count * 32); + let mut locators = Int64Map::with_capacity_and_hasher(row_count, Int64HashBuilder::default()); + output.extend_from_slice(TABLE_PAYLOAD_MAGIC); + encode_u32(&mut output, row_count as u32); + let mut encoded_row = Vec::with_capacity(64); + for row in data.visible_rows() { + encode_i64(&mut output, row.row_id); + Row::encode_values_into(&row.values, &mut encoded_row)?; + let row_body_len = encoded_row + .len() + .saturating_add(TABLE_PAYLOAD_ROW_BODY_PADDING_BYTES); + encode_u32( + &mut output, + u32::try_from(row_body_len) + .ok() + .filter(|len| *len < TABLE_PAYLOAD_ROW_TOMBSTONE_FLAG) + .ok_or_else(|| DbError::constraint("table row body length exceeds u32"))?, + ); + locators.insert( + row.row_id, + u32::try_from(output.len().saturating_sub(4)) + .map_err(|_| DbError::constraint("resident tombstone locator exceeds u32"))?, + ); + output.extend_from_slice(&encoded_row); + output.extend(std::iter::repeat_n( + 0u8, + row_body_len.saturating_sub(encoded_row.len()), + )); + } + Ok((output, locators)) +} + +pub(crate) fn encode_paged_table_chunks_from_rows( + rows: &[StoredRow], + page_size: u32, +) -> Result> { + if rows.is_empty() { + return Ok(Vec::new()); + } + + let target_chunk_bytes = paged_table_target_chunk_bytes(page_size); + let mut chunks = Vec::new(); + let mut chunk = Vec::with_capacity(target_chunk_bytes); + chunk.extend_from_slice(TABLE_PAYLOAD_MAGIC); + chunk.extend_from_slice(&0_u32.to_le_bytes()); + let mut chunk_row_count = 0usize; + let mut encoded_row = Vec::with_capacity(64); + + for row in rows { + encoded_row.clear(); + Row::encode_values_into(&row.values, &mut encoded_row)?; + let encoded_row_len = 8usize.saturating_add(4).saturating_add(encoded_row.len()); + if chunk_row_count > 0 && chunk.len().saturating_add(encoded_row_len) > target_chunk_bytes { + chunks.push(finalize_encoded_paged_table_chunk(chunk, chunk_row_count)?); + chunk = Vec::with_capacity(target_chunk_bytes); + chunk.extend_from_slice(TABLE_PAYLOAD_MAGIC); + chunk.extend_from_slice(&0_u32.to_le_bytes()); + chunk_row_count = 0; + } + encode_i64(&mut chunk, row.row_id); + encode_bytes(&mut chunk, &encoded_row)?; + chunk_row_count += 1; + } + + if chunk_row_count > 0 { + chunks.push(finalize_encoded_paged_table_chunk(chunk, chunk_row_count)?); + } + Ok(chunks) +} + +pub(crate) fn encode_paged_table_chunks( + data: &TableData, + page_size: u32, +) -> Result> { + if !data.has_tombstoned_rows() { + return encode_paged_table_chunks_from_rows(&data.rows, page_size); + } + let rows = data.visible_rows().cloned().collect::>(); + encode_paged_table_chunks_from_rows(&rows, page_size) +} + +pub(crate) fn encode_paged_table_manifest_payload( + manifest: &PersistedPagedTableManifest, +) -> Result> { + let mut output = Vec::with_capacity( + TABLE_PAGED_MANIFEST_MAGIC.len() + 4 + manifest.chunks.len().saturating_mul(30), + ); + output.extend_from_slice(TABLE_PAGED_MANIFEST_MAGIC); + encode_u32( + &mut output, + u32::try_from(manifest.chunks.len()) + .map_err(|_| DbError::constraint("paged table chunk count exceeds u32"))?, + ); + for chunk in &manifest.chunks { + encode_u32(&mut output, chunk.checksum); + encode_u32(&mut output, chunk.pointer.head_page_id); + encode_u32(&mut output, chunk.pointer.logical_len); + output.push(chunk.pointer.flags); + encode_u32( + &mut output, + u32::try_from(chunk.row_count) + .map_err(|_| DbError::constraint("paged table chunk row count exceeds u32"))?, + ); + encode_u32( + &mut output, + u32::try_from(chunk.tombstoned_row_ids.len()).map_err(|_| { + DbError::constraint("paged table chunk tombstone count exceeds u32") + })?, + ); + for row_id in &chunk.tombstoned_row_ids { + encode_i64(&mut output, *row_id); + } + output.push(if chunk.overlay_pointer.is_some() { + 1 + } else { + 0 + }); + if let Some(overlay_pointer) = chunk.overlay_pointer { + encode_u32(&mut output, overlay_pointer.head_page_id); + encode_u32(&mut output, overlay_pointer.logical_len); + output.push(overlay_pointer.flags); + encode_u32( + &mut output, + chunk.overlay_checksum.ok_or_else(|| { + DbError::internal("paged table chunk overlay checksum missing") + })?, + ); + } + } + Ok(output) +} + +pub(crate) fn decode_paged_table_manifest_payload( + bytes: &[u8], +) -> Result { + let mut cursor = Cursor::new(bytes); + let magic = cursor.read_slice(TABLE_PAGED_MANIFEST_MAGIC.len())?; + if magic != TABLE_PAGED_MANIFEST_MAGIC { + return Err(DbError::corruption("paged table manifest magic is invalid")); + } + let chunk_count = cursor.read_u32()? as usize; + let mut chunks = Vec::with_capacity(chunk_count); + for _ in 0..chunk_count { + let checksum = cursor.read_u32()?; + let pointer = OverflowPointer { + head_page_id: cursor.read_u32()?, + logical_len: cursor.read_u32()?, + flags: cursor.read_u8()?, + }; + let row_count = cursor.read_u32()? as usize; + let tombstoned_row_ids_len = cursor.read_u32()? as usize; + let mut tombstoned_row_ids = Vec::with_capacity(tombstoned_row_ids_len); + for _ in 0..tombstoned_row_ids_len { + tombstoned_row_ids.push(cursor.read_i64()?); + } + let has_overlay = cursor.read_bool()?; + let mut overlay_pointer = None; + let mut overlay_checksum = None; + if has_overlay { + overlay_pointer = Some(OverflowPointer { + head_page_id: cursor.read_u32()?, + logical_len: cursor.read_u32()?, + flags: cursor.read_u8()?, + }); + overlay_checksum = Some(cursor.read_u32()?); + } + chunks.push(PersistedTableChunkState { + checksum, + pointer, + row_count, + tombstoned_row_ids, + overlay_pointer, + overlay_checksum, + }); + } + if cursor.offset != cursor.bytes.len() { + return Err(DbError::corruption( + "paged table manifest payload had trailing bytes", + )); + } + Ok(PersistedPagedTableManifest { chunks }) +} + +pub(crate) fn decode_table_payload_rows(bytes: &[u8]) -> Result> { + let mut rows = Vec::new(); + visit_table_payload_rows_from_bytes(bytes, &mut |row_id, values| { + rows.push(StoredRow { + row_id, + values: values.to_vec(), + }); + Ok(()) + })?; + Ok(rows) +} + +pub(crate) fn encode_legacy_table_payload_from_manifest( + manifest: &TablePageManifest, +) -> Result> { + if manifest.row_count() == 0 { + return Ok(Vec::new()); + } + let mut rows = Vec::with_capacity(manifest.row_count()); + for row in manifest.rows() { + let row = row?; + rows.push(StoredRow { + row_id: row.row_id(), + values: row.values().to_vec(), + }); + } + encode_table_payload(&TableData::from_rows(rows)) +} + +pub(crate) fn decode_persisted_table_data( + store: &S, + state: PersistedTableState, +) -> Result { + let manifest = read_table_page_manifest_from_state(store, state)?; + let mut rows = Vec::with_capacity(manifest.row_count()); + for row in manifest.rows() { + let row = row?; + rows.push(StoredRow { + row_id: row.row_id(), + values: row.values().to_vec(), + }); + } + Ok(TableData::from_rows(rows)) +} + +pub(crate) fn encode_appended_table_rows( + data: &TableData, + existing_count: usize, +) -> Result> { + if existing_count > data.rows.len() { + return Err(DbError::internal( + "append-only table payload rewrite saw fewer rows than the previous persisted payload", + )); + } + if existing_count == data.rows.len() { + return Ok(Vec::new()); + } + + let mut appended = Vec::with_capacity((data.rows.len() - existing_count) * 32); + let mut encoded_row = Vec::with_capacity(64); + for row in data.rows.iter().skip(existing_count) { + encode_i64(&mut appended, row.row_id); + Row::encode_values_into(&row.values, &mut encoded_row)?; + let row_body_len = encoded_row + .len() + .saturating_add(TABLE_PAYLOAD_ROW_BODY_PADDING_BYTES); + encode_u32( + &mut appended, + u32::try_from(row_body_len) + .map_err(|_| DbError::constraint("table row body length exceeds u32"))?, + ); + appended.extend_from_slice(&encoded_row); + appended.extend(std::iter::repeat_n( + 0u8, + row_body_len.saturating_sub(encoded_row.len()), + )); + encoded_row.clear(); + } + Ok(appended) +} + +#[cfg(test)] +pub(crate) fn decode_table_payload(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Ok(TableData::default()); + } + let mut cursor = Cursor::new(bytes); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + let mut data = TableData::default(); + data.reserve_rows(row_count); + let mut slots = 0usize; + while cursor.offset < cursor.bytes.len() { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes = cursor.read_slice(row_bytes_len)?; + slots += 1; + if is_tombstone { + continue; + } + let row = Row::decode(row_bytes)?; + data.push_row(StoredRow { + row_id, + values: row.into_values(), + }); + } + if slots < row_count { + return Err(DbError::corruption( + "table payload row count exceeded decoded row content", + )); + } + Ok(data) +} + +pub(crate) fn encode_u32(output: &mut Vec, value: u32) { + output.extend_from_slice(&value.to_le_bytes()); +} + +pub(crate) fn encode_u64(output: &mut Vec, value: u64) { + output.extend_from_slice(&value.to_le_bytes()); +} + +pub(crate) fn encode_i64(output: &mut Vec, value: i64) { + output.extend_from_slice(&value.to_le_bytes()); +} + +pub(crate) fn encode_string(output: &mut Vec, value: &str) -> Result<()> { + encode_u32( + output, + u32::try_from(value.len()).map_err(|_| DbError::constraint("string length exceeds u32"))?, + ); + output.extend_from_slice(value.as_bytes()); + Ok(()) +} + +pub(crate) fn encode_optional_string(output: &mut Vec, value: Option<&str>) -> Result<()> { + output.push(u8::from(value.is_some())); + if let Some(value) = value { + encode_string(output, value)?; + } + Ok(()) +} + +pub(crate) fn encode_strings(output: &mut Vec, values: &[String]) -> Result<()> { + encode_u32( + output, + u32::try_from(values.len()) + .map_err(|_| DbError::constraint("string list length exceeds u32"))?, + ); + for value in values { + encode_string(output, value)?; + } + Ok(()) +} + +pub(crate) fn encode_bytes(output: &mut Vec, bytes: &[u8]) -> Result<()> { + encode_u32( + output, + u32::try_from(bytes.len()) + .map_err(|_| DbError::constraint("byte vector length exceeds u32"))?, + ); + output.extend_from_slice(bytes); + Ok(()) +} + +pub(crate) fn encode_foreign_key( + output: &mut Vec, + foreign_key: &crate::catalog::ForeignKeyConstraint, +) -> Result<()> { + encode_optional_string(output, foreign_key.name.as_deref())?; + encode_strings(output, &foreign_key.columns)?; + encode_string(output, &foreign_key.referenced_table)?; + encode_strings(output, &foreign_key.referenced_columns)?; + output.push(foreign_key.on_delete as u8); + output.push(foreign_key.on_update as u8); + Ok(()) +} + +pub(crate) fn encode_generated_columns_section( + output: &mut Vec, + tables: &BTreeMap, +) -> Result<()> { + let generated_columns = tables + .values() + .flat_map(|table| { + table.columns.iter().filter_map(move |column| { + column.generated_sql.as_ref().map(|generated_sql| { + ( + table.name.as_str(), + column.name.as_str(), + generated_sql.as_str(), + column.generated_stored, + ) + }) + }) + }) + .collect::>(); + output.extend_from_slice(GENERATED_COLUMNS_SECTION_MAGIC); + output.push(1); + encode_u32( + output, + u32::try_from(generated_columns.len()) + .map_err(|_| DbError::constraint("generated column count exceeds u32"))?, + ); + for (table_name, column_name, generated_sql, generated_stored) in generated_columns { + encode_string(output, table_name)?; + encode_string(output, column_name)?; + encode_string(output, generated_sql)?; + output.push(u8::from(generated_stored)); + } + Ok(()) +} + +pub(crate) fn encode_spatial_columns_section( + output: &mut Vec, + tables: &BTreeMap, +) -> Result<()> { + let spatial_columns = tables + .values() + .flat_map(|table| { + table.columns.iter().filter_map(move |column| { + column + .spatial_type + .map(|spatial_type| (table.name.as_str(), column.name.as_str(), spatial_type)) + }) + }) + .collect::>(); + output.extend_from_slice(SPATIAL_COLUMNS_SECTION_MAGIC); + output.push(1); + encode_u32( + output, + u32::try_from(spatial_columns.len()) + .map_err(|_| DbError::constraint("spatial column count exceeds u32"))?, + ); + for (table_name, column_name, spatial_type) in spatial_columns { + encode_string(output, table_name)?; + encode_string(output, column_name)?; + output.push(encode_spatial_subtype_tag(spatial_type.subtype)); + output.push(encode_spatial_dimensions_tag(spatial_type.dimensions)); + let srid = u32::try_from(spatial_type.srid) + .map_err(|_| DbError::constraint("spatial SRID must be non-negative"))?; + encode_u32(output, srid); + } + Ok(()) +} + +pub(crate) fn encode_enum_columns_section( + output: &mut Vec, + tables: &BTreeMap, +) -> Result<()> { + let enum_columns = tables + .values() + .flat_map(|table| { + table.columns.iter().filter_map(move |column| { + column + .enum_type + .as_ref() + .map(|enum_type| (table.name.as_str(), column.name.as_str(), enum_type)) + }) + }) + .collect::>(); + output.extend_from_slice(ENUM_COLUMNS_SECTION_MAGIC); + output.push(1); + encode_u32( + output, + u32::try_from(enum_columns.len()) + .map_err(|_| DbError::constraint("enum column count exceeds u32"))?, + ); + for (table_name, column_name, enum_type) in enum_columns { + encode_string(output, table_name)?; + encode_string(output, column_name)?; + encode_u64(output, enum_type.type_id); + encode_u32( + output, + u32::try_from(enum_type.labels.len()) + .map_err(|_| DbError::constraint("enum label count exceeds u32"))?, + ); + for label in &enum_type.labels { + encode_u64(output, label.id); + encode_string(output, &label.label)?; + } + } + Ok(()) +} + +pub(crate) fn encode_index_include_columns_section( + output: &mut Vec, + indexes: &BTreeMap, +) -> Result<()> { + let include_entries = indexes + .iter() + .filter(|(_, index)| !index.include_columns.is_empty()) + .collect::>(); + output.extend_from_slice(INDEX_INCLUDE_COLUMNS_SECTION_MAGIC); + output.push(1); + encode_u32( + output, + u32::try_from(include_entries.len()) + .map_err(|_| DbError::constraint("index include entry count exceeds u32"))?, + ); + for (index_name, index) in include_entries { + encode_string(output, index_name)?; + encode_strings(output, &index.include_columns)?; + } + Ok(()) +} + +pub(crate) fn encode_full_text_options_section( + output: &mut Vec, + indexes: &BTreeMap, +) -> Result<()> { + let entries = indexes + .iter() + .filter_map(|(name, index)| index.full_text.as_ref().map(|config| (name, config))) + .collect::>(); + output.extend_from_slice(FULL_TEXT_OPTIONS_SECTION_MAGIC); + output.push(1); + encode_u32( + output, + u32::try_from(entries.len()) + .map_err(|_| DbError::constraint("fulltext option entry count exceeds u32"))?, + ); + for (index_name, config) in entries { + encode_string(output, index_name)?; + let bytes = config + .to_json() + .map_err(|error| DbError::internal(error.message))?; + encode_bytes(output, &bytes)?; + } + Ok(()) +} + +pub(crate) fn encode_schemas_section( + output: &mut Vec, + schemas: &BTreeMap, +) -> Result<()> { + output.extend_from_slice(SCHEMAS_SECTION_MAGIC); + output.push(1); + encode_u32( + output, + u32::try_from(schemas.len()) + .map_err(|_| DbError::constraint("schema count exceeds u32"))?, + ); + for schema in schemas.values() { + encode_string(output, &schema.name)?; + } + Ok(()) +} + +pub(crate) fn encode_pk_index_roots_section( + output: &mut Vec, + tables: &BTreeMap, + mut offsets: Option<&mut BTreeMap>, +) -> Result<()> { + output.extend_from_slice(PK_INDEX_ROOTS_SECTION_MAGIC); + output.push(1); + encode_u32( + output, + u32::try_from(tables.len()) + .map_err(|_| DbError::constraint("pk index root entry count exceeds u32"))?, + ); + for table in tables.values() { + encode_string(output, &table.name)?; + if let Some(offsets) = offsets.as_deref_mut() { + offsets.insert(table.name.clone(), output.len()); + } + encode_u32(output, table.pk_index_root.unwrap_or(0)); + } + Ok(()) +} + +pub(crate) fn decode_schemas_section( + cursor: &mut Cursor<'_>, + schemas: &mut BTreeMap, +) -> Result<()> { + let section_is_present = cursor + .bytes + .get(cursor.offset..cursor.offset + SCHEMAS_SECTION_MAGIC.len()) + .is_some_and(|magic| magic == SCHEMAS_SECTION_MAGIC); + if !section_is_present { + return Ok(()); + } + cursor.offset += SCHEMAS_SECTION_MAGIC.len(); + let version = cursor.read_u8()?; + if version != 1 { + return Err(DbError::corruption(format!( + "unknown schemas section version {version}" + ))); + } + let schema_count = cursor.read_u32()?; + for _ in 0..schema_count { + let name = cursor.read_string()?; + schemas.insert(name.clone(), SchemaInfo { name }); + } + Ok(()) +} + +pub(crate) fn decode_index_include_columns_section( + cursor: &mut Cursor<'_>, + indexes: &mut BTreeMap, +) -> Result<()> { + let section_is_present = cursor + .bytes + .get(cursor.offset..cursor.offset + INDEX_INCLUDE_COLUMNS_SECTION_MAGIC.len()) + .is_some_and(|magic| magic == INDEX_INCLUDE_COLUMNS_SECTION_MAGIC); + if !section_is_present { + return Ok(()); + } + cursor.offset += INDEX_INCLUDE_COLUMNS_SECTION_MAGIC.len(); + let version = cursor.read_u8()?; + if version != 1 { + return Err(DbError::corruption(format!( + "unknown index include columns section version {version}" + ))); + } + let entry_count = cursor.read_u32()?; + for _ in 0..entry_count { + let index_name = cursor.read_string()?; + let include_columns = cursor.read_strings()?; + let index = indexes.get_mut(&index_name).ok_or_else(|| { + DbError::corruption(format!( + "index include metadata referenced unknown index {index_name}" + )) + })?; + index.include_columns = include_columns; + } + Ok(()) +} + +pub(crate) fn decode_full_text_options_section( + cursor: &mut Cursor<'_>, + indexes: &mut BTreeMap, +) -> Result<()> { + let section_is_present = cursor + .bytes + .get(cursor.offset..cursor.offset + FULL_TEXT_OPTIONS_SECTION_MAGIC.len()) + .is_some_and(|magic| magic == FULL_TEXT_OPTIONS_SECTION_MAGIC); + if !section_is_present { + return Ok(()); + } + cursor.offset += FULL_TEXT_OPTIONS_SECTION_MAGIC.len(); + let version = cursor.read_u8()?; + if version != 1 { + return Err(DbError::corruption(format!( + "unknown fulltext options section version {version}" + ))); + } + let entry_count = cursor.read_u32()?; + for _ in 0..entry_count { + let index_name = cursor.read_string()?; + let config_bytes_len = cursor.read_u32()? as usize; + let config_bytes = cursor.read_slice(config_bytes_len)?; + let config = AnalyzerConfig::from_json(config_bytes) + .map_err(|error| DbError::corruption(error.message))?; + let index = indexes.get_mut(&index_name).ok_or_else(|| { + DbError::corruption(format!( + "fulltext options metadata referenced unknown index {index_name}" + )) + })?; + if index.kind != IndexKind::FullText { + return Err(DbError::corruption(format!( + "fulltext options metadata referenced non-fulltext index {index_name}" + ))); + } + index.full_text = Some(config); + } + Ok(()) +} + +pub(crate) fn decode_generated_columns_section( + cursor: &mut Cursor<'_>, + tables: &mut BTreeMap, +) -> Result<()> { + let section_is_versioned = cursor + .bytes + .get(cursor.offset..cursor.offset + GENERATED_COLUMNS_SECTION_MAGIC.len()) + .is_some_and(|magic| magic == GENERATED_COLUMNS_SECTION_MAGIC); + if section_is_versioned { + cursor.offset += GENERATED_COLUMNS_SECTION_MAGIC.len(); + let version = cursor.read_u8()?; + if version != 1 { + return Err(DbError::corruption(format!( + "unknown generated columns section version {version}" + ))); + } + } + let generated_column_count = cursor.read_u32()?; + for _ in 0..generated_column_count { + let table_name = cursor.read_string()?; + let column_name = cursor.read_string()?; + let generated_sql = cursor.read_string()?; + let generated_stored = if section_is_versioned { + cursor.read_bool()? + } else { + true + }; + let table = tables.get_mut(&table_name).ok_or_else(|| { + DbError::corruption(format!( + "generated column metadata referenced unknown table {table_name}" + )) + })?; + let column = table + .columns + .iter_mut() + .find(|column| identifiers_equal(&column.name, &column_name)) + .ok_or_else(|| { + DbError::corruption(format!( + "generated column metadata referenced unknown column {}.{}", + table_name, column_name + )) + })?; + column.generated_sql = Some(generated_sql); + column.generated_stored = generated_stored; + } + Ok(()) +} + +pub(crate) fn decode_spatial_columns_section( + cursor: &mut Cursor<'_>, + tables: &mut BTreeMap, +) -> Result<()> { + let section_is_present = cursor + .bytes + .get(cursor.offset..cursor.offset + SPATIAL_COLUMNS_SECTION_MAGIC.len()) + .is_some_and(|magic| magic == SPATIAL_COLUMNS_SECTION_MAGIC); + if !section_is_present { + return Ok(()); + } + cursor.offset += SPATIAL_COLUMNS_SECTION_MAGIC.len(); + let version = cursor.read_u8()?; + if version != 1 { + return Err(DbError::corruption(format!( + "unknown spatial columns section version {version}" + ))); + } + let entry_count = cursor.read_u32()?; + for _ in 0..entry_count { + let table_name = cursor.read_string()?; + let column_name = cursor.read_string()?; + let subtype = decode_spatial_subtype_tag(cursor.read_u8()?)?; + let dimensions = decode_spatial_dimensions_tag(cursor.read_u8()?)?; + let srid = i32::try_from(cursor.read_u32()?) + .map_err(|_| DbError::corruption("spatial SRID exceeds i32"))?; + let table = tables.get_mut(&table_name).ok_or_else(|| { + DbError::corruption(format!( + "spatial column metadata referenced unknown table {table_name}" + )) + })?; + let column = table + .columns + .iter_mut() + .find(|column| identifiers_equal(&column.name, &column_name)) + .ok_or_else(|| { + DbError::corruption(format!( + "spatial column metadata referenced unknown column {}.{}", + table_name, column_name + )) + })?; + column.spatial_type = Some(crate::catalog::SpatialTypeInfo { + subtype, + dimensions, + srid, + }); + } + Ok(()) +} + +pub(crate) fn decode_enum_columns_section( + cursor: &mut Cursor<'_>, + tables: &mut BTreeMap, +) -> Result<()> { + let section_is_present = cursor + .bytes + .get(cursor.offset..cursor.offset + ENUM_COLUMNS_SECTION_MAGIC.len()) + .is_some_and(|magic| magic == ENUM_COLUMNS_SECTION_MAGIC); + if !section_is_present { + return Ok(()); + } + cursor.offset += ENUM_COLUMNS_SECTION_MAGIC.len(); + let version = cursor.read_u8()?; + if version != 1 { + return Err(DbError::corruption(format!( + "unknown enum columns section version {version}" + ))); + } + let entry_count = cursor.read_u32()?; + for _ in 0..entry_count { + let table_name = cursor.read_string()?; + let column_name = cursor.read_string()?; + let type_id = cursor.read_u64()?; + let label_count = cursor.read_u32()?; + let mut labels = Vec::with_capacity(label_count as usize); + for _ in 0..label_count { + labels.push(EnumLabel { + id: cursor.read_u64()?, + label: cursor.read_string()?, + }); + } + let table = tables.get_mut(&table_name).ok_or_else(|| { + DbError::corruption(format!( + "enum column metadata referenced unknown table {table_name}" + )) + })?; + let column = table + .columns + .iter_mut() + .find(|column| identifiers_equal(&column.name, &column_name)) + .ok_or_else(|| { + DbError::corruption(format!( + "enum column metadata referenced unknown column {}.{}", + table_name, column_name + )) + })?; + column.enum_type = Some(EnumTypeInfo { type_id, labels }); + } + Ok(()) +} + +pub(crate) fn decode_pk_index_roots_section( + cursor: &mut Cursor<'_>, + tables: &mut BTreeMap, +) -> Result<()> { + let section_is_present = cursor + .bytes + .get(cursor.offset..cursor.offset + PK_INDEX_ROOTS_SECTION_MAGIC.len()) + .is_some_and(|magic| magic == PK_INDEX_ROOTS_SECTION_MAGIC); + if !section_is_present { + return Ok(()); + } + cursor.offset += PK_INDEX_ROOTS_SECTION_MAGIC.len(); + let version = cursor.read_u8()?; + if version != 1 { + return Err(DbError::corruption(format!( + "unknown pk index roots section version {version}" + ))); + } + let entry_count = cursor.read_u32()?; + for _ in 0..entry_count { + let table_name = cursor.read_string()?; + let pk_index_root = match cursor.read_u32()? { + 0 => None, + page_id => Some(page_id), + }; + let table = tables.get_mut(&table_name).ok_or_else(|| { + DbError::corruption(format!( + "pk index root metadata referenced unknown table {table_name}" + )) + })?; + table.pk_index_root = pk_index_root; + } + Ok(()) +} + +pub(crate) fn encode_spatial_subtype_tag(subtype: crate::catalog::SpatialSubtype) -> u8 { + match subtype { + crate::catalog::SpatialSubtype::Any => 0, + crate::catalog::SpatialSubtype::Point => 1, + crate::catalog::SpatialSubtype::LineString => 2, + crate::catalog::SpatialSubtype::Polygon => 3, + crate::catalog::SpatialSubtype::MultiPoint => 4, + crate::catalog::SpatialSubtype::MultiLineString => 5, + crate::catalog::SpatialSubtype::MultiPolygon => 6, + } +} + +pub(crate) fn decode_spatial_subtype_tag(tag: u8) -> Result { + match tag { + 0 => Ok(crate::catalog::SpatialSubtype::Any), + 1 => Ok(crate::catalog::SpatialSubtype::Point), + 2 => Ok(crate::catalog::SpatialSubtype::LineString), + 3 => Ok(crate::catalog::SpatialSubtype::Polygon), + 4 => Ok(crate::catalog::SpatialSubtype::MultiPoint), + 5 => Ok(crate::catalog::SpatialSubtype::MultiLineString), + 6 => Ok(crate::catalog::SpatialSubtype::MultiPolygon), + _ => Err(DbError::corruption("unknown spatial subtype tag")), + } +} + +pub(crate) fn encode_spatial_dimensions_tag(dimensions: crate::catalog::SpatialDimensions) -> u8 { + match dimensions { + crate::catalog::SpatialDimensions::Any => 0, + crate::catalog::SpatialDimensions::Xy => 1, + crate::catalog::SpatialDimensions::Xyz => 2, + crate::catalog::SpatialDimensions::Xym => 3, + crate::catalog::SpatialDimensions::Xyzm => 4, + } +} + +pub(crate) fn decode_spatial_dimensions_tag(tag: u8) -> Result { + match tag { + 0 => Ok(crate::catalog::SpatialDimensions::Any), + 1 => Ok(crate::catalog::SpatialDimensions::Xy), + 2 => Ok(crate::catalog::SpatialDimensions::Xyz), + 3 => Ok(crate::catalog::SpatialDimensions::Xym), + 4 => Ok(crate::catalog::SpatialDimensions::Xyzm), + _ => Err(DbError::corruption("unknown spatial dimensions tag")), + } +} + +pub(crate) fn encode_column_type(column_type: crate::catalog::ColumnType) -> u8 { + match column_type { + crate::catalog::ColumnType::Int64 => 0, + crate::catalog::ColumnType::Float64 => 1, + crate::catalog::ColumnType::Text => 2, + crate::catalog::ColumnType::Bool => 3, + crate::catalog::ColumnType::Blob => 4, + crate::catalog::ColumnType::Decimal => 5, + crate::catalog::ColumnType::Uuid => 6, + crate::catalog::ColumnType::Timestamp => 7, + crate::catalog::ColumnType::Geometry => 8, + crate::catalog::ColumnType::Geography => 9, + crate::catalog::ColumnType::Enum => 10, + crate::catalog::ColumnType::IpAddr => 11, + crate::catalog::ColumnType::Cidr => 12, + crate::catalog::ColumnType::Date => 13, + crate::catalog::ColumnType::Time => 14, + crate::catalog::ColumnType::TimestampTz => 15, + crate::catalog::ColumnType::Interval => 16, + crate::catalog::ColumnType::MacAddr => 17, + } +} + +pub(crate) fn decode_column_type(tag: u8) -> Result { + match tag { + 0 => Ok(crate::catalog::ColumnType::Int64), + 1 => Ok(crate::catalog::ColumnType::Float64), + 2 => Ok(crate::catalog::ColumnType::Text), + 3 => Ok(crate::catalog::ColumnType::Bool), + 4 => Ok(crate::catalog::ColumnType::Blob), + 5 => Ok(crate::catalog::ColumnType::Decimal), + 6 => Ok(crate::catalog::ColumnType::Uuid), + 7 => Ok(crate::catalog::ColumnType::Timestamp), + 8 => Ok(crate::catalog::ColumnType::Geometry), + 9 => Ok(crate::catalog::ColumnType::Geography), + 10 => Ok(crate::catalog::ColumnType::Enum), + 11 => Ok(crate::catalog::ColumnType::IpAddr), + 12 => Ok(crate::catalog::ColumnType::Cidr), + 13 => Ok(crate::catalog::ColumnType::Date), + 14 => Ok(crate::catalog::ColumnType::Time), + 15 => Ok(crate::catalog::ColumnType::TimestampTz), + 16 => Ok(crate::catalog::ColumnType::Interval), + 17 => Ok(crate::catalog::ColumnType::MacAddr), + _ => Err(DbError::corruption("unknown column type tag")), + } +} + +pub(crate) fn decode_index_kind(tag: u8) -> Result { + match tag { + 0 => Ok(crate::catalog::IndexKind::Btree), + 1 => Ok(crate::catalog::IndexKind::Trigram), + 2 => Ok(crate::catalog::IndexKind::Spatial), + 3 => Ok(crate::catalog::IndexKind::FullText), + _ => Err(DbError::corruption("unknown index kind tag")), + } +} + +pub(crate) fn decode_trigger_kind(tag: u8) -> Result { + match tag { + 0 => Ok(crate::catalog::TriggerKind::After), + 1 => Ok(crate::catalog::TriggerKind::InsteadOf), + _ => Err(DbError::corruption("unknown trigger kind tag")), + } +} + +pub(crate) fn decode_trigger_event(tag: u8) -> Result { + match tag { + 0 => Ok(crate::catalog::TriggerEvent::Insert), + 1 => Ok(crate::catalog::TriggerEvent::Update), + 2 => Ok(crate::catalog::TriggerEvent::Delete), + _ => Err(DbError::corruption("unknown trigger event tag")), + } +} + +pub(crate) fn decode_fk_action(tag: u8) -> Result { + match tag { + 0 => Ok(crate::catalog::ForeignKeyAction::NoAction), + 1 => Ok(crate::catalog::ForeignKeyAction::Restrict), + 2 => Ok(crate::catalog::ForeignKeyAction::Cascade), + 3 => Ok(crate::catalog::ForeignKeyAction::SetNull), + _ => Err(DbError::corruption("unknown foreign-key action tag")), + } +} + +pub(crate) fn decode_foreign_key( + cursor: &mut Cursor<'_>, +) -> Result { + Ok(crate::catalog::ForeignKeyConstraint { + name: cursor.read_optional_string()?, + columns: cursor.read_strings()?, + referenced_table: cursor.read_string()?, + referenced_columns: cursor.read_strings()?, + on_delete: decode_fk_action(cursor.read_u8()?)?, + on_update: decode_fk_action(cursor.read_u8()?)?, + }) +} + +pub(crate) fn encode_row_id_locator_key(row_id: i64) -> u64 { + (row_id as u64) ^ SIGNED_ROW_ID_BIAS +} + +pub(crate) fn decode_row_id_locator_key(key: u64) -> i64 { + (key ^ SIGNED_ROW_ID_BIAS) as i64 +} + +pub(crate) fn encode_row_locator(locator: RowLocatorV1) -> Vec { + let mut bytes = Vec::with_capacity(8); + bytes.extend_from_slice(&locator.byte_offset.to_le_bytes()); + bytes.extend_from_slice(&locator.byte_len.to_le_bytes()); + bytes +} + +pub(crate) fn encode_paged_row_locator(locator: RowLocatorV2) -> Vec { + let mut bytes = Vec::with_capacity(13); + bytes.extend_from_slice(&locator.chunk_index.to_le_bytes()); + bytes.extend_from_slice(&locator.byte_offset.to_le_bytes()); + bytes.extend_from_slice(&locator.byte_len.to_le_bytes()); + bytes.push(if locator.is_overlay { 1 } else { 0 }); + bytes +} + +pub(crate) fn decode_row_locator(bytes: &[u8]) -> Result { + match bytes.len() { + 8 => Ok(DecodedRowLocator::V1(RowLocatorV1 { + byte_offset: u32::from_le_bytes(bytes[0..4].try_into().expect("row locator offset")), + byte_len: u32::from_le_bytes(bytes[4..8].try_into().expect("row locator len")), + })), + 12 => Ok(DecodedRowLocator::V2(RowLocatorV2 { + chunk_index: u32::from_le_bytes(bytes[0..4].try_into().expect("row locator chunk")), + byte_offset: u32::from_le_bytes(bytes[4..8].try_into().expect("row locator offset")), + byte_len: u32::from_le_bytes(bytes[8..12].try_into().expect("row locator len")), + is_overlay: false, + })), + 13 => Ok(DecodedRowLocator::V2(RowLocatorV2 { + chunk_index: u32::from_le_bytes(bytes[0..4].try_into().expect("row locator chunk")), + byte_offset: u32::from_le_bytes(bytes[4..8].try_into().expect("row locator offset")), + byte_len: u32::from_le_bytes(bytes[8..12].try_into().expect("row locator len")), + is_overlay: bytes[12] != 0, + })), + _ => Err(DbError::corruption("row locator payload length is invalid")), + } +} + +pub(crate) fn decode_compressed_table_payload_lookup_entry( + payload: Arc>, +) -> Result { + let mut cursor = Cursor::new(payload.as_slice()); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + let mut row_locators = HashMap::with_capacity(row_count); + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes_offset = cursor.offset; + let _ = cursor.read_slice(row_bytes_len)?; + if is_tombstone { + continue; + } + row_locators.insert( + row_id, + RowLocatorV1 { + byte_offset: u32::try_from(row_bytes_offset) + .map_err(|_| DbError::constraint("row locator offset exceeds u32"))?, + byte_len: u32::try_from(row_bytes_len) + .map_err(|_| DbError::constraint("row locator length exceeds u32"))?, + }, + ); + } + Ok(DeferredCompressedLookupCacheEntry { + payload, + row_locators, + }) +} + +pub(crate) fn decode_row_by_locator_from_payload( + payload: &[u8], + row_id: i64, + locator: RowLocatorV1, +) -> Result { + let start = locator.byte_offset as usize; + let end = start + .checked_add(locator.byte_len as usize) + .ok_or_else(|| DbError::corruption("row locator exceeded payload length"))?; + let row_bytes = payload + .get(start..end) + .ok_or_else(|| DbError::corruption("row locator exceeded payload length"))?; + let row = Row::decode(row_bytes)?; + Ok(StoredRow { + row_id, + values: row.into_values(), + }) +} + +pub(crate) fn decode_projected_values_by_locator_from_payload( + store: Option<&S>, + payload: &[u8], + locator: RowLocatorV1, + projection_indexes: &[usize], +) -> Result> { + let start = locator.byte_offset as usize; + let end = start + .checked_add(locator.byte_len as usize) + .ok_or_else(|| DbError::corruption("row locator exceeded payload length"))?; + let row_bytes = payload + .get(start..end) + .ok_or_else(|| DbError::corruption("row locator exceeded payload length"))?; + Row::decode_projection_sorted_unique_with_overflow(row_bytes, store, projection_indexes) +} + +impl EngineRuntime { + pub(crate) fn decode_runtime_index_group_key(key: &[u8]) -> Option { + let (tag, payload) = key.split_first()?; + match *tag { + 0 if payload.is_empty() => Some(Value::Null), + 1 if payload.len() == 1 => match payload[0] { + 0 => Some(Value::Bool(false)), + 1 => Some(Value::Bool(true)), + _ => None, + }, + 2 if payload.len() == 8 => { + let mut bytes = [0_u8; 8]; + bytes.copy_from_slice(payload); + let bits = u64::from_be_bytes(bytes) ^ 0x8000_0000_0000_0000; + Some(Value::Int64(i64::from_be_bytes(bits.to_be_bytes()))) + } + 3 if payload.len() == 8 => { + let mut bytes = [0_u8; 8]; + bytes.copy_from_slice(payload); + let sortable = u64::from_be_bytes(bytes); + let bits = if sortable & (1_u64 << 63) != 0 { + sortable ^ (1_u64 << 63) + } else { + !sortable + }; + Some(Value::Float64(f64::from_bits(bits))) + } + 6 if payload.len() == 16 => { + let mut bytes = [0_u8; 16]; + bytes.copy_from_slice(payload); + Some(Value::Uuid(bytes)) + } + 7 => { + let text = String::from_utf8(payload.to_vec()).ok()?; + Some(Value::Text(text)) + } + 8 => Some(Value::Blob(payload.to_vec())), + 9 if payload.len() == 16 => { + let mut enum_type_id = [0_u8; 8]; + enum_type_id.copy_from_slice(&payload[..8]); + let mut label_id = [0_u8; 8]; + label_id.copy_from_slice(&payload[8..16]); + Some(Value::Enum { + enum_type_id: u64::from_be_bytes(enum_type_id), + label_id: u64::from_be_bytes(label_id), + }) + } + 10 if payload.len() == 17 => { + let family = *payload.last()?; + match family { + 4 => { + let mut addr = [0_u8; 16]; + addr[..4].copy_from_slice(&payload[12..16]); + Some(Value::IpAddr { family, addr }) + } + 6 => { + let mut addr = [0_u8; 16]; + addr.copy_from_slice(&payload[..16]); + Some(Value::IpAddr { family, addr }) + } + _ => None, + } + } + 11 if matches!(payload.len(), 6 | 18) => { + let family = payload[0]; + let prefix_len = payload[1]; + if family != 4 && family != 6 { + return None; + } + let mut network = [0_u8; 16]; + if family == 4 { + if payload.len() != 6 { + return None; + } + network[..4].copy_from_slice(&payload[2..6]); + } else { + network.copy_from_slice(&payload[2..18]); + } + Some(Value::Cidr { + family, + prefix_len, + network, + }) + } + 12 if payload.len() == 4 => { + let mut bytes = [0_u8; 4]; + bytes.copy_from_slice(payload); + let raw = u32::from_be_bytes(bytes) ^ 0x8000_0000; + Some(Value::DateDays(i32::from_be_bytes(raw.to_be_bytes()))) + } + 13 if payload.len() == 8 => { + let mut bytes = [0_u8; 8]; + bytes.copy_from_slice(payload); + let bits = u64::from_be_bytes(bytes) ^ 0x8000_0000_0000_0000; + Some(Value::TimeMicros(i64::from_be_bytes(bits.to_be_bytes()))) + } + 14 if payload.len() == 8 => { + let mut bytes = [0_u8; 8]; + bytes.copy_from_slice(payload); + let bits = u64::from_be_bytes(bytes) ^ 0x8000_0000_0000_0000; + Some(Value::TimestampTzMicros(i64::from_be_bytes( + bits.to_be_bytes(), + ))) + } + 15 if payload.len() == 16 => { + let mut months = [0_u8; 4]; + months.copy_from_slice(&payload[..4]); + let mut days = [0_u8; 4]; + days.copy_from_slice(&payload[4..8]); + let mut micros = [0_u8; 8]; + micros.copy_from_slice(&payload[8..16]); + Some(Value::Interval { + months: { + let raw = u32::from_be_bytes(months) ^ 0x8000_0000; + i32::from_be_bytes(raw.to_be_bytes()) + }, + days: { + let raw = u32::from_be_bytes(days) ^ 0x8000_0000; + i32::from_be_bytes(raw.to_be_bytes()) + }, + micros: { + let bits = u64::from_be_bytes(micros) ^ 0x8000_0000_0000_0000; + i64::from_be_bytes(bits.to_be_bytes()) + }, + }) + } + 16 if !payload.is_empty() => { + let len = *payload.last()?; + if len > 8 { + return None; + } + let len_usize = usize::from(len); + if payload.len() != len_usize + 1 { + return None; + } + let mut bytes = [0_u8; 8]; + bytes[..len_usize].copy_from_slice(&payload[..len_usize]); + Some(Value::MacAddr { len, bytes }) + } + 5 => None, + _ => None, + } + } +} diff --git a/crates/decentdb/src/exec/deferred.rs b/crates/decentdb/src/exec/deferred.rs new file mode 100644 index 00000000..8ab5a89d --- /dev/null +++ b/crates/decentdb/src/exec/deferred.rs @@ -0,0 +1,1218 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +pub(crate) fn compact_paged_table_state_for_checkpoint( + store: &mut S, + state: PersistedTableState, +) -> Result<(PersistedTableState, bool)> { + if state.pointer.head_page_id == 0 || !state.pointer.is_table_paged_manifest() { + return Ok((state, false)); + } + + let manifest_payload = read_overflow(store, state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); + } + let mut manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + let mut changed = false; + let chunk_compaction_min_bytes = paged_table_checkpoint_compaction_min_bytes(store.page_size()); + let mut freed_pointers: Vec = Vec::new(); + for chunk in &mut manifest.chunks { + let needs_merge = !chunk.tombstoned_row_ids.is_empty() || chunk.overlay_pointer.is_some(); + if !needs_merge { + if chunk.pointer.head_page_id == 0 + || chunk.pointer.is_compressed() + || usize::try_from(chunk.pointer.logical_len) + .ok() + .is_none_or(|len| len < chunk_compaction_min_bytes) + { + continue; + } + let payload = read_overflow(store, chunk.pointer)?; + let pointer = rewrite_overflow( + store, + chunk.pointer, + &payload, + CompressionMode::AutoMinBytes(chunk_compaction_min_bytes), + )?; + if pointer != chunk.pointer { + chunk.pointer = pointer; + changed = true; + } + continue; + } + + // Fold tombstones and overlay into a new base payload. + let base_payload = read_overflow(store, chunk.pointer)?; + let base_rows = decode_table_payload_rows(&base_payload)?; + let mut merged_rows: BTreeMap = BTreeMap::new(); + for row in base_rows { + if !chunk.tombstoned_row_ids.contains(&row.row_id) { + merged_rows.insert(row.row_id, row); + } + } + if let Some(overlay_pointer) = chunk.overlay_pointer { + let overlay_payload = read_overflow(store, overlay_pointer)?; + let overlay_rows = decode_table_payload_rows(&overlay_payload)?; + for row in overlay_rows { + merged_rows.insert(row.row_id, row); + } + } + let merged: Vec = merged_rows.into_values().collect(); + let merged_len = merged.len(); + let new_payload = encode_table_payload(&TableData::from_rows(merged))?; + let new_checksum = crc32c_parts(&[new_payload.as_slice()]); + let new_pointer = write_overflow( + store, + &new_payload, + CompressionMode::AutoMinBytes(chunk_compaction_min_bytes), + )?; + if chunk.pointer.head_page_id != 0 { + freed_pointers.push(chunk.pointer); + } + if let Some(overlay_pointer) = chunk.overlay_pointer { + if overlay_pointer.head_page_id != 0 { + freed_pointers.push(overlay_pointer); + } + } + chunk.pointer = new_pointer; + chunk.checksum = new_checksum; + chunk.row_count = merged_len; + chunk.tombstoned_row_ids.clear(); + chunk.overlay_pointer = None; + chunk.overlay_checksum = None; + changed = true; + } + + let should_rewrite_manifest = changed + || (!state.pointer.is_compressed() + && usize::try_from(state.pointer.logical_len) + .ok() + .is_some_and(|len| len >= AUTO_MIN_PAYLOAD_BYTES)); + if !should_rewrite_manifest { + return Ok((state, false)); + } + + let manifest_payload = encode_paged_table_manifest_payload(&manifest)?; + let checksum = crc32c_parts(&[manifest_payload.as_slice()]); + let pointer = rewrite_overflow( + store, + state.pointer.with_table_paged_manifest(false), + &manifest_payload, + CompressionMode::Auto, + )? + .with_table_paged_manifest(true); + let tail = if pointer.is_compressed() { + OverflowTailInfo::default() + } else { + read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default() + }; + let new_state = PersistedTableState { + pointer, + checksum, + row_count: state.row_count, + tail, + pk_index_root: state.pk_index_root, + }; + + // Free old base and overlay pages that were replaced by merge compaction. + // (Non-merge rewrites use rewrite_overflow which reuses/frees old pages + // on its own.) + for old_pointer in freed_pointers { + if old_pointer.head_page_id != 0 { + free_overflow(store, old_pointer.head_page_id)?; + } + } + + Ok((new_state, new_state != state || changed)) +} + +impl EngineRuntime { + pub(crate) fn deferred_tables_mut(&mut self) -> &mut BTreeSet { + Arc::make_mut(&mut self.deferred_tables) + } + pub(crate) fn compact_dirty_resident_storage_after_transaction_commit(&mut self) -> usize { + if self.dirty_tables.is_empty() { + return 0; + } + + let dirty_tables = self.dirty_tables.iter().cloned().collect::>(); + let mut freed = 0usize; + { + let tables = Arc::make_mut(&mut self.tables); + for table_name in &dirty_tables { + if let Some(row_source) = tables.get_mut(table_name) { + freed = freed.saturating_add(row_source.shrink_resident_to_fit_if_unique()); + } + } + } + + let dirty_index_names = self + .catalog + .indexes + .values() + .filter(|index| { + dirty_tables + .iter() + .any(|table_name| identifiers_equal(table_name, &index.table_name)) + }) + .map(|index| index.name.clone()) + .collect::>(); + if !dirty_index_names.is_empty() { + let indexes = Arc::make_mut(&mut self.indexes); + for index_name in dirty_index_names { + if let Some(index) = indexes.get_mut(&index_name).and_then(Arc::get_mut) { + freed = freed.saturating_add(index.shrink_to_fit_if_unique()); + } + } + } + + freed + } + #[cfg(test)] + pub(crate) fn has_deferred_paged_row_locator_cache_for_tests(&self, table_name: &str) -> bool { + self.deferred_paged_row_locator_caches + .contains_key(table_name) + } + pub(crate) fn load_from_storage( + pager: &PagerHandle, + wal: &WalHandle, + schema_cookie: u32, + config: &crate::config::DbConfig, + ) -> Result<(Self, u64)> { + let reader = wal.begin_reader_with_pager(pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + let runtime = + Self::load_from_storage_at_snapshot(pager, wal, schema_cookie, config, snapshot_lsn)?; + drop(reader); + Ok((runtime, snapshot_lsn)) + } + pub(crate) fn load_from_storage_at_snapshot( + pager: &PagerHandle, + wal: &WalHandle, + schema_cookie: u32, + config: &crate::config::DbConfig, + snapshot_lsn: u64, + ) -> Result { + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + let root_page = store.read_page(page::CATALOG_ROOT_PAGE_ID)?; + let root = decode_root_header(&root_page)?; + let mut runtime = if let Some(root) = root { + let payload = if root.pointer.logical_len == 0 || root.pointer.head_page_id == 0 { + Vec::new() + } else { + read_overflow(&store, root.pointer)? + }; + if crc32c_parts(&[payload.as_slice()]) != root.payload_checksum { + return Err(DbError::corruption("catalog state checksum mismatch")); + } + let mut runtime = if payload.is_empty() { + Self::from_config(root.schema_cookie, config) + } else if payload.starts_with(LEGACY_RUNTIME_PAYLOAD_MAGIC) { + let mut runtime = decode_runtime_payload(&payload)?; + runtime.mark_all_tables_dirty(); + runtime + } else if payload.starts_with(MANIFEST_PAYLOAD_MAGIC) { + decode_manifest_payload(&store, &payload)? + } else { + return Err(DbError::corruption("unknown catalog state payload magic")); + }; + let root_schema_cookie = root.schema_cookie; + runtime.root_state = Some(root); + runtime.catalog_mut().schema_cookie = root_schema_cookie; + runtime.paged_row_storage = config.paged_row_storage; + runtime.extension_trust_anchors = Arc::new(config.extension_trust_anchors.clone()); + runtime.extension_unsigned_development_mode = + config.extension_unsigned_development_mode; + runtime + } else { + Self::from_config(schema_cookie, config) + }; + runtime + .payload_cache + .lock() + .expect("payload cache lock should not be poisoned") + .set_max_entries(config.cached_payloads_max_entries); + if runtime.root_state.is_none() { + runtime.catalog_mut().schema_cookie = schema_cookie; + } + // Materialize any deferred tables under the same reader guard so + // that overflow pointers from the manifest are read against the + // same WAL snapshot. If deferred loading uses a later snapshot, + // a concurrent writer may have extended the overflow chain, causing + // a length mismatch. + // + // ADR 0143 Phase B (opt-in): when + // `DbConfig::defer_table_materialization` is true we intentionally + // skip the eager materialize+rebuild here so that `Db::open` does + // not allocate `Vec` for every persisted table. The + // per-statement/transaction lazy-load path in `db.rs` now pins a + // single reader snapshot across both the manifest refresh and the + // overflow payload read so first-use materialization does not mix + // snapshots under concurrent checkpoints. + if !runtime.deferred_tables.is_empty() && !config.defer_table_materialization { + runtime.materialize_deferred_tables_with_store(&store, pager.page_size(), None)?; + } + if !config.defer_table_materialization || runtime.deferred_tables.is_empty() { + runtime.rebuild_indexes(pager.page_size())?; + } + Ok(runtime) + } + /// Returns `true` when one or more tables still have their row data + /// deferred (not yet loaded from storage). + #[must_use] + pub(crate) fn has_deferred_tables(&self) -> bool { + !self.deferred_tables.is_empty() + } + /// Returns an iterator over deferred table names. + #[allow(clippy::double_must_use)] + #[must_use] + pub(crate) fn deferred_table_names(&self) -> impl Iterator { + self.deferred_tables.iter() + } + /// Materializes all deferred table data from storage, then rebuilds + /// indexes. After this call `deferred_tables` is empty and the runtime + /// is fully populated. + pub(crate) fn load_deferred_tables( + &mut self, + pager: &PagerHandle, + wal: &WalHandle, + page_size: u32, + ) -> Result<()> { + self.load_deferred_tables_with_snapshot(pager, wal, page_size, None, None) + } + pub(crate) fn load_deferred_tables_at_snapshot( + &mut self, + pager: &PagerHandle, + wal: &WalHandle, + page_size: u32, + snapshot_lsn: u64, + ) -> Result<()> { + self.load_deferred_tables_with_snapshot(pager, wal, page_size, None, Some(snapshot_lsn)) + } + /// Loads a subset of deferred tables, specified by name. + /// + /// This is used for per-table on-demand loading where only the tables + /// referenced by the current SQL statement are materialized. + #[allow(dead_code)] + pub(crate) fn load_deferred_tables_filtered( + &mut self, + pager: &PagerHandle, + wal: &WalHandle, + page_size: u32, + filter: &BTreeSet, + ) -> Result<()> { + self.load_deferred_tables_with_snapshot(pager, wal, page_size, Some(filter), None) + } + pub(crate) fn load_deferred_tables_filtered_at_snapshot( + &mut self, + pager: &PagerHandle, + wal: &WalHandle, + page_size: u32, + filter: &BTreeSet, + snapshot_lsn: u64, + ) -> Result<()> { + self.load_deferred_tables_with_snapshot( + pager, + wal, + page_size, + Some(filter), + Some(snapshot_lsn), + ) + } + pub(crate) fn load_deferred_table_row_sources_filtered( + &mut self, + pager: &PagerHandle, + wal: &WalHandle, + page_size: u32, + filter: &BTreeSet, + ) -> Result<()> { + self.load_deferred_table_row_sources_with_snapshot( + pager, + wal, + page_size, + Some(filter), + None, + ) + } + pub(crate) fn load_deferred_table_row_sources_at_snapshot( + &mut self, + pager: &PagerHandle, + wal: &WalHandle, + page_size: u32, + snapshot_lsn: u64, + ) -> Result<()> { + self.load_deferred_table_row_sources_with_snapshot( + pager, + wal, + page_size, + None, + Some(snapshot_lsn), + ) + } + pub(crate) fn load_deferred_table_row_sources( + &mut self, + pager: &PagerHandle, + wal: &WalHandle, + page_size: u32, + ) -> Result<()> { + self.load_deferred_table_row_sources_with_snapshot(pager, wal, page_size, None, None) + } + fn load_deferred_table_row_sources_with_snapshot( + &mut self, + pager: &PagerHandle, + wal: &WalHandle, + page_size: u32, + filter: Option<&BTreeSet>, + snapshot_lsn: Option, + ) -> Result<()> { + if self.deferred_tables.is_empty() { + return Ok(()); + } + let Some(snapshot_lsn) = snapshot_lsn else { + let reader = wal.begin_reader_with_pager(pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + self.materialize_deferred_table_row_sources_with_store(&store, page_size, filter)?; + drop(reader); + return Ok(()); + }; + + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + self.materialize_deferred_table_row_sources_with_store(&store, page_size, filter)?; + Ok(()) + } + pub(crate) fn load_deferred_table_row_sources_filtered_at_snapshot( + &mut self, + pager: &PagerHandle, + wal: &WalHandle, + page_size: u32, + filter: &BTreeSet, + snapshot_lsn: u64, + ) -> Result<()> { + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + self.materialize_deferred_table_row_sources_with_store(&store, page_size, Some(filter)) + } + pub(crate) fn hydrate_deferred_runtime_index_at_snapshot( + &mut self, + pager: &PagerHandle, + wal: &WalHandle, + page_size: u32, + table_name: &str, + index_name: &str, + snapshot_lsn: u64, + ) -> Result<()> { + let Some(canonical_table_name) = self + .deferred_tables + .iter() + .find(|deferred| identifiers_equal(deferred, table_name)) + .cloned() + else { + return Ok(()); + }; + + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + let state = *self + .persisted_tables + .get(&canonical_table_name) + .ok_or_else(|| { + DbError::internal(format!( + "deferred table '{canonical_table_name}' has no persisted state" + )) + })?; + let table_schema = self + .catalog + .table(&canonical_table_name) + .ok_or_else(|| { + DbError::internal(format!( + "deferred table '{canonical_table_name}' has no schema" + )) + })? + .clone(); + let index_schema = self + .catalog + .index(index_name) + .ok_or_else(|| DbError::sql(format!("unknown index {index_name}")))? + .clone(); + let cache_key = DeferredRuntimeBtreeIndexCacheKey::new(&table_schema, &index_schema, state); + if let Some(entry) = cached_deferred_runtime_btree_index(&cache_key)? { + self.indexes_mut() + .insert(index_schema.name.clone(), Arc::clone(&entry.runtime_index)); + if let Some(locator_cache) = entry.paged_locator_cache.as_ref() { + self.deferred_paged_row_locator_caches_mut() + .insert(canonical_table_name, Arc::clone(locator_cache)); + } + return Ok(()); + } + + let row_source = if state.pointer.is_table_paged_manifest() { + TableRowSource::Paged(Arc::new(read_table_page_manifest_from_state( + &store, state, + )?)) + } else { + TableRowSource::Resident(Arc::new(decode_persisted_table_data(&store, state)?)) + }; + let row_count = row_source.row_count(); + if let Some(ps) = self.persisted_tables_mut().get_mut(&canonical_table_name) { + ps.row_count = row_count; + ps.tail = read_uncompressed_overflow_tail(&store, ps.pointer)?.unwrap_or_default(); + } + self.tables_mut() + .insert(canonical_table_name.clone(), row_source); + self.deferred_tables_mut().remove(&canonical_table_name); + if !self.indexes.contains_key(&index_schema.name) { + self.rebuild_index(&index_schema.name, page_size)?; + } + if state.pointer.is_table_paged_manifest() { + let Some(TableRowSource::Paged(manifest)) = self.tables.get(&canonical_table_name) + else { + return Err(DbError::internal(format!( + "paged row source for {canonical_table_name} is missing after index hydration" + ))); + }; + let chunks = Arc::clone(&manifest.chunks); + self.cache_deferred_paged_row_locators(&canonical_table_name, state, chunks.as_ref())?; + } + if let Some(runtime_index) = self.indexes.get(&index_schema.name).cloned() { + let paged_locator_cache = self + .deferred_paged_row_locator_caches + .get(&canonical_table_name) + .cloned(); + cache_deferred_runtime_btree_index( + cache_key, + DeferredRuntimeBtreeIndexCacheEntry { + runtime_index, + paged_locator_cache, + }, + )?; + } + let _ = self.redefer_persisted_tables(&[canonical_table_name.as_str()]); + Ok(()) + } + fn load_deferred_tables_with_snapshot( + &mut self, + pager: &PagerHandle, + wal: &WalHandle, + page_size: u32, + filter: Option<&BTreeSet>, + snapshot_lsn: Option, + ) -> Result<()> { + if self.deferred_tables.is_empty() { + return Ok(()); + } + let Some(snapshot_lsn) = snapshot_lsn else { + let reader = wal.begin_reader_with_pager(pager)?; + let snapshot_lsn = reader.snapshot_lsn(); + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + self.materialize_deferred_tables_with_store(&store, page_size, filter)?; + drop(reader); + return Ok(()); + }; + + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + self.materialize_deferred_tables_with_store(&store, page_size, filter)?; + Ok(()) + } + /// Loads deferred tables using an existing `SnapshotPageStore`. + /// + /// This ensures the overflow pointers recorded in `persisted_tables` + /// (from the manifest) are read against the same WAL snapshot that + /// produced those pointers, avoiding length mismatches when a + /// concurrent writer extends the overflow chain. + /// + /// When `filter` is `Some`, only the named tables are materialized. + /// When `None`, all deferred tables are materialized (legacy behavior). + fn materialize_deferred_tables_with_store( + &mut self, + store: &S, + page_size: u32, + filter: Option<&BTreeSet>, + ) -> Result<()> { + let table_names: Vec = if let Some(f) = filter { + self.deferred_tables + .iter() + .filter(|table_name| { + f.iter() + .any(|filter_name| filter_name.eq_ignore_ascii_case(table_name)) + }) + .cloned() + .collect() + } else { + self.deferred_tables.iter().cloned().collect() + }; + if table_names.is_empty() { + return Ok(()); + } + for table_name in &table_names { + let state = *self.persisted_tables.get(table_name).ok_or_else(|| { + DbError::internal(format!( + "deferred table '{table_name}' has no persisted state" + )) + })?; + let data = decode_persisted_table_data(store, state)?; + + if let Some(ps) = self.persisted_tables_mut().get_mut(table_name) { + ps.row_count = data.row_count(); + ps.tail = read_uncompressed_overflow_tail(store, ps.pointer)?.unwrap_or_default(); + } + self.tables_mut().insert(table_name.clone(), data.into()); + self.deferred_tables_mut().remove(table_name); + } + self.rebuild_stale_indexes(page_size)?; + Ok(()) + } + fn materialize_deferred_table_row_sources_with_store( + &mut self, + store: &S, + page_size: u32, + filter: Option<&BTreeSet>, + ) -> Result<()> { + let table_names: Vec = if let Some(f) = filter { + self.deferred_tables + .iter() + .filter(|table_name| { + f.iter() + .any(|filter_name| filter_name.eq_ignore_ascii_case(table_name)) + }) + .cloned() + .collect() + } else { + self.deferred_tables.iter().cloned().collect() + }; + if table_names.is_empty() { + return Ok(()); + } + for table_name in &table_names { + let state = *self.persisted_tables.get(table_name).ok_or_else(|| { + DbError::internal(format!( + "deferred table '{table_name}' has no persisted state" + )) + })?; + let row_source = if state.pointer.is_table_paged_manifest() { + TableRowSource::Paged(Arc::new(read_table_page_manifest_from_state(store, state)?)) + } else { + TableRowSource::Resident(Arc::new(decode_persisted_table_data(store, state)?)) + }; + let row_count = row_source.row_count(); + if let Some(ps) = self.persisted_tables_mut().get_mut(table_name) { + ps.row_count = row_count; + ps.tail = read_uncompressed_overflow_tail(store, ps.pointer)?.unwrap_or_default(); + } + self.tables_mut().insert(table_name.clone(), row_source); + self.deferred_tables_mut().remove(table_name); + } + self.rebuild_stale_indexes(page_size)?; + Ok(()) + } + pub(crate) fn prepare_resident_payload_offset_caches( + &mut self, + store: &S, + config: &crate::config::DbConfig, + ) -> Result { + if !preserve_resident_payload_offsets_for_delete_tombstones(config) { + return Ok(false); + } + let table_names = self + .persisted_tables + .iter() + .filter_map(|(table_name, state)| { + if state.pointer.head_page_id != 0 + && !state.pointer.is_table_paged_manifest() + && !state.pointer.is_compressed() + && !self.overflow_chain_caches.contains_key(table_name) + { + Some((table_name.clone(), state.pointer.head_page_id)) + } else { + None + } + }) + .collect::>(); + if table_names.is_empty() { + return Ok(false); + } + for (table_name, head_page_id) in table_names { + let chain_cache = build_overflow_chain_cache(store, head_page_id)?; + self.overflow_chain_caches.insert(table_name, chain_cache); + } + Ok(true) + } + pub(crate) fn backfill_missing_persistent_pk_index_for_table( + &mut self, + db: &crate::db::Db, + table_name: &str, + ) -> Result { + let Some(canonical_table_name) = self + .catalog + .tables + .keys() + .find(|candidate| identifiers_equal(candidate, table_name)) + .cloned() + else { + return Ok(false); + }; + let Some(table) = self.catalog.tables.get(&canonical_table_name) else { + return Ok(false); + }; + if table.pk_index_root.is_some() + || self + .persisted_tables + .get(&canonical_table_name) + .is_none_or(|state| state.pointer.head_page_id == 0) + { + return Ok(false); + } + self.backfill_missing_persistent_pk_index_for_canonical_table( + db, + canonical_table_name.as_str(), + ) + } + fn backfill_missing_persistent_pk_index_for_canonical_table( + &mut self, + db: &crate::db::Db, + table_name: &str, + ) -> Result { + let Some(previous_state) = self.persisted_tables.get(table_name).copied() else { + return Ok(false); + }; + if self + .catalog + .tables + .get(table_name) + .is_some_and(|table| table.pk_index_root.is_some()) + || previous_state.pointer.head_page_id == 0 + { + return Ok(false); + } + + let mut store = DbTxnPageStore { db }; + if previous_state.pointer.is_table_paged_manifest() { + let chunk_payloads = read_paged_table_chunk_payloads(&store, previous_state)?; + let pk_index_root = + build_persistent_pk_index_root_from_chunk_payloads(db, &chunk_payloads)?; + replace_table_pk_index_root(self, db, table_name, pk_index_root)?; + return Ok(true); + } + + let payload = Arc::new(read_overflow(&store, previous_state.pointer)?); + let pointer = if previous_state.pointer.is_compressed() { + rewrite_overflow( + &mut store, + previous_state.pointer, + payload.as_slice(), + CompressionMode::Never, + )? + } else { + previous_state.pointer + }; + let tail = read_uncompressed_overflow_tail(&store, pointer)?.unwrap_or_default(); + let checksum = crc32c_parts(&[payload.as_slice()]); + self.persisted_tables_mut().insert( + table_name.to_string(), + PersistedTableState { + pointer, + checksum, + row_count: previous_state.row_count, + tail, + pk_index_root: previous_state.pk_index_root, + }, + ); + self.cache_payload_insert(table_name.to_string(), Arc::clone(&payload)); + self.overflow_chain_caches.remove(table_name); + let chain_cache = build_overflow_chain_cache(&store, pointer.head_page_id)?; + self.overflow_chain_caches + .insert(table_name.to_string(), chain_cache); + + let pk_index_root = build_persistent_pk_index_root(db, payload.as_slice())?; + replace_table_pk_index_root(self, db, table_name, pk_index_root)?; + Ok(true) + } + pub(crate) fn backfill_paged_row_storage(&mut self, db: &crate::db::Db) -> Result { + let table_names = self + .catalog + .tables + .keys() + .filter_map(|table_name| { + self.persisted_tables + .get(table_name) + .filter(|state| { + state.pointer.head_page_id != 0 && !state.pointer.is_table_paged_manifest() + }) + .map(|_| table_name.clone()) + }) + .collect::>(); + if table_names.is_empty() { + return Ok(false); + } + + let mut changed = false; + let mut store = DbTxnPageStore { db }; + for table_name in table_names { + let Some(previous_state) = self.persisted_tables.get(&table_name).copied() else { + continue; + }; + let new_state = wrap_legacy_table_state_as_paged_manifest(&mut store, previous_state)?; + self.persisted_tables_mut() + .insert(table_name.clone(), new_state); + self.overflow_chain_caches.remove(&table_name); + if db.config().persistent_pk_index { + let chunk_payloads = read_paged_table_chunk_payloads(&store, new_state)?; + let pk_index_root = + build_persistent_pk_index_root_from_chunk_payloads(db, &chunk_payloads)?; + let manifest = TablePageManifest::from_chunks(chunk_payloads)?; + let payload = Arc::new(encode_legacy_table_payload_from_manifest(&manifest)?); + replace_table_pk_index_root(self, db, &table_name, pk_index_root)?; + self.cache_payload_insert(table_name.clone(), payload); + } + changed = true; + } + Ok(changed) + } + pub(crate) fn compact_persisted_payloads_for_checkpoint( + &mut self, + db: &crate::db::Db, + ) -> Result { + let old_root = self.root_state; + let mut changed = false; + { + let mut store = DbTxnPageStore { db }; + let table_names = self.persisted_tables.keys().cloned().collect::>(); + for table_name in table_names { + let Some(previous_state) = self.persisted_tables.get(&table_name).copied() else { + continue; + }; + let previous_pointer = previous_state.pointer; + if previous_pointer.head_page_id == 0 { + continue; + } + if previous_pointer.is_table_paged_manifest() { + let (new_state, table_changed) = + compact_paged_table_state_for_checkpoint(&mut store, previous_state)?; + if table_changed { + self.persisted_tables_mut() + .insert(table_name.clone(), new_state); + if db.config().persistent_pk_index { + let chunk_payloads = + read_paged_table_chunk_payloads(&store, new_state)?; + let pk_index_root = build_persistent_pk_index_root_from_chunk_payloads( + db, + &chunk_payloads, + )?; + replace_table_pk_index_root(self, db, &table_name, pk_index_root)?; + } + changed = true; + } + continue; + } + if previous_state.pk_index_root.is_some() + || previous_pointer.is_compressed() + || usize::try_from(previous_pointer.logical_len) + .ok() + .is_none_or(|len| len < AUTO_MIN_PAYLOAD_BYTES) + { + continue; + } + let payload = if let Some(cached) = self.cached_payload(&table_name) { + cached + } else { + Arc::new(read_overflow(&store, previous_pointer)?) + }; + let pointer = rewrite_overflow( + &mut store, + previous_pointer, + payload.as_slice(), + CompressionMode::Auto, + )?; + if pointer != previous_pointer { + changed = true; + } + let tail = if pointer.is_compressed() { + OverflowTailInfo::default() + } else { + read_uncompressed_overflow_tail(&store, pointer)?.unwrap_or_default() + }; + self.persisted_tables_mut().insert( + table_name.clone(), + PersistedTableState { + pointer, + checksum: previous_state.checksum, + row_count: previous_state.row_count, + tail, + pk_index_root: previous_state.pk_index_root, + }, + ); + self.cache_payload_insert(table_name.clone(), payload); + self.overflow_chain_caches.remove(&table_name); + } + } + + let (checksum, pointer) = { + let manifest = self.manifest_payload()?; + let checksum = crc32c_parts(&[manifest]); + let previous_manifest_pointer = old_root.map_or( + OverflowPointer { + head_page_id: 0, + logical_len: 0, + flags: 0, + }, + |root| root.pointer, + ); + let pointer = { + let mut store = DbTxnPageStore { db }; + rewrite_overflow( + &mut store, + previous_manifest_pointer, + manifest, + CompressionMode::Auto, + )? + }; + (checksum, pointer) + }; + + let new_root = RootHeader { + schema_cookie: self.catalog.schema_cookie, + payload_checksum: checksum, + pointer, + }; + if old_root != Some(new_root) { + let root_page = encode_root_header(db.config().page_size, new_root); + db.write_page_owned(page::CATALOG_ROOT_PAGE_ID, root_page)?; + self.root_state = Some(new_root); + changed = true; + } + Ok(changed) + } + pub(crate) fn redefer_persisted_tables(&mut self, names: &[&str]) -> usize { + let mut freed_bytes = 0usize; + for name in names { + let Some(table_name) = self.canonical_catalog_table_name(name) else { + continue; + }; + if self + .persisted_tables + .get(&table_name) + .is_some_and(|state| state.pointer.is_table_paged_manifest()) + { + if let Some(row_source) = self.tables_mut().remove(&table_name) { + freed_bytes = freed_bytes.saturating_add(row_source.approximate_heap_bytes()); + self.deferred_tables_mut().insert(table_name.clone()); + self.dirty_tables_mut().remove(&table_name); + self.paged_mutations.remove(&table_name); + } + } + } + freed_bytes + } + pub(crate) fn redefer_all_persisted_paged_tables(&mut self) -> usize { + let paged_names: Vec = self + .persisted_tables + .iter() + .filter_map(|(name, state)| { + if state.pointer.is_table_paged_manifest() && self.tables.contains_key(name) { + Some(name.clone()) + } else { + None + } + }) + .collect(); + let name_refs: Vec<&str> = paged_names.iter().map(|s| s.as_str()).collect(); + self.redefer_persisted_tables(&name_refs) + } + #[allow(clippy::too_many_arguments)] + pub(crate) fn try_execute_observed_current_linear_three_table_view( + &self, + table_row_readers: &[DeferredViewTableRowReader<'_>], + join_steps: &[DeferredViewJoinStep], + join_keys: &[&RuntimeBtreeKeys], + key_projection_indexes: &[Option], + table_projections: &[DeferredViewTableProjection], + projection_indexes: &[DeferredViewProjectionSource], + lookup_row_id: i64, + column_names: Vec, + linear_tail_can_move: bool, + ) -> Result> { + if table_row_readers.len() != 3 + || join_steps.len() != 2 + || table_projections.len() != 3 + || join_keys.len() != 2 + || key_projection_indexes.len() != 2 + { + return Ok(None); + } + let step0 = &join_steps[0]; + let step1 = &join_steps[1]; + if step0.previous_table_index != 0 + || step0.current_table_index != 1 + || step1.previous_table_index != 1 + || step1.current_table_index != 2 + { + return Ok(None); + } + let [keys0, keys1] = join_keys else { + return Ok(None); + }; + let [key0_projection_index, key1_projection_index] = key_projection_indexes else { + return Ok(None); + }; + let Some(source_row) = table_row_readers[0].read_projected_from_observed_cache( + lookup_row_id, + &table_projections[0].projection_indexes, + )? + else { + return Ok(None); + }; + let Some(source_row) = source_row else { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + }; + + let key0_row_ids = match *key0_projection_index { + Some(projection_index) => { + let Some(key_value) = source_row.values.get(projection_index) else { + return Err(DbError::internal( + "observed-current view root row is shorter than planned schema", + )); + }; + if matches!(key_value, Value::Null) { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + } + keys0.row_ids_for_value_set(key_value)? + } + None => keys0.row_ids_for_row_id(source_row.row_id), + }; + + let mut cache_available = true; + let mut rows = Vec::with_capacity(64); + key0_row_ids.visit_until(|row1_id| { + let Some(row1) = table_row_readers[1].read_projected_from_observed_cache( + row1_id, + &table_projections[1].projection_indexes, + )? + else { + cache_available = false; + return Ok(true); + }; + let Some(row1) = row1 else { + return Ok(false); + }; + let key1_row_ids = match *key1_projection_index { + Some(projection_index) => { + let Some(key_value) = row1.values.get(projection_index) else { + return Err(DbError::internal( + "observed-current view join row is shorter than planned schema", + )); + }; + if matches!(key_value, Value::Null) { + return Ok(false); + } + keys1.row_ids_for_value_set(key_value)? + } + None => keys1.row_ids_for_row_id(row1.row_id), + }; + key1_row_ids.visit_until(|row2_id| { + let Some(row2) = table_row_readers[2].read_projected_from_observed_cache( + row2_id, + &table_projections[2].projection_indexes, + )? + else { + cache_available = false; + return Ok(true); + }; + let Some(row2) = row2 else { + return Ok(false); + }; + rows.push(collect_deferred_view_query_row_from_linear_tail( + &source_row, + &row1, + row2, + projection_indexes, + "observed-current view row-id projection", + linear_tail_can_move, + )?); + Ok(false) + }) + })?; + if !cache_available { + return Ok(None); + } + Ok(Some(QueryResult::with_rows(column_names, rows))) + } + pub(crate) fn deferred_view_join_step( + &self, + table_bindings: &[TableBindingRef<'_>], + table_schemas: &[&TableSchema], + constraint: &Expr, + current_table_index: usize, + ) -> Result> { + let Some(equalities) = simple_join_equalities(constraint) else { + return Ok(None); + }; + let current_binding = table_bindings[current_table_index]; + let mut matched = None; + for (left_ref, right_ref) in equalities { + for (previous_ref, current_ref) in [(left_ref, right_ref), (right_ref, left_ref)] { + if !matches_table_binding(current_binding, current_ref.table) { + continue; + } + let Some(previous_table_index) = table_bindings[..current_table_index] + .iter() + .position(|binding| matches_table_binding(*binding, previous_ref.table)) + else { + continue; + }; + let Some(previous_column_index) = + schema_column_index(table_schemas[previous_table_index], previous_ref.column) + else { + return Ok(None); + }; + let previous_is_rowid_alias = + rowid_alias_column_index(table_schemas[previous_table_index]) + == Some(previous_column_index); + let Some(current_index) = self + .simple_btree_index_for_table_column(current_binding.name, current_ref.column) + else { + return Ok(None); + }; + if matched + .replace(DeferredViewJoinStep { + previous_table_index, + previous_column_index, + current_table_index, + current_index_name: current_index.name.clone(), + previous_is_rowid_alias, + }) + .is_some() + { + return Ok(None); + } + if schema_column_index(table_schemas[current_table_index], current_ref.column) + .is_none() + { + return Err(DbError::internal( + "deferred view join current column is missing from schema", + )); + } + } + } + Ok(matched) + } + #[allow(clippy::too_many_arguments)] + pub(crate) fn push_deferred_view_limit_rows_from_root( + &self, + store: &S, + table_row_readers: &[DeferredViewTableRowReader<'_>], + join_steps: &[DeferredViewJoinStep], + join_keys: &[&RuntimeBtreeKeys], + key_projection_indexes: &[Option], + table_projections: &[DeferredViewTableProjection], + projection_indexes: &[DeferredViewProjectionSource], + root_row: StoredRow, + offset_remaining: &mut usize, + limit_remaining: &mut usize, + rows: &mut Vec, + partial_rows: &mut Vec, + chunk_payload_cache: &mut HashMap>>, + use_persistent_pk_index: bool, + linear_tail_can_move: bool, + ) -> Result { + if let Some(stopped) = self.stream_deferred_view_linear_three_table_rows_from_root( + store, + table_row_readers, + join_steps, + join_keys, + key_projection_indexes, + table_projections, + &root_row, + use_persistent_pk_index, + chunk_payload_cache, + &mut |root_row, row1, row2| { + if *offset_remaining > 0 { + *offset_remaining -= 1; + return Ok(false); + } + if *limit_remaining == 0 { + return Ok(true); + } + let row = collect_deferred_view_query_row_from_linear_tail( + root_row, + row1, + row2, + projection_indexes, + "deferred view limit projection", + linear_tail_can_move, + )?; + rows.push(row); + *limit_remaining = (*limit_remaining).saturating_sub(1); + Ok(*limit_remaining == 0) + }, + )? { + return Ok(stopped); + } + + let Some(stopped) = self.stream_deferred_view_join_rows_from_root( + store, + table_row_readers, + join_steps, + join_keys, + key_projection_indexes, + table_projections, + root_row, + partial_rows, + use_persistent_pk_index, + true, + chunk_payload_cache, + &mut |partial| { + if *offset_remaining > 0 { + *offset_remaining -= 1; + return Ok(false); + } + if *limit_remaining == 0 { + return Ok(true); + } + let values = collect_deferred_view_projection_values( + partial, + projection_indexes, + "deferred view limit projection", + )?; + rows.push(QueryRow::new(values)); + *limit_remaining = (*limit_remaining).saturating_sub(1); + Ok(*limit_remaining == 0) + }, + )? + else { + return Err(DbError::internal( + "index was unavailable while executing deferred view limit join", + )); + }; + Ok(stopped) + } +} diff --git a/crates/decentdb/src/exec/evaluate.rs b/crates/decentdb/src/exec/evaluate.rs new file mode 100644 index 00000000..b97bd51f --- /dev/null +++ b/crates/decentdb/src/exec/evaluate.rs @@ -0,0 +1,1125 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +impl EngineRuntime { + pub(crate) fn clear_fts_eval_context(&self) -> Result<()> { + self.fts_eval_context + .lock() + .map_err(|_| DbError::internal("FTS eval context lock poisoned"))? + .scores + .clear(); + Ok(()) + } + pub(crate) fn apply_select_distinct( + &self, + select: &Select, + dataset: Dataset, + params: &[Value], + ctes: &BTreeMap, + ) -> Result { + if !select.distinct { + return Ok(dataset); + } + if select.distinct_on.iter().any(expr_contains_collation) + || select.projection.iter().any(select_item_contains_collation) + { + return Err(DbError::sql( + "COLLATE in DISTINCT keys is not supported in this compatibility slice", + )); + } + + let Dataset { columns, rows } = dataset; + let rows = if select.distinct_on.is_empty() { + deduplicate_rows_stable(Arc::unwrap_or_clone(rows))? + } else { + let key_dataset = Dataset::with_rows(columns.clone(), Vec::new()); + let mut seen = BTreeSet::new(); + let mut distinct_rows = Vec::new(); + for row in Arc::unwrap_or_clone(rows) { + let key = select + .distinct_on + .iter() + .map(|expr| self.eval_expr(expr, &key_dataset, &row, params, ctes, None)) + .collect::>>()?; + if seen.insert(row_identity(&key)?) { + distinct_rows.push(row); + } + } + distinct_rows + }; + + Ok(Dataset::with_rows(columns, rows)) + } + pub(crate) fn try_view_filter_pushdown( + &self, + select: &Select, + params: &[Value], + ctes: &BTreeMap, + ) -> Result> { + let Some(filter) = select.filter.as_ref() else { + return Ok(None); + }; + if select.from.len() != 1 { + return Ok(None); + } + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if ctes.contains_key(name) { + return Ok(None); + } + let Some(view) = self.visible_view(name, NameResolutionScope::Session) else { + return Ok(None); + }; + let Some((table_qualifier, filter_column, value_expr)) = simple_btree_lookup(filter) else { + return Ok(None); + }; + let view_binding = alias.as_deref().unwrap_or(name.as_str()); + if table_qualifier.is_some_and(|table| !identifiers_equal(table, view_binding)) { + return Ok(None); + } + + let mut query = (*self.cached_view_query(view)?).clone(); + if query.recursive + || !query.ctes.is_empty() + || !query.order_by.is_empty() + || query.limit.is_some() + || query.offset.is_some() + { + return Ok(None); + } + let QueryBody::Select(view_select) = &mut query.body else { + return Ok(None); + }; + if view_select.distinct + || !view_select.distinct_on.is_empty() + || !view_select.group_by.is_empty() + || view_select.having.is_some() + || projection_has_aggregate_items(&view_select.projection) + { + return Ok(None); + } + let Some(view_expr) = + view_projection_expr_for_output_column(&view_select.projection, filter_column) + else { + return Ok(None); + }; + let pushed_filter = Expr::Binary { + left: Box::new(view_expr), + op: BinaryOp::Eq, + right: Box::new(value_expr.clone()), + }; + view_select.filter = match view_select.filter.take() { + Some(existing) => Some(Expr::Binary { + left: Box::new(existing), + op: BinaryOp::And, + right: Box::new(pushed_filter), + }), + None => Some(pushed_filter), + }; + + let mut dataset = if view.temporary { + self.evaluate_query(&query, params, ctes)? + } else { + let persistent_runtime = self.persistent_resolution_runtime(); + persistent_runtime.evaluate_query(&query, params, ctes)? + }; + if let Some(alias) = alias { + for column in &mut dataset.columns { + column.table = Some(alias.clone()); + } + } else { + for column in &mut dataset.columns { + column.table = Some(view.name.clone()); + } + } + Ok(Some(dataset)) + } + pub(crate) fn evaluate_values_body( + &self, + rows: &[Vec], + params: &[Value], + ctes: &BTreeMap, + ) -> Result { + self.evaluate_values_body_inner(rows, params, ctes, &Dataset::empty(), &[]) + } + pub(crate) fn evaluate_values_body_with_outer( + &self, + rows: &[Vec], + params: &[Value], + ctes: &BTreeMap, + outer_dataset: &Dataset, + outer_row: &[Value], + ) -> Result { + self.evaluate_values_body_inner(rows, params, ctes, outer_dataset, outer_row) + } + fn evaluate_values_body_inner( + &self, + rows: &[Vec], + params: &[Value], + ctes: &BTreeMap, + scope_dataset: &Dataset, + scope_row: &[Value], + ) -> Result { + let width = rows.first().map_or(0, Vec::len); + let mut columns = Vec::with_capacity(width); + if let Some(first_row) = rows.first() { + for (index, expr) in first_row.iter().enumerate() { + columns.push(ColumnBinding::visible( + None, + infer_expr_name(expr, index + 1), + )); + } + } + + let mut result_rows = Vec::with_capacity(rows.len()); + for row in rows { + if row.len() != width { + return Err(DbError::sql( + "VALUES rows must all have the same number of columns", + )); + } + let values = row + .iter() + .map(|expr| self.eval_expr(expr, scope_dataset, scope_row, params, ctes, None)) + .collect::>>()?; + result_rows.push(values); + } + Ok(Dataset::with_rows(columns, result_rows)) + } + pub(crate) fn evaluate_from_item( + &self, + item: &FromItem, + params: &[Value], + ctes: &BTreeMap, + ) -> Result { + self.evaluate_from_item_in_scope(item, params, ctes, &Dataset::empty(), &[]) + } + pub(crate) fn evaluate_from_item_in_scope( + &self, + item: &FromItem, + params: &[Value], + ctes: &BTreeMap, + scope_dataset: &Dataset, + scope_row: &[Value], + ) -> Result { + match item { + FromItem::Table { name, alias } => { + if let Some(dataset) = ctes.get(name) { + let mut columns = dataset.columns.clone(); + if let Some(alias) = alias { + for column in &mut columns { + column.table = Some(alias.clone()); + } + } + return Ok(dataset.share_rows(columns)); + } + if let Some(mut dataset) = self.compatibility_virtual_table(name)? { + if let Some(alias) = alias { + for column in &mut dataset.columns { + column.table = Some(alias.clone()); + } + } + return Ok(dataset); + } + if let Some(view) = self.visible_view(name, NameResolutionScope::Session) { + let query = self.cached_view_query(view)?; + let mut dataset = if view.temporary { + self.evaluate_query(query.as_ref(), params, ctes)? + } else { + let persistent_runtime = self.persistent_resolution_runtime(); + persistent_runtime.evaluate_query(query.as_ref(), params, ctes)? + }; + if let Some(alias) = alias { + for column in &mut dataset.columns { + column.table = Some(alias.clone()); + } + } else { + for column in &mut dataset.columns { + column.table = Some(view.name.clone()); + } + } + return Ok(dataset); + } + let table = self + .table_schema(name) + .ok_or_else(|| DbError::sql(format!("unknown table or view {name}")))?; + let row_source = self.visible_table_row_source(name).ok_or_else(|| { + DbError::internal(format!( + "table row source for {name} was not loaded before FROM evaluation" + )) + })?; + self.dataset_from_visible_row_source(table, row_source, alias) + } + FromItem::Subquery { + query, + alias, + column_names, + lateral, + } => { + let mut dataset = if *lateral || query_references_outer_scope(query, scope_dataset) + { + self.evaluate_query_with_outer(query, params, ctes, scope_dataset, scope_row)? + } else { + self.evaluate_query(query, params, ctes)? + }; + if !column_names.is_empty() { + if column_names.len() != dataset.columns.len() { + return Err(DbError::sql(format!( + "subquery alias {} expected {} column names but produced {} columns", + alias, + column_names.len(), + dataset.columns.len() + ))); + } + for (binding, column_name) in dataset.columns.iter_mut().zip(column_names) { + binding.name = column_name.clone(); + } + } + for column in &mut dataset.columns { + column.table = Some(alias.clone()); + } + Ok(dataset) + } + FromItem::Function { + name, + args, + alias, + lateral, + } => { + let eval_dataset = if *lateral { + scope_dataset + } else { + &Dataset::empty() + }; + let eval_row = if *lateral { scope_row } else { &[] }; + let values = args + .iter() + .map(|expr| self.eval_expr(expr, eval_dataset, eval_row, params, ctes, None)) + .collect::>>()?; + self.evaluate_table_function(name, values, alias) + } + FromItem::Join { + left, + right, + kind, + constraint, + } => { + let left = + self.evaluate_from_item_in_scope(left, params, ctes, scope_dataset, scope_row)?; + if from_item_is_lateral(right) { + return self.evaluate_join_with_lateral_right( + left, + right, + *kind, + constraint, + params, + ctes, + scope_dataset, + scope_row, + ); + } + if matches!( + kind, + JoinKind::Inner | JoinKind::Left | JoinKind::Right | JoinKind::Full + ) { + if let Some(dataset) = self.try_indexed_equi_join_with_right_table( + &left, right, *kind, constraint, ctes, + )? { + return Ok(dataset); + } + if let Some(dataset) = self.try_indexed_equi_join_with_right_cte( + &left, right, constraint, *kind, ctes, + )? { + return Ok(dataset); + } + } + let right = self.evaluate_from_item_in_scope( + right, + params, + ctes, + scope_dataset, + scope_row, + )?; + nested_loop_join(left, right, *kind, constraint, self, params, ctes) + } + } + } + fn evaluate_table_function( + &self, + name: &str, + values: Vec, + alias: &Option, + ) -> Result { + let table_name = alias.clone().unwrap_or_else(|| name.to_string()); + match name { + "json_each" | "pg_catalog.json_each" => { + self.evaluate_json_table_function(table_name, values, false) + } + "json_tree" | "pg_catalog.json_tree" => { + self.evaluate_json_table_function(table_name, values, true) + } + "generate_series" | "pg_catalog.generate_series" => { + self.evaluate_generate_series(table_name, values) + } + "pragma_table_info" | "main.pragma_table_info" | "temp.pragma_table_info" => { + self.evaluate_pragma_table_info_function(table_name, values, false) + } + "pragma_table_xinfo" | "main.pragma_table_xinfo" | "temp.pragma_table_xinfo" => { + self.evaluate_pragma_table_info_function(table_name, values, true) + } + "pragma_table_list" | "main.pragma_table_list" | "temp.pragma_table_list" => { + self.evaluate_pragma_table_list_function(table_name, values) + } + "pragma_index_list" | "main.pragma_index_list" | "temp.pragma_index_list" => { + self.evaluate_pragma_index_list_function(table_name, values) + } + "pragma_index_info" | "main.pragma_index_info" | "temp.pragma_index_info" => { + self.evaluate_pragma_index_info_function(table_name, values, false) + } + "pragma_index_xinfo" | "main.pragma_index_xinfo" | "temp.pragma_index_xinfo" => { + self.evaluate_pragma_index_info_function(table_name, values, true) + } + "pragma_foreign_key_list" + | "main.pragma_foreign_key_list" + | "temp.pragma_foreign_key_list" => { + self.evaluate_pragma_foreign_key_list_function(table_name, values) + } + "pragma_database_list" | "main.pragma_database_list" | "temp.pragma_database_list" => { + self.evaluate_pragma_database_list_function(table_name, values) + } + other => { + if let Some(dataset) = crate::extensions::evaluate_table_function_from_runtime( + self, other, values, table_name, + )? { + return Ok(dataset); + } + Err(DbError::sql(format!("unsupported table function {other}"))) + } + } + } + fn compatibility_virtual_table(&self, name: &str) -> Result> { + let normalized = name.to_ascii_lowercase(); + let table_name = match normalized.as_str() { + "sqlite_schema" | "sqlite_master" | "main.sqlite_schema" | "main.sqlite_master" => { + return Ok(Some(self.sqlite_schema_dataset("sqlite_schema", false))); + } + "sqlite_temp_schema" | "sqlite_temp_master" | "temp.sqlite_schema" + | "temp.sqlite_master" => { + return Ok(Some(self.sqlite_schema_dataset("sqlite_temp_schema", true))); + } + "information_schema.schemata" => { + return Ok(Some(self.information_schema_schemata_dataset())); + } + "information_schema.tables" => { + return Ok(Some(self.information_schema_tables_dataset())); + } + "information_schema.columns" => { + return Ok(Some(self.information_schema_columns_dataset())); + } + "sys_audit_context" => { + return self.sys_audit_context_dataset().map(Some); + } + _ => name, + }; + let _ = table_name; + Ok(None) + } + fn sys_audit_context_dataset(&self) -> Result { + let context = self + .audit_context + .lock() + .map_err(|_| DbError::internal("audit context lock poisoned"))? + .snapshot(); + let rows = context + .into_iter() + .map(|(key, value)| vec![Value::Text(key), value]) + .collect::>(); + Ok(Dataset::with_rows( + visible_columns("sys_audit_context", &["key", "value"]), + rows, + )) + } + fn evaluate_generate_series(&self, table_name: String, values: Vec) -> Result { + if !(values.len() == 2 || values.len() == 3) { + return Err(DbError::sql("generate_series expects 2 or 3 arguments")); + } + let rows = generate_series_rows(&values)?; + Ok(Dataset::with_rows( + vec![ColumnBinding::visible( + Some(table_name), + "value".to_string(), + )], + rows.into_iter().map(|value| vec![value]).collect(), + )) + } + fn evaluate_pragma_table_info_function( + &self, + table_name: String, + values: Vec, + extended: bool, + ) -> Result { + let target = one_text_arg("pragma_table_info", values)?; + let Some(table) = self.table_schema(&target) else { + return Ok(pragma_table_info_dataset(table_name, &[], extended)); + }; + Ok(pragma_table_info_dataset( + table_name, + &table.columns, + extended, + )) + } + fn evaluate_pragma_table_list_function( + &self, + table_name: String, + values: Vec, + ) -> Result { + if !values.is_empty() { + return Err(DbError::sql("pragma_table_list expects no arguments")); + } + Ok(self.pragma_table_list_dataset(table_name)) + } + fn evaluate_pragma_index_list_function( + &self, + table_name: String, + values: Vec, + ) -> Result { + let target = one_text_arg("pragma_index_list", values)?; + let mut rows = Vec::new(); + for (seq, index) in self.indexes_for_table(&target).into_iter().enumerate() { + rows.push(vec![ + Value::Int64(seq as i64), + Value::Text(index.name.clone()), + Value::Int64(i64::from(index.unique)), + Value::Text(if index.unique { "u" } else { "c" }.to_string()), + Value::Int64(i64::from(index.predicate_sql.is_some())), + ]); + } + Ok(Dataset::with_rows( + visible_columns(&table_name, &["seq", "name", "unique", "origin", "partial"]), + rows, + )) + } + fn evaluate_pragma_index_info_function( + &self, + table_name: String, + values: Vec, + extended: bool, + ) -> Result { + let target = one_text_arg("pragma_index_info", values)?; + let rows = self + .index_by_name(&target) + .map(|index| index_info_rows(index, extended)) + .unwrap_or_default(); + let columns = if extended { + visible_columns( + &table_name, + &["seqno", "cid", "name", "desc", "coll", "key"], + ) + } else { + visible_columns(&table_name, &["seqno", "cid", "name"]) + }; + Ok(Dataset::with_rows(columns, rows)) + } + fn evaluate_pragma_foreign_key_list_function( + &self, + table_name: String, + values: Vec, + ) -> Result { + let target = one_text_arg("pragma_foreign_key_list", values)?; + let mut rows = Vec::new(); + if let Some(table) = self.table_schema(&target) { + rows.extend(foreign_key_rows(table)); + } + Ok(Dataset::with_rows( + visible_columns( + &table_name, + &[ + "id", + "seq", + "table", + "from", + "to", + "on_update", + "on_delete", + "match", + ], + ), + rows, + )) + } + fn evaluate_pragma_database_list_function( + &self, + table_name: String, + values: Vec, + ) -> Result { + if !values.is_empty() { + return Err(DbError::sql("pragma_database_list expects no arguments")); + } + Ok(Dataset::with_rows( + visible_columns(&table_name, &["seq", "name", "file"]), + vec![vec![ + Value::Int64(0), + Value::Text("main".to_string()), + Value::Text("main".to_string()), + ]], + )) + } + fn pragma_table_list_dataset(&self, table_name: String) -> Dataset { + let mut rows = Vec::new(); + for table in self.catalog.tables.values() { + if !compat_catalog_object_is_visible(&table.name) { + continue; + } + rows.push(table_list_row( + "main", + &table.name, + "table", + table.columns.len(), + )); + } + for view in self.catalog.views.values() { + rows.push(table_list_row( + "main", + &view.name, + "view", + view.column_names.len(), + )); + } + for table in self.temp_tables.values() { + rows.push(table_list_row( + "temp", + &table.name, + "table", + table.columns.len(), + )); + } + for view in self.temp_views.values() { + rows.push(table_list_row( + "temp", + &view.name, + "view", + view.column_names.len(), + )); + } + Dataset::with_rows( + visible_columns( + &table_name, + &["schema", "name", "type", "ncol", "wr", "strict"], + ), + rows, + ) + } + fn sqlite_schema_dataset(&self, table_name: &str, temporary: bool) -> Dataset { + let mut rows = Vec::new(); + if temporary { + for table in self.temp_tables.values() { + if !compat_catalog_object_is_visible(&table.name) { + continue; + } + rows.push(sqlite_schema_row( + "table", + &table.name, + &table.name, + Some(render_compat_create_table(table)), + )); + } + for view in self.temp_views.values() { + rows.push(sqlite_schema_row( + "view", + &view.name, + &view.name, + Some(render_compat_create_view(view)), + )); + } + for index in self.temp_indexes.values() { + if !compat_catalog_object_is_visible(&index.name) + || !compat_catalog_object_is_visible(&index.table_name) + { + continue; + } + rows.push(sqlite_schema_row( + "index", + &index.name, + &index.table_name, + Some(render_compat_create_index(index)), + )); + } + } else { + for table in self.catalog.tables.values() { + if !compat_catalog_object_is_visible(&table.name) { + continue; + } + rows.push(sqlite_schema_row( + "table", + &table.name, + &table.name, + Some(render_compat_create_table(table)), + )); + } + for view in self.catalog.views.values() { + rows.push(sqlite_schema_row( + "view", + &view.name, + &view.name, + Some(render_compat_create_view(view)), + )); + } + for index in self.catalog.indexes.values() { + if !compat_catalog_object_is_visible(&index.name) + || !compat_catalog_object_is_visible(&index.table_name) + { + continue; + } + rows.push(sqlite_schema_row( + "index", + &index.name, + &index.table_name, + Some(render_compat_create_index(index)), + )); + } + for trigger in self.catalog.triggers.values() { + rows.push(sqlite_schema_row( + "trigger", + &trigger.name, + &trigger.target_name, + Some(render_compat_create_trigger(trigger)), + )); + } + } + Dataset::with_rows( + visible_columns(table_name, &["type", "name", "tbl_name", "rootpage", "sql"]), + rows, + ) + } + fn information_schema_schemata_dataset(&self) -> Dataset { + let table_name = "schemata"; + let mut rows = vec![ + information_schema_schemata_row("main"), + information_schema_schemata_row("temp"), + ]; + for schema in self.catalog.schemas.values() { + if !identifiers_equal(&schema.name, "main") && !identifiers_equal(&schema.name, "temp") + { + rows.push(information_schema_schemata_row(&schema.name)); + } + } + Dataset::with_rows( + visible_columns( + table_name, + &[ + "catalog_name", + "schema_name", + "schema_owner", + "default_character_set_catalog", + "default_character_set_schema", + "default_character_set_name", + ], + ), + rows, + ) + } + fn information_schema_tables_dataset(&self) -> Dataset { + let table_name = "tables"; + let mut rows = Vec::new(); + for table in self.catalog.tables.values() { + if !compat_catalog_object_is_visible(&table.name) { + continue; + } + rows.push(information_schema_table_row( + "main", + &table.name, + "BASE TABLE", + )); + } + for view in self.catalog.views.values() { + rows.push(information_schema_table_row("main", &view.name, "VIEW")); + } + for table in self.temp_tables.values() { + if !compat_catalog_object_is_visible(&table.name) { + continue; + } + rows.push(information_schema_table_row( + "temp", + &table.name, + "LOCAL TEMPORARY", + )); + } + for view in self.temp_views.values() { + rows.push(information_schema_table_row( + "temp", + &view.name, + "LOCAL TEMPORARY", + )); + } + Dataset::with_rows( + visible_columns( + table_name, + &["table_catalog", "table_schema", "table_name", "table_type"], + ), + rows, + ) + } + fn information_schema_columns_dataset(&self) -> Dataset { + let table_name = "columns"; + let mut rows = Vec::new(); + for table in self.catalog.tables.values() { + if !compat_catalog_object_is_visible(&table.name) { + continue; + } + rows.extend(information_schema_column_rows( + "main", + &table.name, + &table.columns, + )); + } + for table in self.temp_tables.values() { + if !compat_catalog_object_is_visible(&table.name) { + continue; + } + rows.extend(information_schema_column_rows( + "temp", + &table.name, + &table.columns, + )); + } + Dataset::with_rows( + visible_columns( + table_name, + &[ + "table_catalog", + "table_schema", + "table_name", + "column_name", + "ordinal_position", + "column_default", + "is_nullable", + "data_type", + ], + ), + rows, + ) + } + fn evaluate_json_table_function( + &self, + table_name: String, + values: Vec, + recursive: bool, + ) -> Result { + if values.len() != 1 { + return Err(DbError::sql(if recursive { + "json_tree expects 1 argument" + } else { + "json_each expects 1 argument" + })); + } + let rows = if recursive { + expand_json_tree_rows(&values[0])? + } else { + expand_json_each_rows(&values[0])? + }; + let mut columns = vec![ + ColumnBinding::visible(Some(table_name.clone()), "key".to_string()), + ColumnBinding::visible(Some(table_name.clone()), "value".to_string()), + ColumnBinding::visible(Some(table_name.clone()), "type".to_string()), + ]; + if recursive { + columns.push(ColumnBinding::visible(Some(table_name), "path".to_string())); + } + Ok(Dataset::with_rows(columns, rows)) + } + pub(crate) fn dataset_from_row_id_set( + &self, + table: &TableSchema, + row_source: Option<&TableRowSource>, + alias: &Option, + row_ids: RuntimeRowIdSet<'_>, + include_hidden_row_id: bool, + ) -> Result { + let table_name = alias.clone().unwrap_or_else(|| table.name.clone()); + let mut rows = Vec::with_capacity(row_ids.len()); + let mut row_lookup_error = None; + row_ids.for_each(|row_id| { + if row_lookup_error.is_some() { + return; + } + match row_source + .map(|source| source.row_by_id(row_id)) + .transpose() + { + Ok(Some(Some(row))) => { + let mut values = row.values().to_vec(); + if include_hidden_row_id { + values.push(Value::Int64(row.row_id())); + } + rows.push(values); + } + Ok(Some(None)) | Ok(None) => {} + Err(error) => row_lookup_error = Some(error), + } + }); + if let Some(error) = row_lookup_error { + return Err(error); + } + let mut columns = table + .columns + .iter() + .map(|column| { + ColumnBinding::visible_source( + Some(table_name.clone()), + Some(table.name.clone()), + column.name.clone(), + ) + }) + .collect::>(); + if include_hidden_row_id { + columns.push(ColumnBinding::hidden_source( + Some(table_name), + Some(table.name.clone()), + FTS_HIDDEN_ROW_ID_COLUMN.to_string(), + )); + } + Ok(Dataset::with_rows(columns, rows)) + } + fn dataset_from_visible_row_source( + &self, + table: &TableSchema, + row_source: VisibleTableRowSource<'_>, + alias: &Option, + ) -> Result { + let table_name = alias.clone().unwrap_or_else(|| table.name.clone()); + let mut rows = Vec::with_capacity(row_source.row_count()); + for row in row_source.rows() { + let row = row?; + let mut values = row.values().to_vec(); + if !generated_columns_are_stored(table) { + self.apply_virtual_generated_columns(table, &mut values)?; + } + rows.push(values); + } + let columns = table + .columns + .iter() + .map(|column| { + ColumnBinding::visible_source( + Some(table_name.clone()), + Some(table.name.clone()), + column.name.clone(), + ) + }) + .collect::>(); + let mut dataset = Dataset::with_rows(columns, rows); + self.apply_row_policies(table, &mut dataset)?; + Ok(dataset) + } + fn apply_row_policies(&self, table: &TableSchema, dataset: &mut Dataset) -> Result<()> { + if table.temporary || crate::security::is_security_internal_table(&table.name) { + return Ok(()); + } + let policies = self.active_row_policies_for_table(&table.name)?; + if policies.is_empty() { + return Ok(()); + } + let filter_dataset = Dataset::with_rows(dataset.columns.clone(), Vec::new()); + let mut kept = Vec::with_capacity(dataset.rows.len()); + for row in dataset.rows.iter() { + let mut visible = true; + for policy in &policies { + match self.eval_expr( + &policy.expr, + &filter_dataset, + row, + &[], + &BTreeMap::new(), + None, + )? { + Value::Bool(true) => {} + Value::Bool(false) | Value::Null => { + visible = false; + break; + } + other => { + return Err(DbError::sql(format!( + "policy {} did not evaluate to BOOL: {other:?}", + policy.name + ))) + } + } + } + if visible { + kept.push(row.clone()); + } + } + dataset.set_rows(kept); + Ok(()) + } + fn active_row_policies_for_table(&self, table_name: &str) -> Result> { + let Some(row_source) = self.visible_table_row_source(crate::security::POLICIES_TABLE) + else { + return Ok(Vec::new()); + }; + let mut policies = Vec::new(); + for row in row_source.rows() { + let row = row?; + let values = row.values(); + let enabled = matches!(values.get(3), Some(Value::Bool(true))); + if !enabled { + continue; + } + let Some(Value::Text(policy_name)) = values.first() else { + continue; + }; + let Some(Value::Text(policy_table)) = values.get(1) else { + continue; + }; + if !identifiers_equal(policy_table, table_name) { + continue; + } + let Some(Value::Text(using_sql)) = values.get(2) else { + continue; + }; + policies.push(ActiveRowPolicy { + name: policy_name.clone(), + expr: parse_sql_statement(&format!("SELECT {using_sql}")).and_then( + |statement| { + let Statement::Query(query) = statement else { + return Err(DbError::sql("policy expression did not parse as SELECT")); + }; + let QueryBody::Select(select) = query.body else { + return Err(DbError::sql("policy expression did not parse as SELECT")); + }; + let Some(SelectItem::Expr { expr, .. }) = select.projection.first() else { + return Err(DbError::sql("policy expression is not scalar")); + }; + Ok(expr.clone()) + }, + )?, + }); + } + Ok(policies) + } + fn active_column_masks(&self) -> Result> { + let Some(row_source) = self.visible_table_row_source(crate::security::MASKS_TABLE) else { + return Ok(Vec::new()); + }; + let mut masks = Vec::new(); + for row in row_source.rows() { + let row = row?; + let values = row.values(); + if !matches!(values.get(4), Some(Value::Bool(true))) { + continue; + } + let ( + Some(Value::Text(_mask_name)), + Some(Value::Text(table_name)), + Some(Value::Text(column_name)), + Some(Value::Text(expression_sql)), + ) = (values.first(), values.get(1), values.get(2), values.get(3)) + else { + continue; + }; + masks.push(ActiveColumnMask { + table_name: table_name.clone(), + column_name: column_name.clone(), + expr: parse_sql_statement(&format!("SELECT {expression_sql}")).and_then( + |statement| { + let Statement::Query(query) = statement else { + return Err(DbError::sql("mask expression did not parse as SELECT")); + }; + let QueryBody::Select(select) = query.body else { + return Err(DbError::sql("mask expression did not parse as SELECT")); + }; + let Some(SelectItem::Expr { expr, .. }) = select.projection.first() else { + return Err(DbError::sql("mask expression is not scalar")); + }; + Ok(expr.clone()) + }, + )?, + }); + } + Ok(masks) + } + pub(crate) fn security_rules_active(&self) -> Result { + if let Some(row_source) = self.visible_table_row_source(crate::security::POLICIES_TABLE) { + for row in row_source.rows() { + if matches!(row?.values().get(3), Some(Value::Bool(true))) { + return Ok(true); + } + } + } + self.security_masks_active() + } + pub(crate) fn security_masks_active(&self) -> Result { + let Some(row_source) = self.visible_table_row_source(crate::security::MASKS_TABLE) else { + return Ok(false); + }; + for row in row_source.rows() { + if matches!(row?.values().get(4), Some(Value::Bool(true))) { + return Ok(true); + } + } + Ok(false) + } + pub(crate) fn masked_output_value( + &self, + binding: &ColumnBinding, + value: &Value, + dataset: &Dataset, + row: &[Value], + params: &[Value], + ctes: &BTreeMap, + ) -> Result { + let masks = self.active_column_masks()?; + let exact = masks.iter().find(|mask| { + identifiers_equal(&mask.column_name, &binding.name) + && binding + .source_table + .as_deref() + .or(binding.table.as_deref()) + .is_some_and(|table| identifiers_equal(table, &mask.table_name)) + }); + let display_table = if exact.is_none() { + binding.table.as_deref().filter(|table| { + binding + .source_table + .as_deref() + .is_none_or(|source| !identifiers_equal(source, table)) + }) + } else { + None + }; + let alias_match = display_table.and_then(|table| { + let matches = masks + .iter() + .filter(|mask| { + identifiers_equal(&mask.column_name, &binding.name) + && identifiers_equal(&mask.table_name, table) + }) + .collect::>(); + if matches.len() == 1 { + matches.first().copied() + } else { + None + } + }); + let fallback = if exact.is_none() && alias_match.is_none() { + let matches = masks + .iter() + .filter(|mask| identifiers_equal(&mask.column_name, &binding.name)) + .collect::>(); + if matches.len() == 1 { + matches.first().copied() + } else { + None + } + } else { + None + }; + if let Some(mask) = exact.or(alias_match).or(fallback) { + self.eval_expr(&mask.expr, dataset, row, params, ctes, None) + } else { + Ok(value.clone()) + } + } +} diff --git a/crates/decentdb/src/exec/grouped.rs b/crates/decentdb/src/exec/grouped.rs new file mode 100644 index 00000000..7df41887 --- /dev/null +++ b/crates/decentdb/src/exec/grouped.rs @@ -0,0 +1,572 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +pub(crate) fn aggregate_matches_single_binding_column( + expr: &Expr, + aggregate_name: &str, + binding: TableBindingRef<'_>, + column: &str, +) -> bool { + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return false; + }; + if !name.eq_ignore_ascii_case(aggregate_name) + || *distinct + || *star + || !order_by.is_empty() + || *within_group + || args.len() != 1 + { + return false; + } + expr_matches_binding_column(&args[0], binding, column) +} + +pub(crate) fn aggregate_matches_count_star(expr: &Expr) -> bool { + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return false; + }; + name.eq_ignore_ascii_case("count") + && *star + && args.is_empty() + && !*distinct + && order_by.is_empty() + && !*within_group +} + +pub(crate) fn aggregate_matches_single_binding_column_or_unqualified( + expr: &Expr, + aggregate_name: &str, + binding: TableBindingRef<'_>, + column: &str, +) -> bool { + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return false; + }; + if !name.eq_ignore_ascii_case(aggregate_name) + || *distinct + || *star + || !order_by.is_empty() + || *within_group + || args.len() != 1 + { + return false; + } + expr_matches_binding_column_or_unqualified(&args[0], binding, column) +} + +pub(crate) fn aggregate_matches_status_case_sum( + expr: &Expr, + aggregate_name: &str, + binding: TableBindingRef<'_>, + status_column: &str, + status_value: &str, +) -> bool { + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return false; + }; + if !name.eq_ignore_ascii_case(aggregate_name) + || *distinct + || *star + || !order_by.is_empty() + || *within_group + || args.len() != 1 + { + return false; + } + let Expr::Case { + operand: None, + branches, + else_expr: Some(else_expr), + } = &args[0] + else { + return false; + }; + if branches.len() != 1 + || !matches!(&branches[0].1, Expr::Literal(Value::Int64(1))) + || !matches!(&**else_expr, Expr::Literal(Value::Int64(0))) + { + return false; + } + status_case_condition_matches(&branches[0].0, binding, status_column, status_value) +} + +pub(crate) fn aggregate_matches_binding_product( + expr: &Expr, + aggregate_name: &str, + left_binding: TableBindingRef<'_>, + left_column: &str, + right_binding: TableBindingRef<'_>, + right_column: &str, +) -> bool { + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return false; + }; + if !name.eq_ignore_ascii_case(aggregate_name) + || *distinct + || *star + || !order_by.is_empty() + || *within_group + || args.len() != 1 + { + return false; + } + let Expr::Binary { left, op, right } = &args[0] else { + return false; + }; + if *op != BinaryOp::Mul { + return false; + } + (expr_matches_binding_column(left, left_binding, left_column) + && expr_matches_binding_column(right, right_binding, right_column)) + || (expr_matches_binding_column(left, right_binding, right_column) + && expr_matches_binding_column(right, left_binding, left_column)) +} + +impl EngineRuntime { + pub(crate) fn runtime_index_key_values_to_group_values<'a>( + key: &'a [u8], + row_id: Option, + row_source: Option>, + projection_indexes: &[usize], + ) -> Result>> { + if let Some(value) = Self::decode_runtime_index_group_key(key) { + return Ok(Some(vec![value])); + } + + let (row_source, row_id) = match (row_source, row_id) { + (Some(row_source), Some(row_id)) => (row_source, row_id), + _ => return Ok(None), + }; + row_source.projected_values_by_id(row_id, projection_indexes) + } + pub(crate) fn try_execute_general_grouped_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_general_grouped_single_table_query(query) else { + return Ok(None); + }; + let Some(source) = self.visible_table_row_source(plan.table_name) else { + return Ok(None); + }; + Ok(Some(self.execute_general_grouped_from_source( + source, &plan, params, + )?)) + } + fn analyze_general_grouped_single_table_query<'a>( + &self, + query: &'a Query, + ) -> Option> { + if !query.ctes.is_empty() || query.recursive { + return None; + } + let QueryBody::Select(select) = &query.body else { + return None; + }; + if select.group_by.is_empty() && !projection_has_aggregate_items(&select.projection) { + return None; + } + if select.from.len() != 1 { + return None; + } + let FromItem::Table { name, alias } = &select.from[0] else { + return None; + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + { + return None; + } + self.table_schema(name)?; + if select.projection.iter().any(|item| { + matches!( + item, + SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) + ) + }) { + return None; + } + for item in &select.projection { + if let SelectItem::Expr { expr, .. } = item { + if expr_contains_window(expr) { + return None; + } + } + } + if select.having.as_ref().is_some_and(expr_contains_window) { + return None; + } + for order in &query.order_by { + if expr_contains_window(&order.expr) { + return None; + } + } + if select.filter.as_ref().is_some_and(expr_contains_window) { + return None; + } + for gb in &select.group_by { + if expr_contains_window(gb) { + return None; + } + } + Some(GeneralGroupedSingleTablePlan { + table_name: name, + table_alias: alias.as_deref(), + group_by: &select.group_by, + filter: select.filter.as_ref(), + projection: &select.projection, + having: select.having.as_ref(), + order_by: &query.order_by, + distinct: select.distinct, + limit: query.limit.as_ref(), + offset: query.offset.as_ref(), + }) + } + fn execute_general_grouped_from_source( + &self, + source: VisibleTableRowSource<'_>, + plan: &GeneralGroupedSingleTablePlan<'_>, + params: &[Value], + ) -> Result { + let table = self.table_schema(plan.table_name).ok_or_else(|| { + DbError::internal(format!( + "table {} not found for general grouped query", + plan.table_name + )) + })?; + + let binding_name = plan.table_alias.unwrap_or(plan.table_name); + let columns: Vec = table + .columns + .iter() + .map(|c| ColumnBinding::visible(Some(binding_name.to_string()), c.name.clone())) + .collect(); + + let empty_dataset = Dataset::with_rows(columns.clone(), Vec::new()); + let ctes = BTreeMap::new(); + let needs_virtual_generated = !generated_columns_are_stored(table); + + // Materialize filtered rows into a single flat buffer and store only + // row indices per group. Previously this path stored full row clones + // per group and then cloned them again into a per-group `Dataset`; + // that doubled the copy cost and regressed aggregate-over-full-table + // queries by ~2x versus the prior `evaluate_grouped_select` path. + // Indexing into one shared Dataset matches that older path's + // efficiency while keeping the streaming scan from `source.rows()`. + let mut all_rows: Vec> = Vec::new(); + let mut groups: BTreeMap, Vec> = BTreeMap::new(); + + for row_result in source.rows() { + let row_ref = row_result?; + let mut values = row_ref.values().to_vec(); + if needs_virtual_generated { + self.apply_virtual_generated_columns(table, &mut values)?; + } + + if let Some(filter) = plan.filter { + let val = self.eval_expr(filter, &empty_dataset, &values, params, &ctes, None)?; + if !matches!(val, Value::Bool(true)) { + continue; + } + } + + let key_values: Vec = plan + .group_by + .iter() + .map(|expr| self.eval_expr(expr, &empty_dataset, &values, params, &ctes, None)) + .collect::>>()?; + let key = row_identity(&key_values)?; + + let row_index = all_rows.len(); + all_rows.push(values); + groups.entry(key).or_default().push(row_index); + } + + if groups.is_empty() && plan.group_by.is_empty() { + groups.insert(Vec::new(), Vec::new()); + } + + let result_columns: Vec = plan + .projection + .iter() + .enumerate() + .map(|(index, item)| match item { + SelectItem::Expr { expr, alias } => ColumnBinding::visible( + None, + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + ), + _ => ColumnBinding::visible(None, format!("col{}", index + 1)), + }) + .collect(); + + let has_order_by = !plan.order_by.is_empty(); + let projection_order_plan = projection_order_by_plan(plan.order_by, plan.projection); + let mut output_with_order: Vec<(Vec, Vec)> = Vec::new(); + + // Build a single shared dataset that every group indexes into. This + // replaces the per-group `Dataset::with_rows(columns.clone(), + // group_rows.clone())` that previously cloned every matching row + // twice. + let group_dataset = Dataset::with_rows(columns.clone(), all_rows); + + for group_row_indexes in groups.values() { + if let Some(having) = plan.having { + let val = + self.eval_group_expr(having, &group_dataset, group_row_indexes, params, &ctes)?; + if !matches!(val, Value::Bool(true)) { + continue; + } + } + + let mut output = Vec::with_capacity(plan.projection.len()); + for item in plan.projection { + let SelectItem::Expr { expr, .. } = item else { + return Err(DbError::sql( + "wildcards not supported in grouped SELECT output", + )); + }; + output.push(self.eval_group_expr( + expr, + &group_dataset, + group_row_indexes, + params, + &ctes, + )?); + } + + let order_values = if let Some(order_plan) = &projection_order_plan { + order_plan + .iter() + .map(|plan| output[plan.projection_index].clone()) + .collect() + } else if has_order_by { + plan.order_by + .iter() + .map(|order| { + self.eval_group_expr( + &order.expr, + &group_dataset, + group_row_indexes, + params, + &ctes, + ) + }) + .collect::>>()? + } else { + Vec::new() + }; + + output_with_order.push((output, order_values)); + } + + if has_order_by { + let mut sort_error = None; + output_with_order.sort_by(|(_, left_order), (_, right_order)| { + if let Some(order_plan) = &projection_order_plan { + let mut ord = std::cmp::Ordering::Equal; + for (i, plan) in order_plan.iter().enumerate() { + match compare_values_with_runtime_collation( + Some(self), + &left_order[i], + &right_order[i], + plan.collation.clone(), + ) { + Ok(std::cmp::Ordering::Equal) => continue, + Ok(o) => { + ord = if plan.descending { o.reverse() } else { o }; + break; + } + Err(error) => { + if sort_error.is_none() { + sort_error = Some(error); + } + break; + } + } + } + ord + } else { + match compare_query_row_order_values( + Some(self), + left_order, + right_order, + plan.order_by, + ) { + Ok(ordering) => ordering, + Err(error) => { + if sort_error.is_none() { + sort_error = Some(error); + } + std::cmp::Ordering::Equal + } + } + } + }); + if let Some(error) = sort_error { + return Err(error); + } + } + + let mut rows: Vec = if plan.distinct { + let mut seen = BTreeSet::new(); + let mut distinct_rows = Vec::new(); + for (output, _) in output_with_order { + if seen.insert(row_identity(&output)?) { + distinct_rows.push(QueryRow::new(output)); + } + } + distinct_rows + } else { + output_with_order + .into_iter() + .map(|(output, _)| QueryRow::new(output)) + .collect() + }; + + let offset_val = plan + .offset + .map(|expr| self.eval_constant_i64(expr, params, &ctes)) + .transpose()? + .unwrap_or(0); + let limit_val = plan + .limit + .map(|expr| self.eval_constant_i64(expr, params, &ctes)) + .transpose()?; + + let start = usize::try_from(offset_val.max(0)).unwrap_or(usize::MAX); + if start > 0 || limit_val.is_some() { + let take = limit_val + .map(|l| usize::try_from(l.max(0)).unwrap_or(0)) + .unwrap_or(usize::MAX); + rows = rows.into_iter().skip(start).take(take).collect(); + } + + let column_names: Vec = result_columns.into_iter().map(|c| c.name).collect(); + Ok(QueryResult::with_rows(column_names, rows)) + } + #[allow(clippy::too_many_arguments)] + pub(crate) fn apply_simple_grouped_postprocessing( + &self, + rows: I, + column_names: Vec, + having_bindings: &[ColumnBinding], + having: Option<&Expr>, + params: &[Value], + order_by: Option<&[SimpleOrderByPlan]>, + limit: Option, + offset: usize, + ) -> Result + where + I: IntoIterator, + { + let bounded_order = order_by + .and_then(|order_by| limit.map(|limit| (order_by, offset.saturating_add(limit)))); + let mut rows_out = if let Some((_, bounded_row_count)) = bounded_order { + if bounded_row_count == 0 { + return Ok(QueryResult::with_rows(column_names, Vec::new())); + } + Vec::with_capacity(bounded_row_count) + } else { + Vec::new() + }; + + let mut push_row = |row: QueryRow| -> Result<()> { + if let Some((order_by, bounded_row_count)) = bounded_order { + push_bounded_projection_ordered_query_row( + Some(self), + &mut rows_out, + row, + order_by, + bounded_row_count, + ) + } else { + rows_out.push(row); + Ok(()) + } + }; + + if let Some(having) = having { + let having_dataset = Dataset::with_rows(having_bindings.to_vec(), Vec::new()); + let ctes = BTreeMap::new(); + for row in rows { + if matches!( + self.eval_expr(having, &having_dataset, row.values(), params, &ctes, None)?, + Value::Bool(true) + ) { + push_row(row)?; + } + } + } else { + for row in rows { + push_row(row)?; + } + } + + if let Some((order_by, _)) = bounded_order { + sort_query_rows_by_projection_order(Some(self), &mut rows_out, order_by)?; + let rows = rows_out + .into_iter() + .skip(offset) + .take(limit.unwrap_or(usize::MAX)) + .collect(); + return Ok(QueryResult::with_rows(column_names, rows)); + } + + if let Some(order_by) = order_by { + sort_query_rows_by_projection_order(Some(self), &mut rows_out, order_by)?; + } + + let rows = rows_out + .into_iter() + .skip(offset) + .take(limit.unwrap_or(usize::MAX)) + .collect(); + Ok(QueryResult::with_rows(column_names, rows)) + } +} diff --git a/crates/decentdb/src/exec/indexes.rs b/crates/decentdb/src/exec/indexes.rs new file mode 100644 index 00000000..26da1061 --- /dev/null +++ b/crates/decentdb/src/exec/indexes.rs @@ -0,0 +1,833 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +pub(crate) fn build_runtime_index( + index: &IndexSchema, + runtime: &EngineRuntime, + page_size: u32, +) -> Result { + let table = runtime.catalog.table(&index.table_name).ok_or_else(|| { + DbError::corruption(format!( + "index {} references missing table {}", + index.name, index.table_name + )) + })?; + let source = runtime.table_row_source(&index.table_name).ok_or_else(|| { + DbError::corruption(format!("table data for {} is missing", index.table_name)) + })?; + + match index.kind { + IndexKind::Btree => { + let int64_keys = btree_uses_typed_int64_keys(index, table); + let uuid_keys = btree_uses_typed_uuid_keys(index, table); + let mut covering = covering_payloads_for_index(index, table); + if index.unique && int64_keys { + let mut keys = UniqueInt64Keys::new(); + for row in source.rows() { + let row = row?; + let Some(key) = compute_index_key(runtime, index, table, row.values())? else { + continue; + }; + let RuntimeBtreeKey::Int64(key) = key else { + return Err(DbError::internal( + "typed INT64 runtime index received an encoded key", + )); + }; + if keys.insert(key, row.row_id()).is_some() { + return Err(DbError::corruption(format!( + "unique index {} contains duplicate keys", + index.name + ))); + } + if let Some(covering) = covering.as_mut() { + if let Some(values) = + covering_payload_values_for_row(index, table, row.values()) + { + covering.insert_row_values(row.row_id(), values); + } + } + } + Ok(RuntimeIndex::Btree { + keys: RuntimeBtreeKeys::UniqueInt64(Arc::new(keys), BTreeSet::new()), + covering, + }) + } else if index.unique && uuid_keys { + let mut keys = BTreeMap::<[u8; 16], i64>::new(); + for row in source.rows() { + let row = row?; + let Some(key) = compute_index_key(runtime, index, table, row.values())? else { + continue; + }; + let RuntimeBtreeKey::Uuid(key) = key else { + return Err(DbError::internal( + "typed UUID runtime index received an encoded key", + )); + }; + if keys.insert(key, row.row_id()).is_some() { + return Err(DbError::corruption(format!( + "unique index {} contains duplicate keys", + index.name + ))); + } + if let Some(covering) = covering.as_mut() { + if let Some(values) = + covering_payload_values_for_row(index, table, row.values()) + { + covering.insert_row_values(row.row_id(), values); + } + } + } + Ok(RuntimeIndex::Btree { + keys: RuntimeBtreeKeys::UniqueUuid(Arc::new(keys), BTreeSet::new()), + covering, + }) + } else if index.unique { + let mut keys = BTreeMap::::new(); + for row in source.rows() { + let row = row?; + let Some(key) = compute_index_key(runtime, index, table, row.values())? else { + continue; + }; + let RuntimeBtreeKey::Encoded(key) = key else { + return Err(DbError::internal( + "encoded runtime index received an INT64 key", + )); + }; + if keys.insert(key, row.row_id()).is_some() { + return Err(DbError::corruption(format!( + "unique index {} contains duplicate keys", + index.name + ))); + } + if let Some(covering) = covering.as_mut() { + if let Some(values) = + covering_payload_values_for_row(index, table, row.values()) + { + covering.insert_row_values(row.row_id(), values); + } + } + } + Ok(RuntimeIndex::Btree { + keys: RuntimeBtreeKeys::UniqueEncoded(Arc::new(keys), BTreeSet::new()), + covering, + }) + } else if int64_keys { + let mut keys = NonUniqueInt64Keys::new(); + for row in source.rows() { + let row = row?; + let Some(key) = compute_index_key(runtime, index, table, row.values())? else { + continue; + }; + let RuntimeBtreeKey::Int64(key) = key else { + return Err(DbError::internal( + "typed INT64 runtime index received an encoded key", + )); + }; + keys.insert_row_id(key, row.row_id()); + if let Some(covering) = covering.as_mut() { + if let Some(values) = + covering_payload_values_for_row(index, table, row.values()) + { + covering.insert_row_values(row.row_id(), values); + } + } + } + Ok(RuntimeIndex::Btree { + keys: RuntimeBtreeKeys::NonUniqueInt64(Arc::new(keys), BTreeSet::new()), + covering, + }) + } else if uuid_keys { + let mut keys = BTreeMap::<[u8; 16], Vec>::new(); + for row in source.rows() { + let row = row?; + let Some(key) = compute_index_key(runtime, index, table, row.values())? else { + continue; + }; + let RuntimeBtreeKey::Uuid(key) = key else { + return Err(DbError::internal( + "typed UUID runtime index received an encoded key", + )); + }; + keys.entry(key).or_default().push(row.row_id()); + if let Some(covering) = covering.as_mut() { + if let Some(values) = + covering_payload_values_for_row(index, table, row.values()) + { + covering.insert_row_values(row.row_id(), values); + } + } + } + Ok(RuntimeIndex::Btree { + keys: RuntimeBtreeKeys::NonUniqueUuid(Arc::new(keys), BTreeSet::new()), + covering, + }) + } else { + let mut keys = BTreeMap::::new(); + // Pre-parse the partial-index predicate once instead of + // re-parsing the predicate SQL for every row in the table + // (row_satisfies_index_predicate parses on each call). Also + // pre-resolve the single indexed column position for the + // common single-column plain-column index so the build loop + // avoids per-row column lookups and Value clones. + let predicate_expr = index + .predicate_sql + .as_ref() + .map(|sql| crate::sql::parser::parse_expression_sql(sql)) + .transpose()?; + let single_column_position = single_plain_index_column_position(index, table); + let multi_column_positions = if single_column_position.is_none() { + plain_index_column_positions(index, table) + } else { + None + }; + let has_virtual_generated = !generated_columns_are_stored(table); + for row in source.rows() { + let row = row?; + let values = row.values(); + if let Some(predicate_expr) = &predicate_expr { + let row_materialized = if has_virtual_generated { + let mut materialized = values.to_vec(); + runtime.apply_virtual_generated_columns(table, &mut materialized)?; + Cow::Owned(materialized) + } else { + Cow::Borrowed(values) + }; + let row_for_eval = row_materialized.as_ref(); + let dataset = table_row_dataset(table, row_for_eval, &table.name); + let bindings = dataset.rows.first().map(Vec::as_slice).unwrap_or(&[]); + if !matches!( + runtime.eval_expr( + predicate_expr, + &dataset, + bindings, + &[], + &BTreeMap::new(), + None + )?, + Value::Bool(true) + ) { + continue; + } + } + let key = if let Some(position) = single_column_position { + // Fast path: encode the single indexed column value + // directly from the borrowed row slice, avoiding the + // intermediate Value clone that compute_index_values + // would perform. + encode_runtime_index_key(&values[position])? + } else if let Some(positions) = &multi_column_positions { + // Fast path for composite plain-column indexes: read + // each indexed column value by position and encode the + // composite key without building a Dataset. + let key_values: Vec = positions + .iter() + .map(|position| values.get(*position).cloned().unwrap_or(Value::Null)) + .collect(); + if index.unique && key_values.iter().any(|v| matches!(v, Value::Null)) { + continue; + } + RuntimeEncodedKey::from_vec(Row::new(key_values).encode()?) + } else { + let Some(encoded) = compute_index_key(runtime, index, table, values)? + else { + continue; + }; + let RuntimeBtreeKey::Encoded(encoded) = encoded else { + return Err(DbError::internal( + "encoded runtime index received an INT64 key", + )); + }; + encoded + }; + match keys.entry(key) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(RuntimeEncodedRowIds::one(row.row_id())); + } + std::collections::btree_map::Entry::Occupied(mut entry) => { + entry.get_mut().push(row.row_id()); + } + } + if let Some(covering) = covering.as_mut() { + if let Some(values) = covering_payload_values_for_row(index, table, values) + { + covering.insert_row_values(row.row_id(), values); + } + } + } + Ok(RuntimeIndex::Btree { + keys: RuntimeBtreeKeys::NonUniqueEncoded( + Arc::new(RuntimeEncodedPostings::new(keys)), + BTreeSet::new(), + ), + covering, + }) + } + } + IndexKind::Trigram => { + let mut trigram = TrigramIndex::new(page_size, 100_000); + let mut builder = TrigramIndexBuilder::new(); + // Fast path: trigram indexes are constrained by DDL to a single + // plain text column with no predicate. Resolve its position once + // and read the text directly, avoiding the per-row Dataset + // construction in compute_index_values. + let single_text_position = plain_single_text_index_column_position(index, table); + let has_predicate = index.predicate_sql.is_some(); + let predicate_expr = index + .predicate_sql + .as_ref() + .map(|sql| crate::sql::parser::parse_expression_sql(sql)) + .transpose()?; + let has_virtual_generated = !generated_columns_are_stored(table); + for row in source.rows() { + let row = row?; + let values = row.values(); + if has_predicate { + if let Some(predicate_expr) = &predicate_expr { + let row_materialized = if has_virtual_generated { + let mut materialized = values.to_vec(); + runtime.apply_virtual_generated_columns(table, &mut materialized)?; + Cow::Owned(materialized) + } else { + Cow::Borrowed(values) + }; + let row_for_eval = row_materialized.as_ref(); + let dataset = table_row_dataset(table, row_for_eval, &table.name); + let bindings = dataset.rows.first().map(Vec::as_slice).unwrap_or(&[]); + if !matches!( + runtime.eval_expr( + predicate_expr, + &dataset, + bindings, + &[], + &BTreeMap::new(), + None + )?, + Value::Bool(true) + ) { + continue; + } + } + } + let text = if let Some(position) = single_text_position { + match values.get(position) { + Some(Value::Text(text)) => Some(text.clone()), + // NULL or non-text: skip, matching compute_index_values + // which would error on non-text for a trigram index. + _ => None, + } + } else { + compute_index_values(runtime, index, table, values)? + .into_iter() + .next() + .and_then(|value| match value { + Value::Text(text) => Some(text), + _ => None, + }) + }; + if let Some(text) = text { + builder.insert(row.row_id() as u64, &text); + } + } + builder.finish_into(&mut trigram)?; + Ok(RuntimeIndex::Trigram { index: trigram }) + } + IndexKind::Spatial => { + let backend = spatial_index_backend(index, table)?; + let mut spatial = SpatialRuntimeIndex::new(backend); + for row in source.rows() { + let row = row?; + if let Some(value) = + spatial_index_value_for_row(runtime, index, table, row.values())? + { + spatial.insert(row.row_id(), value).map_err(spatial_error)?; + } + } + Ok(RuntimeIndex::Spatial { index: spatial }) + } + IndexKind::FullText => { + let config = index + .full_text + .clone() + .ok_or_else(|| DbError::corruption("fulltext index is missing analyzer config"))?; + let mut fulltext = FullTextIndexBuilder::with_capacity(config, source.row_count()); + // Fast path: fulltext indexes are constrained by DDL to plain text + // columns with no predicate. Resolve their positions once and read + // the text directly, avoiding the per-row Dataset construction in + // full_text_fields_for_row / compute_index_values. + let text_positions = plain_text_index_column_positions(index, table); + let has_predicate = index.predicate_sql.is_some(); + let predicate_expr = index + .predicate_sql + .as_ref() + .map(|sql| crate::sql::parser::parse_expression_sql(sql)) + .transpose()?; + let has_virtual_generated = !generated_columns_are_stored(table); + for row in source.rows() { + let row = row?; + let values = row.values(); + if has_predicate { + if let Some(predicate_expr) = &predicate_expr { + let row_materialized = if has_virtual_generated { + let mut materialized = values.to_vec(); + runtime.apply_virtual_generated_columns(table, &mut materialized)?; + Cow::Owned(materialized) + } else { + Cow::Borrowed(values) + }; + let row_for_eval = row_materialized.as_ref(); + let dataset = table_row_dataset(table, row_for_eval, &table.name); + let bindings = dataset.rows.first().map(Vec::as_slice).unwrap_or(&[]); + if !matches!( + runtime.eval_expr( + predicate_expr, + &dataset, + bindings, + &[], + &BTreeMap::new(), + None + )?, + Value::Bool(true) + ) { + continue; + } + } + } + if let Some(positions) = &text_positions { + match positions.len() { + 1 => { + let text_ref = positions + .first() + .and_then(|position| values.get(*position)) + .and_then(|value| value.as_text()); + let fields = [text_ref]; + fulltext.add_row(row.row_id() as u64, &fields); + } + 2 => { + let fields = [ + values.get(positions[0]).and_then(Value::as_text), + values.get(positions[1]).and_then(Value::as_text), + ]; + fulltext.add_row(row.row_id() as u64, &fields); + } + _ => { + let field_refs: Vec> = positions + .iter() + .map(|position| match values.get(*position) { + Some(Value::Text(text)) => Some(text.as_str()), + _ => None, + }) + .collect(); + fulltext.add_row(row.row_id() as u64, &field_refs); + } + } + } else { + let fields = full_text_fields_for_row(runtime, index, table, values)?; + let field_refs = fields.iter().map(Option::as_deref).collect::>(); + fulltext.add_row(row.row_id() as u64, &field_refs); + } + } + Ok(RuntimeIndex::FullText { + index: fulltext.finish(), + }) + } + } +} + +impl EngineRuntime { + pub(crate) fn refresh_paged_lookup_cache_and_pk_index( + &mut self, + db: &crate::db::Db, + store: &DbTxnPageStore<'_>, + table_name: &str, + state: PersistedTableState, + ) -> Result> { + let needs_locator_cache = self.should_cache_deferred_paged_row_locators(table_name); + if !db.config().persistent_pk_index && !needs_locator_cache { + self.deferred_paged_row_locator_caches_mut() + .remove(table_name); + return Ok(None); + } + + let chunk_payloads = read_paged_table_chunk_payloads(store, state)?; + self.refresh_paged_lookup_cache_and_pk_index_from_chunks( + db, + table_name, + state, + &chunk_payloads, + ) + } + pub(crate) fn refresh_paged_lookup_cache_and_pk_index_from_chunks( + &mut self, + db: &crate::db::Db, + table_name: &str, + state: PersistedTableState, + chunk_payloads: &[TablePageManifestChunk], + ) -> Result> { + let needs_locator_cache = self.should_cache_deferred_paged_row_locators(table_name); + if needs_locator_cache { + self.cache_deferred_paged_row_locators(table_name, state, chunk_payloads)?; + } else { + self.deferred_paged_row_locator_caches_mut() + .remove(table_name); + } + + if db.config().persistent_pk_index { + build_persistent_pk_index_root_from_chunk_payloads(db, chunk_payloads) + } else { + Ok(None) + } + } + pub(crate) fn rebuild_indexes(&mut self, page_size: u32) -> Result<()> { + let indexes = self.catalog.indexes.values().cloned().collect::>(); + let mut rebuilt: BTreeMap> = BTreeMap::new(); + for index in indexes { + rebuilt.insert( + index.name.clone(), + Arc::new(build_runtime_index(&index, self, page_size)?), + ); + } + *self.indexes_mut() = rebuilt; + for index in self.catalog_mut().indexes.values_mut() { + index.fresh = true; + } + self.manifest_template = None; + self.index_state_epoch = self.index_state_epoch.wrapping_add(1); + Ok(()) + } + pub(crate) fn rebuild_index(&mut self, name: &str, page_size: u32) -> Result<()> { + let index = self + .catalog + .index(name) + .cloned() + .ok_or_else(|| DbError::sql(format!("unknown index {name}")))?; + let rebuilt = build_runtime_index(&index, self, page_size)?; + self.indexes_mut() + .insert(name.to_string(), Arc::new(rebuilt)); + if let Some(index) = self.catalog_mut().indexes.get_mut(name) { + index.fresh = true; + } + self.manifest_template = None; + self.index_state_epoch = self.index_state_epoch.wrapping_add(1); + Ok(()) + } + pub(crate) fn verify_index(&self, name: &str, page_size: u32) -> Result<()> { + self.catalog + .index(name) + .ok_or_else(|| DbError::sql(format!("unknown index {name}")))?; + let existing = self.index(name).map_or(0, runtime_index_entry_count); + let mut rebuilt = self.clone(); + rebuilt.rebuild_index(name, page_size)?; + let actual = rebuilt.index(name).map_or(0, runtime_index_entry_count); + if existing != actual { + return Err(DbError::corruption(format!( + "index {name} verification failed: expected {existing} entries, rebuilt {actual}" + ))); + } + Ok(()) + } + pub(super) fn mark_indexes_stale_for_table(&mut self, table_name: &str) { + if self.visible_table_is_temporary(table_name) { + return; + } + let Some(table_name) = self.canonical_catalog_table_name(table_name) else { + return; + }; + let index_names = self + .catalog + .indexes + .values() + .filter(|index| identifiers_equal(&index.table_name, &table_name)) + .map(|index| index.name.clone()) + .collect::>(); + self.mark_named_indexes_stale(&index_names); + } + /// Mark only the named indexes (and their catalog entries) as stale, + /// discarding any in-memory runtime index for them. Used when a DML + /// successfully incrementally updates some indexes on a table but fails to + /// incrementally update others — the successful ones stay fresh and the + /// failed ones are rebuilt on next access. + pub(super) fn mark_named_indexes_stale(&mut self, index_names: &[String]) { + if index_names.is_empty() { + return; + } + let mut changed = false; + { + let catalog = self.catalog_mut(); + for name in index_names { + if let Some(index) = catalog.indexes.get_mut(name) { + if index.fresh { + index.fresh = false; + changed = true; + } + } + } + } + if changed { + self.manifest_template = None; + } + let indexes = self.indexes_mut(); + let mut any_removed = false; + for name in index_names { + if indexes.remove(name).is_some() { + any_removed = true; + } + } + if changed || any_removed { + self.index_state_epoch = self.index_state_epoch.wrapping_add(1); + } + } + pub(super) fn prepare_insert_index_updates( + &mut self, + table_name: &str, + row: &StoredRow, + page_size: u32, + ) -> Result> { + if self.visible_table_is_temporary(table_name) { + return Ok(Vec::new()); + } + let Some(canonical_table_name) = self.canonical_catalog_table_name(table_name) else { + return Ok(Vec::new()); + }; + let table = self + .table_schema(table_name) + .cloned() + .ok_or_else(|| DbError::sql(format!("unknown table {table_name}")))?; + let indexes = self + .catalog + .indexes + .values() + .filter(|index| { + identifiers_equal(&index.table_name, &canonical_table_name) && index.fresh + }) + .cloned() + .collect::>(); + let mut updates = Vec::new(); + + for index in indexes { + if !self.indexes.contains_key(&index.name) { + self.rebuild_index(&index.name, page_size)?; + } + + match index.kind { + IndexKind::Btree => { + let Some(key) = compute_index_key(self, &index, &table, &row.values)? else { + continue; + }; + if index.unique { + self.remove_tombstoned_unique_btree_entries_for_insert( + &canonical_table_name, + &index, + &key, + )?; + } + updates.push(PendingIndexInsert::Btree { + name: index.name.clone(), + key, + row_id: row.row_id, + covering_values: covering_payload_values_for_row( + &index, + &table, + &row.values, + ), + }); + } + IndexKind::Trigram => { + if !row_satisfies_index_predicate(self, &index, &table, &row.values)? { + continue; + } + let text = compute_index_values(self, &index, &table, &row.values)? + .into_iter() + .next() + .ok_or_else(|| { + DbError::constraint("trigram index requires a single text expression") + })?; + let Value::Text(text) = text else { + return Err(DbError::constraint( + "trigram index requires a single text expression", + )); + }; + updates.push(PendingIndexInsert::Trigram { + name: index.name.clone(), + row_id: row.row_id as u64, + text, + }); + } + IndexKind::Spatial => { + if let Some(value) = + spatial_index_value_for_row(self, &index, &table, &row.values)? + { + updates.push(PendingIndexInsert::Spatial { + name: index.name.clone(), + row_id: row.row_id, + value, + }); + } + } + IndexKind::FullText => { + if !row_satisfies_index_predicate(self, &index, &table, &row.values)? { + continue; + } + let fields = full_text_fields_for_row(self, &index, &table, &row.values)?; + updates.push(PendingIndexInsert::FullText { + name: index.name.clone(), + row_id: row.row_id as u64, + fields, + }); + } + } + } + + Ok(updates) + } + fn remove_tombstoned_unique_btree_entries_for_insert( + &mut self, + table_name: &str, + index: &IndexSchema, + key: &RuntimeBtreeKey, + ) -> Result<()> { + let Some(row_source) = self.visible_table_row_source(table_name) else { + return Ok(()); + }; + if !row_source.has_tombstoned_rows() { + return Ok(()); + } + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { + return Ok(()); + }; + let mut stale_row_ids = Vec::new(); + for row_id in keys.row_ids_for_key(key) { + if row_source.row_by_id(row_id)?.is_none() { + stale_row_ids.push(row_id); + } + } + if stale_row_ids.is_empty() { + return Ok(()); + } + let Some(RuntimeIndex::Btree { keys, covering }) = self.index_mut(&index.name) else { + return Ok(()); + }; + for row_id in stale_row_ids { + keys.remove_row_id(key, row_id)?; + if let Some(covering) = covering.as_mut() { + covering.remove_row_id(row_id); + } + } + Ok(()) + } + pub(super) fn apply_insert_index_updates( + &mut self, + updates: Vec, + ) -> Result<()> { + for update in updates { + match update { + PendingIndexInsert::Btree { + name, + key, + row_id, + covering_values, + } => match self.index_mut(&name) { + Some(RuntimeIndex::Btree { keys, covering }) => { + keys.insert_row_id(key, row_id)?; + if let (Some(covering), Some(values)) = (covering.as_mut(), covering_values) + { + covering.insert_row_values(row_id, values); + } + } + Some(_) => { + return Err(DbError::internal(format!( + "runtime index {name} is not a BTREE index" + ))) + } + None => { + return Err(DbError::internal(format!( + "runtime index {name} is missing" + ))) + } + }, + PendingIndexInsert::Trigram { name, row_id, text } => match self.index_mut(&name) { + Some(RuntimeIndex::Trigram { index }) => { + index.queue_insert(row_id, &text); + } + Some(_) => { + return Err(DbError::internal(format!( + "runtime index {name} is not a trigram index" + ))) + } + None => { + return Err(DbError::internal(format!( + "runtime index {name} is missing" + ))) + } + }, + PendingIndexInsert::Spatial { + name, + row_id, + value, + } => match self.index_mut(&name) { + Some(RuntimeIndex::Spatial { index }) => { + index.insert(row_id, value).map_err(spatial_error)?; + } + Some(_) => { + return Err(DbError::internal(format!( + "runtime index {name} is not a SPATIAL index" + ))) + } + None => { + return Err(DbError::internal(format!( + "runtime index {name} is missing" + ))) + } + }, + PendingIndexInsert::FullText { + name, + row_id, + fields, + } => match self.index_mut(&name) { + Some(RuntimeIndex::FullText { index }) => { + let refs = fields.iter().map(Option::as_deref).collect::>(); + index.insert_document(row_id, &refs); + } + Some(_) => { + return Err(DbError::internal(format!( + "runtime index {name} is not a fulltext index" + ))) + } + None => { + return Err(DbError::internal(format!( + "runtime index {name} is missing" + ))) + } + }, + } + } + Ok(()) + } + pub(crate) fn indexes_for_table(&self, table_name: &str) -> Vec<&IndexSchema> { + let (qualifier, object) = compat_schema_qualified_name(table_name); + let mut indexes = self + .catalog + .indexes + .values() + .filter(|index| { + qualifier != Some(CompatSchemaQualifier::Temp) + && identifiers_equal(&index.table_name, object) + }) + .chain(self.temp_indexes.values().filter(|index| { + qualifier != Some(CompatSchemaQualifier::Main) + && identifiers_equal(&index.table_name, object) + })) + .collect::>(); + indexes.sort_by(|left, right| left.name.cmp(&right.name)); + indexes + } + pub(crate) fn index_by_name(&self, index_name: &str) -> Option<&IndexSchema> { + let (qualifier, object) = compat_schema_qualified_name(index_name); + match qualifier { + Some(CompatSchemaQualifier::Main) => map_get_ci(&self.catalog.indexes, object), + Some(CompatSchemaQualifier::Temp) => map_get_ci(&self.temp_indexes, object), + None => map_get_ci(&self.catalog.indexes, object) + .or_else(|| map_get_ci(&self.temp_indexes, object)), + } + } +} diff --git a/crates/decentdb/src/exec/joins.rs b/crates/decentdb/src/exec/joins.rs new file mode 100644 index 00000000..61a3a888 --- /dev/null +++ b/crates/decentdb/src/exec/joins.rs @@ -0,0 +1,2665 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +/// Resolves the projected position of a join step's previous (outer) column. +/// Returns `None` when the column is the previous table's row-id alias, in +/// which case the join key is available directly from the row's `row_id` and is +/// not present in the projection. +pub(crate) fn join_key_projection_index( + step: &DeferredViewJoinStep, + previous_table_projection: &DeferredViewTableProjection, +) -> Result> { + if step.previous_is_rowid_alias { + return Ok(None); + } + previous_table_projection + .position(step.previous_column_index) + .map(Some) + .ok_or_else(|| { + DbError::internal( + "deferred view linear join projection is missing required join column", + ) + }) +} + +pub(crate) fn join_constraint_matches_columns( + on: &Expr, + left_binding: TableBindingRef<'_>, + left_column: &str, + right_binding: TableBindingRef<'_>, + right_column: &str, +) -> bool { + let Some((left_ref, right_ref)) = simple_join_equality(on) else { + return false; + }; + (matches_table_binding(left_binding, left_ref.table) + && identifiers_equal(left_ref.column, left_column) + && matches_table_binding(right_binding, right_ref.table) + && identifiers_equal(right_ref.column, right_column)) + || (matches_table_binding(left_binding, right_ref.table) + && identifiers_equal(right_ref.column, left_column) + && matches_table_binding(right_binding, left_ref.table) + && identifiers_equal(left_ref.column, right_column)) +} + +pub(crate) fn join_constraints_match_columns( + constraints: &[&Expr], + left_binding: TableBindingRef<'_>, + left_column: &str, + right_binding: TableBindingRef<'_>, + right_column: &str, +) -> bool { + constraints.iter().any(|constraint| { + simple_join_equalities(constraint).is_some_and(|equalities| { + equalities.iter().any(|(left_ref, right_ref)| { + (matches_table_binding(left_binding, left_ref.table) + && identifiers_equal(left_ref.column, left_column) + && matches_table_binding(right_binding, right_ref.table) + && identifiers_equal(right_ref.column, right_column)) + || (matches_table_binding(left_binding, right_ref.table) + && identifiers_equal(right_ref.column, left_column) + && matches_table_binding(right_binding, left_ref.table) + && identifiers_equal(left_ref.column, right_column)) + }) + }) + }) +} + +pub(crate) fn join_output_columns( + left: &Dataset, + right: &Dataset, + using_columns: &[JoinUsingColumn], +) -> Vec { + if using_columns.is_empty() { + let mut columns = left.columns.clone(); + columns.extend(right.columns.clone()); + return columns; + } + + let left_hidden = using_columns + .iter() + .map(|column| column.left_index) + .collect::>(); + let right_hidden = using_columns + .iter() + .map(|column| column.right_index) + .collect::>(); + + let mut columns = + Vec::with_capacity(using_columns.len() + left.columns.len() + right.columns.len()); + for column in using_columns { + columns.push(ColumnBinding::visible(None, column.name.clone())); + } + for (index, binding) in left.columns.iter().enumerate() { + let mut binding = binding.clone(); + if left_hidden.contains(&index) { + binding.hidden = true; + } + columns.push(binding); + } + for (index, binding) in right.columns.iter().enumerate() { + let mut binding = binding.clone(); + if right_hidden.contains(&index) { + binding.hidden = true; + } + columns.push(binding); + } + columns +} + +pub(crate) fn merged_join_value(left: &Value, right: &Value) -> Value { + if matches!(left, Value::Null) { + right.clone() + } else { + left.clone() + } +} + +pub(crate) fn join_output_row( + left_row: &[Value], + right_row: &[Value], + using_columns: &[JoinUsingColumn], +) -> Result> { + if using_columns.is_empty() { + let mut row = left_row.to_vec(); + row.extend_from_slice(right_row); + return Ok(row); + } + + let mut row = Vec::with_capacity(using_columns.len() + left_row.len() + right_row.len()); + for column in using_columns { + let left_value = left_row + .get(column.left_index) + .ok_or_else(|| DbError::internal("left join row is shorter than its bindings"))?; + let right_value = right_row + .get(column.right_index) + .ok_or_else(|| DbError::internal("right join row is shorter than its bindings"))?; + row.push(merged_join_value(left_value, right_value)); + } + row.extend_from_slice(left_row); + row.extend_from_slice(right_row); + Ok(row) +} + +pub(crate) fn join_rows_match( + constraint: &JoinConstraint, + using_columns: &[JoinUsingColumn], + eval_row: &[Value], + left_row: &[Value], + right_row: &[Value], + context: &JoinEvalContext<'_>, +) -> Result { + match constraint { + JoinConstraint::On(on) => Ok(matches!( + context.runtime.eval_expr( + on, + context.dataset, + eval_row, + context.params, + context.ctes, + None + )?, + Value::Bool(true) + )), + JoinConstraint::Using(_) | JoinConstraint::Natural => { + for column in using_columns { + let left_value = left_row.get(column.left_index).ok_or_else(|| { + DbError::internal("left join row is shorter than its bindings") + })?; + let right_value = right_row.get(column.right_index).ok_or_else(|| { + DbError::internal("right join row is shorter than its bindings") + })?; + if matches!(left_value, Value::Null) || matches!(right_value, Value::Null) { + return Ok(false); + } + if compare_values(left_value, right_value)? != std::cmp::Ordering::Equal { + return Ok(false); + } + } + Ok(true) + } + } +} + +pub(crate) fn nested_loop_join( + left: Dataset, + right: Dataset, + kind: JoinKind, + constraint: &JoinConstraint, + runtime: &EngineRuntime, + params: &[Value], + ctes: &BTreeMap, +) -> Result { + let using_columns = resolve_join_using_columns(&left, &right, constraint)?; + + let mut eval_columns = left.columns.clone(); + eval_columns.extend(right.columns.clone()); + let eval_dataset = Dataset::with_rows(eval_columns, Vec::new()); + let eval_context = JoinEvalContext { + dataset: &eval_dataset, + runtime, + params, + ctes, + }; + let columns = join_output_columns(&left, &right, &using_columns); + let mut rows = Vec::new(); + let mut matched_right = vec![false; right.rows.len()]; + let left_nulls = vec![Value::Null; left.columns.len()]; + let right_nulls = vec![Value::Null; right.columns.len()]; + for left_row in left.rows.iter() { + let mut matched = false; + for (right_index, right_row) in right.rows.iter().enumerate() { + let mut eval_row = left_row.clone(); + eval_row.extend(right_row.clone()); + if join_rows_match( + constraint, + &using_columns, + &eval_row, + left_row, + right_row, + &eval_context, + )? { + matched = true; + matched_right[right_index] = true; + rows.push(join_output_row(left_row, right_row, &using_columns)?); + } + } + if !matched && matches!(kind, JoinKind::Left | JoinKind::Full) { + rows.push(join_output_row(left_row, &right_nulls, &using_columns)?); + } + } + if matches!(kind, JoinKind::Right | JoinKind::Full) { + for (matched, right_row) in matched_right.iter().zip(right.rows.iter()) { + if !matched { + rows.push(join_output_row(&left_nulls, right_row, &using_columns)?); + } + } + } + Ok(Dataset::with_rows(columns, rows)) +} + +impl EngineRuntime { + pub(crate) fn try_execute_left_join_aggregate_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_left_join_aggregate_query(query, params)? else { + return Ok(None); + }; + let Some(parent_source) = self.visible_table_row_source(plan.parent_table_name) else { + return Ok(None); + }; + let Some(child_source) = self.visible_table_row_source(plan.child_table_name) else { + return Ok(None); + }; + let child_index_keys = + plan.child_index_name + .as_deref() + .and_then(|index_name| match self.index(index_name) { + Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), + _ => None, + }); + + if child_index_keys.is_none() { + return Ok(None); + } + let keys = child_index_keys.unwrap(); + + let bounded_order = plan + .order_by + .as_deref() + .zip(plan.limit) + .filter(|(_, _)| plan.offset == 0); + let mut rows = Vec::new(); + + for parent_row in parent_source.rows() { + let parent_row = parent_row?; + let parent_values = parent_row.values(); + let Some(join_value) = parent_values.get(plan.parent_join_index) else { + return Err(DbError::internal("parent join row is shorter than schema")); + }; + + let mut state = IndexedJoinAggregateState::new(&plan.aggregate_kinds); + let mut matched_child = false; + + if !matches!(join_value, Value::Null) { + let child_row_ids = keys.row_ids_for_value_set(join_value)?; + match child_row_ids { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(child_row_id) => { + let Some(child_row) = child_source.row_by_id(child_row_id)? else { + return Err(DbError::internal("child index referenced missing row id")); + }; + matched_child = true; + state.accumulate(child_row.values())?; + } + RuntimeRowIdSet::Contiguous { start, len } => { + for child_row_id in contiguous_row_ids(start, len) { + let Some(child_row) = child_source.row_by_id(child_row_id)? else { + return Err(DbError::internal( + "child index referenced missing row id", + )); + }; + matched_child = true; + state.accumulate(child_row.values())?; + } + } + RuntimeRowIdSet::Many(row_ids) => { + for child_row_id in row_ids { + let Some(child_row) = child_source.row_by_id(*child_row_id)? else { + return Err(DbError::internal( + "child index referenced missing row id", + )); + }; + matched_child = true; + state.accumulate(child_row.values())?; + } + } + RuntimeRowIdSet::Owned(row_ids) => { + for child_row_id in row_ids { + let Some(child_row) = child_source.row_by_id(child_row_id)? else { + return Err(DbError::internal( + "child index referenced missing row id", + )); + }; + matched_child = true; + state.accumulate(child_row.values())?; + } + } + } + } + + if !matched_child && !plan.include_empty_parent { + continue; + } + + let mut output = + Vec::with_capacity(plan.group_column_indexes.len() + plan.aggregate_kinds.len()); + for index in &plan.group_column_indexes { + output.push(parent_values[*index].clone()); + } + state.finalize_into(&mut output); + let row = QueryRow::new(output); + + if let Some((order_by, limit)) = bounded_order { + push_bounded_projection_ordered_query_row( + Some(self), + &mut rows, + row, + order_by, + limit, + )?; + } else { + rows.push(row); + } + } + + if let Some((order_by, _)) = bounded_order { + sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; + return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); + } + + Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + plan.column_names, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + )?)) + } + pub(crate) fn execute_indexed_join_limit_projection_plan( + &self, + plan: &IndexedJoinLimitPlan<'_>, + ) -> Result { + let sources = plan + .tables + .iter() + .map(|table| { + self.visible_table_row_source(table.name).ok_or_else(|| { + DbError::internal(format!("table {} row source is missing", table.name)) + }) + }) + .collect::>>()?; + let keys = plan + .steps + .iter() + .map(|step| { + let Some(index_name) = step.right_index_name.as_deref() else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(index_name) else { + return Err(DbError::internal(format!( + "index {index_name} is missing for indexed join limit plan", + ))); + }; + Ok(Some(keys)) + }) + .collect::>>()?; + + let mut rows = Vec::new(); + let mut offset_remaining = plan.offset; + let mut limit_remaining = plan.limit; + for root_row in sources[0].rows() { + let root_row = root_row?; + let current0 = root_row.values().to_vec(); + if plan.tables.len() == 2 { + let step0 = &plan.steps[0]; + let Some(probe_value) = current0.get(step0.previous_column_index) else { + return Err(DbError::internal("join probe row is shorter than schema")); + }; + for row1 in indexed_join_limit_rows_for_value(sources[1], keys[0], probe_value)? { + let current = [¤t0[..], &row1[..]]; + if push_indexed_join_limit_projection( + ¤t, + &plan.projections, + &mut offset_remaining, + &mut limit_remaining, + &mut rows, + ) { + return Ok(indexed_join_limit_result(plan, rows)); + } + } + } else { + let step0 = &plan.steps[0]; + let Some(probe_value0) = current0.get(step0.previous_column_index) else { + return Err(DbError::internal("join probe row is shorter than schema")); + }; + for row1 in indexed_join_limit_rows_for_value(sources[1], keys[0], probe_value0)? { + let current01 = [¤t0[..], &row1[..]]; + let step1 = &plan.steps[1]; + let Some(probe_value1) = current01 + .get(step1.previous_table_index) + .and_then(|row| row.get(step1.previous_column_index)) + else { + return Err(DbError::internal("join probe row is shorter than schema")); + }; + for row2 in + indexed_join_limit_rows_for_value(sources[2], keys[1], probe_value1)? + { + let current = [¤t0[..], &row1[..], &row2[..]]; + if push_indexed_join_limit_projection( + ¤t, + &plan.projections, + &mut offset_remaining, + &mut limit_remaining, + &mut rows, + ) { + return Ok(indexed_join_limit_result(plan, rows)); + } + } + } + } + } + Ok(indexed_join_limit_result(plan, rows)) + } + pub(crate) fn execute_ordered_indexed_join_limit_projection_plan( + &self, + plan: &IndexedJoinLimitPlan<'_>, + root_filter: Option<&Expr>, + root_filter_columns: Option>, + order_index_name: &str, + descending: bool, + params: &[Value], + ) -> Result { + let sources = plan + .tables + .iter() + .map(|table| { + self.visible_table_row_source(table.name).ok_or_else(|| { + DbError::internal(format!("table {} row source is missing", table.name)) + }) + }) + .collect::>>()?; + let keys = plan + .steps + .iter() + .map(|step| { + let Some(index_name) = step.right_index_name.as_deref() else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(index_name) else { + return Err(DbError::internal(format!( + "index {index_name} is missing for ordered indexed join limit plan", + ))); + }; + Ok(Some(keys)) + }) + .collect::>>()?; + let Some(RuntimeIndex::Btree { + keys: order_keys, .. + }) = self.index(order_index_name) + else { + return Err(DbError::internal(format!( + "ordered index {order_index_name} is missing for ordered view limit plan", + ))); + }; + + let root_filter_dataset = + root_filter_columns.map(|columns| Dataset::with_rows(columns, Vec::new())); + let mut rows = Vec::new(); + let mut offset_remaining = plan.offset; + let mut limit_remaining = plan.limit; + let ctes = BTreeMap::new(); + + visit_runtime_btree_row_ids_in_order(order_keys, descending, |root_row_id| { + let Some(root_row) = sources[0].row_by_id(root_row_id)? else { + return Ok(false); + }; + if let (Some(filter), Some(dataset)) = (root_filter, root_filter_dataset.as_ref()) { + if !matches!( + self.eval_expr(filter, dataset, root_row.values(), params, &ctes, None)?, + Value::Bool(true) + ) { + return Ok(false); + } + } + + if plan.tables.len() == 2 { + let step0 = &plan.steps[0]; + let Some(probe_value) = root_row.values().get(step0.previous_column_index) else { + return Err(DbError::internal("join probe row is shorter than schema")); + }; + for row1_id in indexed_join_row_ids_for_value(keys[0], probe_value)? { + let Some(row1) = sources[1].row_by_id(row1_id)? else { + continue; + }; + let current = [root_row.values(), row1.values()]; + if push_indexed_join_limit_projection( + ¤t, + &plan.projections, + &mut offset_remaining, + &mut limit_remaining, + &mut rows, + ) { + return Ok(true); + } + } + } else { + let step0 = &plan.steps[0]; + let Some(probe_value0) = root_row.values().get(step0.previous_column_index) else { + return Err(DbError::internal("join probe row is shorter than schema")); + }; + for row1_id in indexed_join_row_ids_for_value(keys[0], probe_value0)? { + let Some(row1) = sources[1].row_by_id(row1_id)? else { + continue; + }; + let current01 = [root_row.values(), row1.values()]; + let step1 = &plan.steps[1]; + let Some(probe_value1) = current01 + .get(step1.previous_table_index) + .and_then(|row| row.get(step1.previous_column_index)) + else { + return Err(DbError::internal("join probe row is shorter than schema")); + }; + for row2_id in indexed_join_row_ids_for_value(keys[1], probe_value1)? { + let Some(row2) = sources[2].row_by_id(row2_id)? else { + continue; + }; + let current = [root_row.values(), row1.values(), row2.values()]; + if push_indexed_join_limit_projection( + ¤t, + &plan.projections, + &mut offset_remaining, + &mut limit_remaining, + &mut rows, + ) { + return Ok(true); + } + } + } + } + Ok(false) + })?; + + Ok(indexed_join_limit_result(plan, rows)) + } + pub(crate) fn execute_indexed_join_projection_rows( + &self, + plan: &IndexedJoinLimitPlan<'_>, + enforce_root_rowid_order: bool, + second_table_order_column: Option, + ) -> Result> { + if plan.tables.len() != 3 || plan.steps.len() != 2 { + return Err(DbError::internal( + "indexed join projection rows path expects a three-table chain", + )); + } + let sources = plan + .tables + .iter() + .map(|table| { + self.visible_table_row_source(table.name).ok_or_else(|| { + DbError::internal(format!("table {} row source is missing", table.name)) + }) + }) + .collect::>>()?; + let keys = plan + .steps + .iter() + .map(|step| { + let Some(index_name) = step.right_index_name.as_deref() else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(index_name) else { + return Err(DbError::internal(format!( + "index {index_name} is missing for indexed join projection plan", + ))); + }; + Ok(Some(keys)) + }) + .collect::>>()?; + + let mut root_row_ids = Vec::with_capacity(sources[0].row_count()); + for root_row in sources[0].rows() { + root_row_ids.push(root_row?.row_id()); + } + if enforce_root_rowid_order { + root_row_ids.sort_unstable(); + } + + let mut rows = Vec::new(); + for root_row_id in root_row_ids { + let Some(root_row) = sources[0].row_by_id(root_row_id)? else { + continue; + }; + let step0 = &plan.steps[0]; + let Some(probe_value0) = root_row.values().get(step0.previous_column_index) else { + return Err(DbError::internal("join probe row is shorter than schema")); + }; + let mut row1_ids = indexed_join_row_ids_for_value(keys[0], probe_value0)?; + if let Some(column_index) = second_table_order_column { + row1_ids = sort_join_row_ids_by_column(sources[1], row1_ids, column_index)?; + } + for row1_id in row1_ids { + let Some(row1) = sources[1].row_by_id(row1_id)? else { + continue; + }; + let current01 = [root_row.values(), row1.values()]; + let step1 = &plan.steps[1]; + let Some(probe_value1) = current01 + .get(step1.previous_table_index) + .and_then(|row| row.get(step1.previous_column_index)) + else { + return Err(DbError::internal("join probe row is shorter than schema")); + }; + for row2_id in indexed_join_row_ids_for_value(keys[1], probe_value1)? { + let Some(row2) = sources[2].row_by_id(row2_id)? else { + continue; + }; + let current = [root_row.values(), row1.values(), row2.values()]; + rows.push(project_indexed_join_row(¤t, &plan.projections)?); + } + } + } + Ok(rows) + } + pub(crate) fn execute_base_table_join_from_sources( + &self, + left_source: VisibleTableRowSource<'_>, + right_source: VisibleTableRowSource<'_>, + plan: &BaseTableJoinPlan<'_>, + params: &[Value], + ) -> Result { + let left_table = self.table_schema(plan.left_name).ok_or_else(|| { + DbError::internal(format!("table {} not found for join", plan.left_name)) + })?; + let right_table = self.table_schema(plan.right_name).ok_or_else(|| { + DbError::internal(format!("table {} not found for join", plan.right_name)) + })?; + + let left_binding_name = plan.left_alias.unwrap_or(plan.left_name); + let right_binding_name = plan.right_alias.unwrap_or(plan.right_name); + + let left_columns: Vec = left_table + .columns + .iter() + .map(|c| { + ColumnBinding::visible_source( + Some(left_binding_name.to_string()), + Some(plan.left_name.to_string()), + c.name.clone(), + ) + }) + .collect(); + let right_columns: Vec = right_table + .columns + .iter() + .map(|c| { + ColumnBinding::visible_source( + Some(right_binding_name.to_string()), + Some(plan.right_name.to_string()), + c.name.clone(), + ) + }) + .collect(); + + let using_columns = resolve_join_using_columns_for_schemas( + &left_columns, + &right_columns, + plan.constraint, + left_table, + right_table, + )?; + + let eval_columns: Vec = left_columns + .iter() + .cloned() + .chain(right_columns.iter().cloned()) + .collect(); + let eval_dataset = Dataset::with_rows(eval_columns, Vec::new()); + let ctes = BTreeMap::new(); + + let left_needs_virtual_generated = !generated_columns_are_stored(left_table); + let right_needs_virtual_generated = !generated_columns_are_stored(right_table); + + let left_nulls = vec![Value::Null; left_columns.len()]; + let right_nulls = vec![Value::Null; right_columns.len()]; + + let mut right_rows: Vec> = Vec::new(); + for row_result in right_source.rows() { + let row_ref = row_result?; + let mut values = row_ref.values().to_vec(); + if right_needs_virtual_generated { + self.apply_virtual_generated_columns(right_table, &mut values)?; + } + right_rows.push(values); + } + + let mut matched_right = vec![false; right_rows.len()]; + let mut join_output: Vec> = Vec::new(); + + for left_row_result in left_source.rows() { + let mut left_values = left_row_result?.values().to_vec(); + if left_needs_virtual_generated { + self.apply_virtual_generated_columns(left_table, &mut left_values)?; + } + + let mut matched = false; + for (right_index, right_values) in right_rows.iter().enumerate() { + let mut eval_row = left_values.clone(); + eval_row.extend(right_values.clone()); + if join_rows_match( + plan.constraint, + &using_columns, + &eval_row, + &left_values, + right_values, + &JoinEvalContext { + dataset: &eval_dataset, + runtime: self, + params, + ctes: &ctes, + }, + )? { + matched = true; + matched_right[right_index] = true; + join_output.push(join_output_row(&left_values, right_values, &using_columns)?); + } + } + if !matched && matches!(plan.kind, JoinKind::Left | JoinKind::Full) { + join_output.push(join_output_row(&left_values, &right_nulls, &using_columns)?); + } + } + if matches!(plan.kind, JoinKind::Right | JoinKind::Full) { + for (matched, right_values) in matched_right.iter().zip(right_rows.iter()) { + if !matched { + join_output.push(join_output_row(&left_nulls, right_values, &using_columns)?); + } + } + } + + let result_columns: Vec = plan + .projection + .iter() + .enumerate() + .map(|(index, item)| match item { + SelectItem::Expr { expr, alias } => ColumnBinding::visible( + None, + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + ), + _ => ColumnBinding::visible(None, format!("col{}", index + 1)), + }) + .collect(); + + let join_ds_columns: Vec = left_columns + .iter() + .cloned() + .chain(right_columns.iter().cloned()) + .collect(); + let mut join_dataset = Dataset::with_rows(join_ds_columns, join_output); + if let Some(filter) = plan.filter { + let filter_ds = Dataset::with_rows(join_dataset.columns.clone(), Vec::new()); + let mut filtered = Vec::with_capacity(join_dataset.rows.len()); + for row in join_dataset.take_rows() { + let val = self.eval_expr(filter, &filter_ds, &row, params, &ctes, None)?; + if matches!(val, Value::Bool(true)) { + filtered.push(row); + } + } + join_dataset.set_rows(filtered); + } + + let mut output_rows: Vec> = Vec::new(); + for row in join_dataset.rows.iter() { + let mut projected = Vec::with_capacity(plan.projection.len()); + for item in plan.projection { + let SelectItem::Expr { expr, .. } = item else { + return Err(DbError::sql( + "wildcards not supported in join SELECT output", + )); + }; + projected.push(self.eval_expr(expr, &join_dataset, row, params, &ctes, None)?); + } + output_rows.push(projected); + } + + let has_order_by = !plan.order_by.is_empty(); + let mut rows_with_order: Vec<(Vec, Vec)> = if has_order_by { + let order_ds = Dataset::with_rows(result_columns.clone(), output_rows.clone()); + output_rows + .into_iter() + .map(|row| { + let order_values: Vec = plan + .order_by + .iter() + .map(|order| { + self.eval_expr(&order.expr, &order_ds, &row, params, &ctes, None) + }) + .collect::>>()?; + Ok((row, order_values)) + }) + .collect::>>()? + } else { + output_rows + .into_iter() + .map(|row| (row, Vec::new())) + .collect() + }; + + if has_order_by { + let mut sort_error = None; + rows_with_order.sort_by(|(_, left_order), (_, right_order)| { + match compare_query_row_order_values( + Some(self), + left_order, + right_order, + plan.order_by, + ) { + Ok(ordering) => ordering, + Err(error) => { + if sort_error.is_none() { + sort_error = Some(error); + } + std::cmp::Ordering::Equal + } + } + }); + if let Some(error) = sort_error { + return Err(error); + } + } + + let mut rows: Vec = if plan.distinct { + let mut seen = BTreeSet::new(); + let mut distinct_rows = Vec::new(); + for (output, _) in rows_with_order { + if seen.insert(row_identity(&output)?) { + distinct_rows.push(QueryRow::new(output)); + } + } + distinct_rows + } else { + rows_with_order + .into_iter() + .map(|(output, _)| QueryRow::new(output)) + .collect() + }; + + let offset_val = plan + .offset + .map(|expr| self.eval_constant_i64(expr, params, &ctes)) + .transpose()? + .unwrap_or(0); + let limit_val = plan + .limit + .map(|expr| self.eval_constant_i64(expr, params, &ctes)) + .transpose()?; + + let start = usize::try_from(offset_val.max(0)).unwrap_or(usize::MAX); + if start > 0 || limit_val.is_some() { + let take = limit_val + .map(|l| usize::try_from(l.max(0)).unwrap_or(0)) + .unwrap_or(usize::MAX); + rows = rows.into_iter().skip(start).take(take).collect(); + } + + let column_names: Vec = result_columns.into_iter().map(|c| c.name).collect(); + Ok(QueryResult::with_rows(column_names, rows)) + } + #[allow(clippy::too_many_arguments)] + pub(crate) fn stream_deferred_view_join_rows_from_root( + &self, + store: &S, + table_row_readers: &[DeferredViewTableRowReader<'_>], + join_steps: &[DeferredViewJoinStep], + join_keys: &[&RuntimeBtreeKeys], + key_projection_indexes: &[Option], + table_projections: &[DeferredViewTableProjection], + root_row: StoredRow, + partial_rows: &mut Vec, + use_persistent_pk_index: bool, + require_index: bool, + chunk_payload_cache: &mut HashMap>>, + visit: &mut F, + ) -> Result> + where + F: FnMut(&[StoredRow]) -> Result, + { + #[allow(clippy::too_many_arguments)] + fn walk_join_rows( + store: &S, + table_row_readers: &[DeferredViewTableRowReader<'_>], + join_steps: &[DeferredViewJoinStep], + table_projections: &[DeferredViewTableProjection], + join_keys: &[&RuntimeBtreeKeys], + key_projection_indexes: &[Option], + step_index: usize, + partial_rows: &mut Vec, + use_persistent_pk_index: bool, + chunk_payload_cache: &mut HashMap>>, + visit: &mut F, + ) -> Result> + where + F: FnMut(&[StoredRow]) -> Result, + { + if step_index == join_steps.len() { + return Ok(Some(visit(partial_rows.as_slice())?)); + } + + let step = &join_steps[step_index]; + let keys = join_keys[step_index]; + let Some(previous_row) = partial_rows.get(step.previous_table_index) else { + return Err(DbError::internal( + "deferred view limit join row is shorter than the planned schema", + )); + }; + let current_table_index = step.current_table_index; + let current_projection_indexes = + &table_projections[current_table_index].projection_indexes; + + let mut outcome = None; + let row_ids = match key_projection_indexes[step_index] { + Some(projection_index) => { + let Some(key_value) = previous_row.values.get(projection_index) else { + return Err(DbError::internal( + "deferred view join row is shorter than planned schema", + )); + }; + if matches!(key_value, Value::Null) { + return Ok(Some(false)); + } + keys.row_ids_for_value_set(key_value)? + } + None => keys.row_ids_for_row_id(previous_row.row_id), + }; + + let mut visit_row_id = |row_id| -> Result> { + if partial_rows.len() != step.current_table_index { + return Err(DbError::internal( + "deferred view join row is not in expected table order", + )); + } + + let Some(joined_row) = table_row_readers[current_table_index] + .read_projected_with_chunk_cache( + store, + row_id, + use_persistent_pk_index, + current_projection_indexes, + chunk_payload_cache, + )? + else { + return Ok(Some(false)); + }; + partial_rows.push(joined_row); + let child_outcome = walk_join_rows( + store, + table_row_readers, + join_steps, + table_projections, + join_keys, + key_projection_indexes, + step_index + 1, + partial_rows, + use_persistent_pk_index, + chunk_payload_cache, + visit, + )?; + partial_rows.pop(); + Ok(child_outcome) + }; + + match row_ids { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(row_id) => match visit_row_id(row_id)? { + Some(false) => {} + Some(true) => return Ok(Some(true)), + None => return Ok(None), + }, + RuntimeRowIdSet::Contiguous { start, len } => { + for row_id in contiguous_row_ids(start, len) { + if outcome.is_some() { + break; + } + match visit_row_id(row_id)? { + Some(false) => {} + Some(true) => return Ok(Some(true)), + None => { + outcome = Some(None); + break; + } + } + } + } + RuntimeRowIdSet::Many(row_ids) => { + for row_id in row_ids { + if outcome.is_some() { + break; + } + match visit_row_id(*row_id)? { + Some(false) => {} + Some(true) => return Ok(Some(true)), + None => { + outcome = Some(None); + break; + } + } + } + } + RuntimeRowIdSet::Owned(row_ids) => { + for row_id in row_ids { + if outcome.is_some() { + break; + } + match visit_row_id(row_id)? { + Some(false) => {} + Some(true) => return Ok(Some(true)), + None => { + outcome = Some(None); + break; + } + } + } + } + } + + Ok(outcome.unwrap_or(Some(false))) + } + + if join_keys.len() != join_steps.len() || key_projection_indexes.len() != join_steps.len() { + if !require_index { + return Ok(None); + } + return Err(DbError::internal( + "deferred view join metadata is missing while executing index-required join", + )); + } + partial_rows.clear(); + partial_rows.push(root_row); + let result = walk_join_rows( + store, + table_row_readers, + join_steps, + table_projections, + join_keys, + key_projection_indexes, + 0, + partial_rows, + use_persistent_pk_index, + chunk_payload_cache, + visit, + ); + partial_rows.clear(); + result + } + pub(crate) fn try_indexed_scan( + &self, + select: &Select, + params: &[Value], + ctes: &BTreeMap, + ) -> Result> { + let Some(filter) = &select.filter else { + return Ok(None); + }; + if select.from.len() != 1 { + return Ok(None); + } + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if ctes.contains_key(name) + || self + .visible_view(name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(name) + { + return Ok(None); + } + let Some(table) = self.table_schema(name) else { + return Ok(None); + }; + if !generated_columns_are_stored(table) { + return Ok(None); + } + let row_source = self.table_row_source(name); + + if let Some(fulltext_lookup) = simple_fulltext_lookup(filter) { + let index_value = self.eval_expr( + fulltext_lookup.index_name_expr, + &Dataset::empty(), + &[], + params, + ctes, + None, + )?; + let query_value = self.eval_expr( + fulltext_lookup.query_expr, + &Dataset::empty(), + &[], + params, + ctes, + None, + )?; + let Some(index_name) = expect_text_arg("FULLTEXT_MATCH", "first", &index_value)? else { + return Ok(Some(Dataset::with_rows( + table_bindings_with_hidden_row_id(table, alias.as_deref().unwrap_or(name)), + Vec::new(), + ))); + }; + let Some(query_text) = expect_text_arg("FULLTEXT_MATCH", "second", &query_value)? + else { + return Ok(Some(Dataset::with_rows( + table_bindings_with_hidden_row_id(table, alias.as_deref().unwrap_or(name)), + Vec::new(), + ))); + }; + if let Some(index_schema) = self.catalog.index(index_name) { + if identifiers_equal(&index_schema.table_name, name) + && index_schema.fresh + && index_schema.kind == IndexKind::FullText + { + if let Some(RuntimeIndex::FullText { index }) = self.index(&index_schema.name) { + let row_ids = index + .search(query_text) + .map_err(|error| DbError::sql(error.message))? + .into_iter() + .filter_map(|hit| i64::try_from(hit.row_id).ok()) + .collect::>(); + return self + .dataset_from_row_id_set( + table, + row_source, + alias, + RuntimeRowIdSet::Many(&row_ids), + true, + ) + .map(Some); + } + } + } + } + + if let Some(spatial_lookup) = simple_spatial_lookup(filter) { + if !matches_filter_binding(name, alias, spatial_lookup.table_qualifier) { + return Ok(None); + } + if let Some(index) = self.catalog.indexes.values().find(|index| { + identifiers_equal(&index.table_name, name) + && index.fresh + && index.kind == IndexKind::Spatial + && index.predicate_sql.is_none() + && index.columns.len() == 1 + && index.columns[0] + .column_name + .as_deref() + .is_some_and(|index_column| { + identifiers_equal(index_column, spatial_lookup.column_name) + }) + && index.columns[0].expression_sql.is_none() + }) { + let query_value = self.eval_expr( + spatial_lookup.value_expr, + &Dataset::empty(), + &[], + params, + ctes, + None, + )?; + let Some((query_is_geography, query_spatial)) = + spatial_value_from_db(&query_value)? + else { + return Ok(None); + }; + let Some(RuntimeIndex::Spatial { index: spatial }) = self.index(&index.name) else { + return Ok(None); + }; + match (spatial.backend(), query_is_geography) { + (SpatialIndexBackend::GeographyS2, true) + | (SpatialIndexBackend::GeometryQuadCell, false) => {} + _ => return Ok(None), + } + let mut envelope = + SpatialEnvelope::from_value(&query_spatial).map_err(spatial_error)?; + if let Some(radius_expr) = spatial_lookup.radius_expr { + let radius_value = + self.eval_expr(radius_expr, &Dataset::empty(), &[], params, ctes, None)?; + let radius = numeric_value_as_f64("ST_DWithin", "third", &radius_value)?; + envelope = match spatial.backend() { + SpatialIndexBackend::GeographyS2 => { + envelope.expand_geography_meters(radius) + } + SpatialIndexBackend::GeometryQuadCell => envelope.expand_planar(radius), + }; + } + let row_ids = spatial.candidate_row_ids(envelope); + return self + .dataset_from_row_id_set( + table, + row_source, + alias, + RuntimeRowIdSet::Many(&row_ids), + false, + ) + .map(Some); + } + } + + if let Some((table_qualifier, column_name, value_expr)) = simple_btree_lookup(filter) { + if !matches_filter_binding(name, alias, table_qualifier) { + return Ok(None); + } + if let Some(index) = self.catalog.indexes.values().find(|index| { + identifiers_equal(&index.table_name, name) + && index.fresh + && index.kind == IndexKind::Btree + && index.predicate_sql.is_none() + && index.columns.len() == 1 + && index.columns[0] + .column_name + .as_deref() + .is_some_and(|index_column| identifiers_equal(index_column, column_name)) + && index.columns[0].expression_sql.is_none() + }) { + let value = + self.eval_expr(value_expr, &Dataset::empty(), &[], params, ctes, None)?; + if let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) { + let row_ids = keys.row_ids_for_value_set(&value)?; + if let Some(ref tracing) = self.tracing { + tracing.record_index_usage( + name, + &index.name, + "btree", + crate::tracing::index_usage::IndexUsageKind::Read, + ); + } + return self + .dataset_from_row_id_set(table, row_source, alias, row_ids, false) + .map(Some); + } + } + } + + if let Some(row_ids) = + self.trigram_candidate_row_ids_for_filter(name, alias, filter, params, ctes)? + { + return self + .dataset_from_row_id_set( + table, + row_source, + alias, + RuntimeRowIdSet::Many(&row_ids), + false, + ) + .map(Some); + } + + Ok(None) + } + pub(crate) fn try_fulltext_bm25_top_k_select( + &self, + select: &Select, + order_by: &[crate::sql::ast::OrderBy], + limit: Option<&Expr>, + offset: Option<&Expr>, + params: &[Value], + ctes: &BTreeMap, + ) -> Result> { + if offset.is_some() + || select.distinct + || !select.distinct_on.is_empty() + || !select.group_by.is_empty() + || select.having.is_some() + || select.from.len() != 1 + || order_by.len() != 1 + { + return Ok(None); + } + let Some(limit_expr) = limit else { + return Ok(None); + }; + let limit = self.eval_constant_i64(limit_expr, params, ctes)?; + if limit <= 0 { + return Ok(None); + } + let Ok(limit) = usize::try_from(limit) else { + return Ok(None); + }; + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if ctes.contains_key(name) + || self + .visible_view(name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(name) + { + return Ok(None); + } + let Some(table_schema) = self.table_schema(name) else { + return Ok(None); + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + let Some(row_source) = self.table_row_source(name) else { + return Ok(None); + }; + let binding_name = alias.as_deref().unwrap_or(name.as_str()); + let Some(fulltext_lookup) = exact_fulltext_lookup(select.filter.as_ref()) else { + return Ok(None); + }; + let index_value = self.eval_expr( + fulltext_lookup.index_name_expr, + &Dataset::empty(), + &[], + params, + ctes, + None, + )?; + let query_value = self.eval_expr( + fulltext_lookup.query_expr, + &Dataset::empty(), + &[], + params, + ctes, + None, + )?; + let Some(index_name) = expect_text_arg("FULLTEXT_MATCH", "first", &index_value)? else { + return Ok(None); + }; + let Some(query_text) = expect_text_arg("FULLTEXT_MATCH", "second", &query_value)? else { + return Ok(None); + }; + let Some(index_schema) = self.catalog.index(index_name) else { + return Ok(None); + }; + if !identifiers_equal(&index_schema.table_name, name) + || !index_schema.fresh + || index_schema.kind != IndexKind::FullText + { + return Ok(None); + } + if !order_by[0].descending { + return Ok(None); + } + let Some(RuntimeIndex::FullText { index }) = self.index(&index_schema.name) else { + return Ok(None); + }; + + enum ProjectionKind { + Column(usize), + Score, + } + + let mut projection_kinds = Vec::with_capacity(select.projection.len()); + let mut column_names = Vec::with_capacity(select.projection.len()); + let mut score_alias = None; + let mut score_expr = None; + for (item_index, item) in select.projection.iter().enumerate() { + match item { + SelectItem::Expr { expr, alias } => match expr { + Expr::Column { + table: column_table, + column, + } => { + let Some(column_index) = simple_expression_projection_column_index( + table_schema, + name, + binding_name, + column_table.as_deref(), + column, + ) else { + return Ok(None); + }; + projection_kinds.push(ProjectionKind::Column(column_index)); + column_names.push(alias.clone().unwrap_or_else(|| column.clone())); + } + Expr::Function { name, args } + if name.eq_ignore_ascii_case("bm25") && args.len() == 1 => + { + if score_expr.is_some() { + return Ok(None); + } + if !order_by_matches_alias_or_projection( + &order_by[0], + alias.as_deref(), + expr, + true, + ) { + return Ok(None); + } + score_alias = alias.clone(); + score_expr = Some(expr); + projection_kinds.push(ProjectionKind::Score); + column_names.push( + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, item_index + 1)), + ); + } + _ => return Ok(None), + }, + SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return Ok(None), + } + } + let Some(score_expr) = score_expr else { + return Ok(None); + }; + if score_alias.is_none() + && !order_by_matches_alias_or_projection(&order_by[0], None, score_expr, true) + { + return Ok(None); + } + let Expr::Function { args, .. } = score_expr else { + return Ok(None); + }; + let score_index_value = + self.eval_expr(&args[0], &Dataset::empty(), &[], params, ctes, None)?; + let Some(score_index_name) = expect_text_arg("BM25", "first", &score_index_value)? else { + return Ok(None); + }; + if !identifiers_equal(score_index_name, index_name) { + return Ok(None); + } + + let hits = index + .search_top_k(query_text, limit) + .map_err(|error| DbError::sql(error.message))?; + let mut rows = Vec::with_capacity(hits.len()); + for hit in hits { + let Some(row_id) = i64::try_from(hit.row_id).ok() else { + continue; + }; + let Some(row) = row_source.row_by_id(row_id)? else { + continue; + }; + let mut values = Vec::with_capacity(projection_kinds.len()); + for projection_kind in &projection_kinds { + match projection_kind { + ProjectionKind::Column(index) => { + let Some(value) = row.values().get(*index) else { + return Err(DbError::internal( + "fulltext fast path projection index is out of bounds", + )); + }; + values.push(value.clone()); + } + ProjectionKind::Score => values.push(Value::Float64(hit.score)), + } + } + rows.push(values); + } + + let columns = column_names + .into_iter() + .map(|name| ColumnBinding::visible(None, name)) + .collect(); + Ok(Some(Dataset::with_rows(columns, rows))) + } + pub(crate) fn trigram_candidate_row_ids_for_filter( + &self, + table_name: &str, + alias: &Option, + filter: &Expr, + params: &[Value], + ctes: &BTreeMap, + ) -> Result>> { + let Some(lookup) = simple_trigram_lookup(filter) else { + return Ok(None); + }; + if !matches_filter_binding(table_name, alias, lookup.table_qualifier) { + return Ok(None); + } + let Some(index_schema) = self.catalog.indexes.values().find(|index| { + identifiers_equal(&index.table_name, table_name) + && index.fresh + && index.kind == IndexKind::Trigram + && index.predicate_sql.is_none() + && index.columns.len() == 1 + && index.columns[0] + .column_name + .as_deref() + .is_some_and(|index_column| identifiers_equal(index_column, lookup.column_name)) + }) else { + return Ok(None); + }; + let pattern = self.eval_expr( + lookup.pattern_expr, + &Dataset::empty(), + &[], + params, + ctes, + None, + )?; + let Value::Text(pattern) = pattern else { + return Ok(None); + }; + let Some(RuntimeIndex::Trigram { index }) = self.index(&index_schema.name) else { + return Ok(None); + }; + if !index.planner_may_use_index() { + return Ok(None); + } + let row_ids = match index.query_candidates(&pattern, lookup.has_additional_filter)? { + TrigramQueryResult::Candidates(ids) | TrigramQueryResult::Capped(ids) => ids + .into_iter() + .filter_map(|row_id| i64::try_from(row_id).ok()) + .collect::>(), + TrigramQueryResult::FallbackTooShort + | TrigramQueryResult::FallbackRequiresAdditionalFilter + | TrigramQueryResult::RebuildRequired => return Ok(None), + }; + Ok(Some(row_ids)) + } + pub(crate) fn try_spatial_join( + &self, + select: &Select, + params: &[Value], + ctes: &BTreeMap, + ) -> Result> { + if select.from.len() != 1 { + return Ok(None); + } + let FromItem::Join { + left, + right, + kind: JoinKind::Inner, + constraint, + } = &select.from[0] + else { + return Ok(None); + }; + let JoinConstraint::On(on) = constraint else { + return Ok(None); + }; + let (left_name, left_alias) = match &**left { + FromItem::Table { name, alias } => (name, alias), + _ => return Ok(None), + }; + let (right_name, right_alias) = match &**right { + FromItem::Table { name, alias } => (name, alias), + _ => return Ok(None), + }; + if ctes.contains_key(left_name) + || ctes.contains_key(right_name) + || self + .visible_view(left_name, NameResolutionScope::Session) + .is_some() + || self + .visible_view(right_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(left_name) + || self.visible_table_is_temporary(right_name) + { + return Ok(None); + } + + let left_binding = TableBindingRef { + name: left_name, + alias: left_alias, + }; + let right_binding = TableBindingRef { + name: right_name, + alias: right_alias, + }; + let Some(join_predicate) = simple_spatial_join_predicate(on, left_binding, right_binding) + else { + return Ok(None); + }; + + for (indexed_ref, probe_ref) in [ + (join_predicate.left, join_predicate.right), + (join_predicate.right, join_predicate.left), + ] { + let Some((indexed_table, probe_table, indexed_on_left)) = + spatial_join_argument_orientation( + left_binding, + right_binding, + indexed_ref, + probe_ref, + ) + else { + continue; + }; + if let Some(dataset) = self.try_spatial_join_orientation(SpatialJoinOrientation { + indexed_table, + indexed_ref, + probe_table, + probe_ref, + indexed_on_left, + left_alias, + right_alias, + constraint, + radius_expr: join_predicate.radius_expr, + params, + ctes, + })? { + return Ok(Some(dataset)); + } + } + + Ok(None) + } + fn try_spatial_join_orientation( + &self, + plan: SpatialJoinOrientation<'_>, + ) -> Result> { + if !matches_table_binding(plan.indexed_table, plan.indexed_ref.table) + || !matches_table_binding(plan.probe_table, plan.probe_ref.table) + { + return Ok(None); + } + let Some(index_schema) = + self.spatial_index_for_table_column(plan.indexed_table.name, plan.indexed_ref.column) + else { + return Ok(None); + }; + let indexed_table = self.table_schema(plan.indexed_table.name).ok_or_else(|| { + DbError::sql(format!("unknown table or view {}", plan.indexed_table.name)) + })?; + let probe_table = self.table_schema(plan.probe_table.name).ok_or_else(|| { + DbError::sql(format!("unknown table or view {}", plan.probe_table.name)) + })?; + if !generated_columns_are_stored(indexed_table) + || !generated_columns_are_stored(probe_table) + { + return Ok(None); + } + let Some(indexed_source) = self.table_row_source(plan.indexed_table.name) else { + return Ok(None); + }; + let Some(probe_source) = self.table_row_source(plan.probe_table.name) else { + return Ok(None); + }; + let Some(probe_column_index) = schema_column_index(probe_table, plan.probe_ref.column) + else { + return Ok(None); + }; + let Some(RuntimeIndex::Spatial { index: spatial }) = self.index(&index_schema.name) else { + return Ok(None); + }; + + let left_table = if plan.indexed_on_left { + indexed_table + } else { + probe_table + }; + let right_table = if plan.indexed_on_left { + probe_table + } else { + indexed_table + }; + let left_columns = table_output_columns(left_table, plan.left_alias); + let right_columns = table_output_columns(right_table, plan.right_alias); + let mut eval_columns = left_columns.clone(); + eval_columns.extend(right_columns.clone()); + let eval_dataset = Dataset::with_rows(eval_columns, Vec::new()); + let eval_context = JoinEvalContext { + dataset: &eval_dataset, + runtime: self, + params: plan.params, + ctes: plan.ctes, + }; + let columns = join_output_columns( + &Dataset::with_rows(left_columns, Vec::new()), + &Dataset::with_rows(right_columns, Vec::new()), + &[], + ); + let mut rows = Vec::new(); + + for probe_row in probe_source.rows() { + let probe_row = probe_row?; + let Some(probe_value) = probe_row.values().get(probe_column_index) else { + return Err(DbError::internal( + "spatial join probe row is shorter than schema", + )); + }; + let Some((probe_is_geography, probe_spatial)) = spatial_value_from_db(probe_value)? + else { + continue; + }; + match (spatial.backend(), probe_is_geography) { + (SpatialIndexBackend::GeographyS2, true) + | (SpatialIndexBackend::GeometryQuadCell, false) => {} + _ => return Ok(None), + } + let mut envelope = + SpatialEnvelope::from_value(&probe_spatial).map_err(spatial_error)?; + if let Some(radius_expr) = plan.radius_expr { + let radius_value = self.eval_expr( + radius_expr, + &Dataset::empty(), + &[], + plan.params, + plan.ctes, + None, + )?; + let radius = numeric_value_as_f64("ST_DWithin", "third", &radius_value)?; + envelope = match spatial.backend() { + SpatialIndexBackend::GeographyS2 => envelope.expand_geography_meters(radius), + SpatialIndexBackend::GeometryQuadCell => envelope.expand_planar(radius), + }; + } + + for indexed_row_id in spatial.candidate_row_ids(envelope) { + let Some(indexed_row) = indexed_source.row_by_id(indexed_row_id)? else { + continue; + }; + let (left_values, right_values) = if plan.indexed_on_left { + (indexed_row.values(), probe_row.values()) + } else { + (probe_row.values(), indexed_row.values()) + }; + let mut eval_row = left_values.to_vec(); + eval_row.extend_from_slice(right_values); + if join_rows_match( + plan.constraint, + &[], + &eval_row, + left_values, + right_values, + &eval_context, + )? { + rows.push(join_output_row(left_values, right_values, &[])?); + } + } + } + + Ok(Some(Dataset::with_rows(columns, rows))) + } + fn spatial_index_for_table_column( + &self, + table_name: &str, + column_name: &str, + ) -> Option<&IndexSchema> { + self.catalog.indexes.values().find(|index| { + identifiers_equal(&index.table_name, table_name) + && index.fresh + && index.kind == IndexKind::Spatial + && index.predicate_sql.is_none() + && index.columns.len() == 1 + && index.columns[0].expression_sql.is_none() + && index.columns[0] + .column_name + .as_deref() + .is_some_and(|indexed| identifiers_equal(indexed, column_name)) + }) + } + pub(crate) fn try_indexed_prefiltered_inner_join_tree( + &self, + select: &Select, + params: &[Value], + ctes: &BTreeMap, + ) -> Result> { + let Some(filter) = &select.filter else { + return Ok(None); + }; + if select.from.len() != 1 { + return Ok(None); + } + let Some((Some(filter_table), filter_column, value_expr)) = simple_btree_lookup(filter) + else { + return Ok(None); + }; + if !from_item_is_all_inner_table_joins(&select.from[0]) { + return Ok(None); + } + + let mut applied_prefilter = false; + let dataset = self.evaluate_from_item_with_indexed_prefilter( + &select.from[0], + params, + ctes, + filter_table, + filter_column, + value_expr, + &mut applied_prefilter, + )?; + if applied_prefilter { + Ok(Some(dataset)) + } else { + Ok(None) + } + } + #[allow(clippy::too_many_arguments)] + fn evaluate_from_item_with_indexed_prefilter( + &self, + item: &FromItem, + params: &[Value], + ctes: &BTreeMap, + filter_table: &str, + filter_column: &str, + value_expr: &Expr, + applied_prefilter: &mut bool, + ) -> Result { + match item { + FromItem::Table { name, alias } => { + if !*applied_prefilter + && matches_filter_binding(name, alias, Some(filter_table)) + && !ctes.contains_key(name) + && self + .visible_view(name, NameResolutionScope::Session) + .is_none() + && !self.visible_table_is_temporary(name) + { + if let Some(dataset) = self.indexed_table_lookup( + name, + alias, + filter_column, + value_expr, + params, + ctes, + )? { + *applied_prefilter = true; + return Ok(dataset); + } + } + self.evaluate_from_item(item, params, ctes) + } + FromItem::Join { + left, + right, + kind, + constraint, + } => { + let left_dataset = self.evaluate_from_item_with_indexed_prefilter( + left, + params, + ctes, + filter_table, + filter_column, + value_expr, + applied_prefilter, + )?; + if matches!(kind, JoinKind::Inner | JoinKind::Left) { + if let Some(dataset) = self.try_indexed_equi_join_with_right_table( + &left_dataset, + right, + *kind, + constraint, + ctes, + )? { + return Ok(dataset); + } + if let Some(dataset) = self.try_indexed_equi_join_with_right_cte( + &left_dataset, + right, + constraint, + *kind, + ctes, + )? { + return Ok(dataset); + } + } + let right_dataset = self.evaluate_from_item_with_indexed_prefilter( + right, + params, + ctes, + filter_table, + filter_column, + value_expr, + applied_prefilter, + )?; + nested_loop_join( + left_dataset, + right_dataset, + *kind, + constraint, + self, + params, + ctes, + ) + } + _ => self.evaluate_from_item(item, params, ctes), + } + } + pub(crate) fn indexed_table_lookup( + &self, + table_name: &str, + alias: &Option, + column_name: &str, + value_expr: &Expr, + params: &[Value], + ctes: &BTreeMap, + ) -> Result> { + let table = self + .table_schema(table_name) + .ok_or_else(|| DbError::sql(format!("unknown table or view {table_name}")))?; + if !generated_columns_are_stored(table) { + return Ok(None); + } + let row_source = self.table_row_source(table_name); + if row_id_alias_column_name(table) + .is_some_and(|row_id_column| identifiers_equal(row_id_column, column_name)) + { + let Some(row_source) = row_source else { + return Ok(None); + }; + let value = self.eval_expr(value_expr, &Dataset::empty(), &[], params, ctes, None)?; + let row_ids = match value { + Value::Int64(row_id) => RuntimeRowIdSet::Single(row_id), + _ => RuntimeRowIdSet::Empty, + }; + return self + .dataset_from_row_id_set(table, Some(row_source), alias, row_ids, false) + .map(Some); + } + let Some(index) = self.catalog.indexes.values().find(|index| { + identifiers_equal(&index.table_name, table_name) + && index.fresh + && index.kind == IndexKind::Btree + && index.predicate_sql.is_none() + && index.columns.len() == 1 + && index.columns[0] + .column_name + .as_deref() + .is_some_and(|index_column| identifiers_equal(index_column, column_name)) + && index.columns[0].expression_sql.is_none() + }) else { + return Ok(None); + }; + + let value = self.eval_expr(value_expr, &Dataset::empty(), &[], params, ctes, None)?; + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { + return Ok(None); + }; + let row_ids = keys.row_ids_for_value_set(&value)?; + self.dataset_from_row_id_set(table, row_source, alias, row_ids, false) + .map(Some) + } + pub(crate) fn indexed_inner_join_filtered( + &self, + plan: IndexedJoinPlan<'_>, + ) -> Result> { + let filtered_table = self.table_schema(plan.filtered_table.name).ok_or_else(|| { + DbError::sql(format!( + "unknown table or view {}", + plan.filtered_table.name + )) + })?; + let probe_table = self.table_schema(plan.probe_table.name).ok_or_else(|| { + DbError::sql(format!("unknown table or view {}", plan.probe_table.name)) + })?; + if !generated_columns_are_stored(filtered_table) + || !generated_columns_are_stored(probe_table) + { + return Ok(None); + } + let probe_source = self.visible_table_row_source(plan.probe_table.name); + let mut filtered_join_indexes = Vec::with_capacity(plan.filtered_join_columns.len()); + for filtered_join_column in &plan.filtered_join_columns { + let filtered_join_index = filtered_table + .columns + .iter() + .position(|column| identifiers_equal(&column.name, filtered_join_column)) + .ok_or_else(|| DbError::sql(format!("unknown column {filtered_join_column}")))?; + filtered_join_indexes.push(filtered_join_index); + } + let is_probe_rowid_alias = plan.probe_join_columns.len() == 1 + && crate::exec::dml::row_id_alias_column_name(probe_table) + .is_some_and(|name| identifiers_equal(name, plan.probe_join_columns[0])); + + let mut probe_hash_join_indexes = None; + let (probe_index, ordered_filtered_join_indexes) = if is_probe_rowid_alias { + (None, filtered_join_indexes) + } else if let Some((probe_index, ordered_filtered_join_indexes)) = + self.catalog.indexes.values().find_map(|index| { + if !identifiers_equal(&index.table_name, plan.probe_table.name) + || !index.fresh + || index.kind != IndexKind::Btree + || index.predicate_sql.is_some() + || index.columns.len() != plan.probe_join_columns.len() + { + return None; + } + let mut ordered_filtered_join_indexes = Vec::with_capacity(index.columns.len()); + for index_column in &index.columns { + if index_column.expression_sql.is_some() { + return None; + } + let index_column_name = index_column.column_name.as_deref()?; + let join_position = plan.probe_join_columns.iter().position(|join_column| { + identifiers_equal(join_column, index_column_name) + })?; + ordered_filtered_join_indexes.push(filtered_join_indexes[join_position]); + } + Some((index, ordered_filtered_join_indexes)) + }) + { + (Some(probe_index), ordered_filtered_join_indexes) + } else { + let mut ordered_probe_join_indexes = Vec::with_capacity(plan.probe_join_columns.len()); + for probe_join_column in &plan.probe_join_columns { + let Some(probe_join_index) = schema_column_index(probe_table, probe_join_column) + else { + return Ok(None); + }; + ordered_probe_join_indexes.push(probe_join_index); + } + probe_hash_join_indexes = Some(ordered_probe_join_indexes); + (None, filtered_join_indexes) + }; + let keys = if let Some(index) = probe_index { + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { + return Ok(None); + }; + Some(keys) + } else { + None + }; + let probe_hash_rows = if let Some(probe_join_indexes) = probe_hash_join_indexes.as_ref() { + let Some(probe_source) = probe_source else { + return Ok(None); + }; + let mut hashed = SimpleJoinHashRows::new(); + for probe_row in probe_source.rows() { + let probe_row = probe_row?; + let Some(join_key) = + simple_join_key_from_indexes(probe_row.values(), probe_join_indexes)? + else { + continue; + }; + hashed + .entry(join_key) + .or_default() + .push((probe_row.row_id(), probe_row.values().to_vec())); + } + Some(hashed) + } else { + None + }; + let use_probe_row_position_map = probe_hash_rows.is_none() + && probe_source + .map_or(0, |source| source.row_count()) + .saturating_mul(plan.filtered_dataset.rows.len()) + > 8_192; + let probe_row_positions = if use_probe_row_position_map { + let mut positions = Int64Map::::default(); + for (position, row) in probe_source + .map(|source| source.rows()) + .unwrap_or_else(TableRowIter::empty) + .enumerate() + { + positions.insert(row?.row_id(), position); + } + Some(positions) + } else { + None + }; + + let probe_columns = probe_table + .columns + .iter() + .map(|column| { + ColumnBinding::visible_source( + Some(plan.probe_table.binding_name().to_string()), + Some(plan.probe_table.name.to_string()), + column.name.clone(), + ) + }) + .collect::>(); + let mut columns = if plan.filtered_on_left { + plan.filtered_dataset.columns.clone() + } else { + probe_columns.clone() + }; + if plan.filtered_on_left { + columns.extend(probe_columns.clone()); + } else { + columns.extend(plan.filtered_dataset.columns.clone()); + } + let mut rows = Vec::new(); + for filtered_row in plan.filtered_dataset.rows.iter() { + let join_values = ordered_filtered_join_indexes + .iter() + .map(|index| { + filtered_row.get(*index).ok_or_else(|| { + DbError::internal("join row is shorter than filtered table schema") + }) + }) + .collect::>>()?; + if join_values + .iter() + .any(|join_value| matches!(join_value, Value::Null)) + { + continue; + } + if let Some(probe_hash_rows) = probe_hash_rows.as_ref() { + let join_key = Row::new(join_values.iter().cloned().cloned().collect()).encode()?; + let Some(matching_probe_rows) = probe_hash_rows.get(&join_key) else { + continue; + }; + for (_, probe_row) in matching_probe_rows { + let mut row = Vec::with_capacity(filtered_row.len() + probe_row.len()); + if plan.filtered_on_left { + row.extend_from_slice(filtered_row); + row.extend_from_slice(probe_row); + } else { + row.extend_from_slice(probe_row); + row.extend_from_slice(filtered_row); + } + rows.push(row); + } + continue; + } + let row_ids = if let Some(keys) = keys { + if join_values.len() == 1 { + keys.row_ids_for_value_set(join_values[0])? + } else { + keys.row_id_set_for_key(&RuntimeBtreeKey::Encoded(RuntimeEncodedKey::from_vec( + Row::new(join_values.into_iter().cloned().collect()).encode()?, + ))) + } + } else if join_values.len() == 1 { + match join_values[0] { + Value::Int64(val) => RuntimeRowIdSet::Single(*val), + _ => RuntimeRowIdSet::Empty, + } + } else { + RuntimeRowIdSet::Empty + }; + if row_ids.is_empty() { + continue; + } + row_ids.for_each(|row_id| { + let probe_row = if let Some(positions) = probe_row_positions.as_ref() { + let Some(probe_position) = positions.get(&row_id).copied() else { + return; + }; + match probe_source + .map(|source| source.row_at_position(probe_position)) + .transpose() + { + Ok(Some(Some(probe_row))) => probe_row.values().to_vec(), + Ok(Some(None)) | Ok(None) => return, + Err(_) => return, + } + } else { + match probe_source + .map(|source| source.row_by_id(row_id)) + .transpose() + { + Ok(Some(Some(probe_row))) => probe_row.values().to_vec(), + Ok(Some(None)) | Ok(None) => return, + Err(_) => return, + } + }; + let mut row = Vec::with_capacity(filtered_row.len() + probe_row.len()); + if plan.filtered_on_left { + row.extend_from_slice(filtered_row); + row.extend_from_slice(&probe_row); + } else { + row.extend_from_slice(&probe_row); + row.extend_from_slice(filtered_row); + } + rows.push(row); + }); + } + Ok(Some(Dataset::with_rows(columns, rows))) + } + /// Indexed equi-join probe path. + /// + /// For each row in `left`, probes the right table via a b-tree index + /// (or the rowid alias) instead of doing a full O(|left| * |right|) + /// nested loop. Supported join kinds: + /// + /// * `Inner` — skips left rows with NULL join values and left rows + /// whose join value has no match. + /// * `Left` — preserves every left row, emitting a NULL-extended + /// right half when the left join value is NULL or has no match. + /// + /// Returns `Ok(None)` if any of the preconditions for the fast path + /// are not met (non-table right side, non-equi join, view/CTE/temp + /// table, etc.), in which case the caller falls back to the nested + /// loop join. + pub(crate) fn try_indexed_equi_join_with_right_table( + &self, + left: &Dataset, + right_item: &FromItem, + kind: JoinKind, + constraint: &JoinConstraint, + ctes: &BTreeMap, + ) -> Result> { + if !matches!( + kind, + JoinKind::Inner | JoinKind::Left | JoinKind::Right | JoinKind::Full + ) { + return Ok(None); + } + let JoinConstraint::On(on) = constraint else { + return Ok(None); + }; + let Some(join_equalities) = simple_join_equalities(on) else { + return Ok(None); + }; + let FromItem::Table { + name: right_name, + alias: right_alias, + } = right_item + else { + return Ok(None); + }; + if ctes.contains_key(right_name) { + return Ok(None); + } + if self + .visible_view(right_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(right_name) + { + return Ok(None); + } + + let right_binding = TableBindingRef { + name: right_name, + alias: right_alias, + }; + let mut left_probe_refs = Vec::with_capacity(join_equalities.len()); + let mut right_join_columns = Vec::with_capacity(join_equalities.len()); + for (left_join_ref, right_join_ref) in join_equalities { + let (left_probe_ref, right_join_column) = + if matches_table_binding(right_binding, right_join_ref.table) { + (left_join_ref, right_join_ref.column) + } else if matches_table_binding(right_binding, left_join_ref.table) { + (right_join_ref, left_join_ref.column) + } else { + return Ok(None); + }; + left_probe_refs.push(left_probe_ref); + right_join_columns.push(right_join_column); + } + let mut left_join_indexes = Vec::with_capacity(left_probe_refs.len()); + for left_probe_ref in &left_probe_refs { + let Some(left_join_index) = + dataset_column_index(left, left_probe_ref.table, left_probe_ref.column) + else { + return Ok(None); + }; + left_join_indexes.push(left_join_index); + } + + let right_table = self + .table_schema(right_name) + .ok_or_else(|| DbError::sql(format!("unknown table or view {right_name}")))?; + if !generated_columns_are_stored(right_table) { + return Ok(None); + } + let right_source = self.visible_table_row_source(right_name); + let is_probe_rowid_alias = right_join_columns.len() == 1 + && crate::exec::dml::row_id_alias_column_name(right_table) + .is_some_and(|name| identifiers_equal(name, right_join_columns[0])); + + let mut right_hash_join_indexes = None; + let (probe_index, ordered_left_join_indexes) = if is_probe_rowid_alias { + (None, left_join_indexes) + } else if let Some((probe_index, ordered_left_join_indexes)) = + self.catalog.indexes.values().find_map(|index| { + if !identifiers_equal(&index.table_name, right_name) + || !index.fresh + || index.kind != IndexKind::Btree + || index.predicate_sql.is_some() + || index.columns.len() != right_join_columns.len() + { + return None; + } + let mut ordered_left_join_indexes = Vec::with_capacity(index.columns.len()); + for index_column in &index.columns { + if index_column.expression_sql.is_some() { + return None; + } + let index_column_name = index_column.column_name.as_deref()?; + let join_position = right_join_columns.iter().position(|join_column| { + identifiers_equal(join_column, index_column_name) + })?; + ordered_left_join_indexes.push(left_join_indexes[join_position]); + } + Some((index, ordered_left_join_indexes)) + }) + { + (Some(probe_index), ordered_left_join_indexes) + } else { + let mut ordered_right_join_indexes = Vec::with_capacity(right_join_columns.len()); + for right_join_column in &right_join_columns { + let Some(right_join_index) = schema_column_index(right_table, right_join_column) + else { + return Ok(None); + }; + ordered_right_join_indexes.push(right_join_index); + } + right_hash_join_indexes = Some(ordered_right_join_indexes); + (None, left_join_indexes) + }; + let keys = if let Some(index) = probe_index { + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { + return Ok(None); + }; + Some(keys) + } else { + None + }; + let right_hash_rows = if let Some(right_join_indexes) = right_hash_join_indexes.as_ref() { + let Some(right_source) = right_source else { + return Ok(None); + }; + let mut hashed = SimpleJoinHashRows::new(); + for right_row in right_source.rows() { + let right_row = right_row?; + let Some(join_key) = + simple_join_key_from_indexes(right_row.values(), right_join_indexes)? + else { + continue; + }; + hashed + .entry(join_key) + .or_default() + .push((right_row.row_id(), right_row.values().to_vec())); + } + Some(hashed) + } else { + None + }; + + let use_right_row_position_map = right_hash_rows.is_none() + && right_source + .map_or(0, |source| source.row_count()) + .saturating_mul(left.rows.len()) + > 8_192; + let right_row_positions = if use_right_row_position_map { + let mut positions = Int64Map::::default(); + for (position, row) in right_source + .map(|source| source.rows()) + .unwrap_or_else(TableRowIter::empty) + .enumerate() + { + positions.insert(row?.row_id(), position); + } + Some(positions) + } else { + None + }; + + let right_binding_name = right_alias.clone().unwrap_or_else(|| right_name.clone()); + let mut columns = left.columns.clone(); + columns.extend(right_table.columns.iter().map(|column| { + ColumnBinding::visible_source( + Some(right_binding_name.clone()), + Some(right_name.clone()), + column.name.clone(), + ) + })); + let right_column_count = right_table.columns.len(); + let is_left_outer = matches!(kind, JoinKind::Left | JoinKind::Full); + let is_right_outer = matches!(kind, JoinKind::Right | JoinKind::Full); + let mut rows = Vec::new(); + let mut matched_right_row_ids = is_right_outer.then(Int64Map::<()>::default); + for left_row in left.rows.iter() { + let join_values = ordered_left_join_indexes + .iter() + .map(|index| { + left_row.get(*index).ok_or_else(|| { + DbError::internal("join row is shorter than the left input schema") + }) + }) + .collect::>>()?; + if join_values + .iter() + .any(|join_value| matches!(join_value, Value::Null)) + { + if is_left_outer { + let mut row = Vec::with_capacity(left_row.len() + right_column_count); + row.extend_from_slice(left_row); + row.extend(std::iter::repeat_n(Value::Null, right_column_count)); + rows.push(row); + } + continue; + } + let rows_before = rows.len(); + if let Some(right_hash_rows) = right_hash_rows.as_ref() { + let join_key = Row::new(join_values.iter().cloned().cloned().collect()).encode()?; + if let Some(matching_rows) = right_hash_rows.get(&join_key) { + for (row_id, right_values) in matching_rows { + if let Some(matched_right_row_ids) = matched_right_row_ids.as_mut() { + matched_right_row_ids.insert(*row_id, ()); + } + let mut row = Vec::with_capacity(left_row.len() + right_values.len()); + row.extend_from_slice(left_row); + row.extend_from_slice(right_values); + rows.push(row); + } + } + } else { + let row_ids = if let Some(keys) = keys { + if join_values.len() == 1 { + keys.row_ids_for_value_set(join_values[0])? + } else { + keys.row_id_set_for_key(&RuntimeBtreeKey::Encoded( + RuntimeEncodedKey::from_vec( + Row::new(join_values.into_iter().cloned().collect()).encode()?, + ), + )) + } + } else if join_values.len() == 1 { + match join_values[0] { + Value::Int64(val) => RuntimeRowIdSet::Single(*val), + _ => RuntimeRowIdSet::Empty, + } + } else { + RuntimeRowIdSet::Empty + }; + row_ids.for_each(|row_id| { + let right_values = if let Some(positions) = right_row_positions.as_ref() { + let Some(right_position) = positions.get(&row_id).copied() else { + return; + }; + match right_source + .map(|source| source.row_at_position(right_position)) + .transpose() + { + Ok(Some(Some(right_row))) => right_row.values().to_vec(), + Ok(Some(None)) | Ok(None) => return, + Err(_) => return, + } + } else { + match right_source + .map(|source| source.row_by_id(row_id)) + .transpose() + { + Ok(Some(Some(right_row))) => right_row.values().to_vec(), + Ok(Some(None)) | Ok(None) => return, + Err(_) => return, + } + }; + if let Some(matched_right_row_ids) = matched_right_row_ids.as_mut() { + matched_right_row_ids.insert(row_id, ()); + } + let mut row = Vec::with_capacity(left_row.len() + right_values.len()); + row.extend_from_slice(left_row); + row.extend_from_slice(&right_values); + rows.push(row); + }); + } + if is_left_outer && rows.len() == rows_before { + let mut row = Vec::with_capacity(left_row.len() + right_column_count); + row.extend_from_slice(left_row); + row.extend(std::iter::repeat_n(Value::Null, right_column_count)); + rows.push(row); + } + } + if let Some(matched_right_row_ids) = matched_right_row_ids.as_ref() { + let left_nulls = vec![Value::Null; left.columns.len()]; + for right_row in right_source + .map(|source| source.rows()) + .unwrap_or_else(TableRowIter::empty) + { + let right_row = right_row?; + if matched_right_row_ids.contains_key(&right_row.row_id()) { + continue; + } + let mut row = Vec::with_capacity(left_nulls.len() + right_row.values().len()); + row.extend_from_slice(&left_nulls); + row.extend_from_slice(right_row.values()); + rows.push(row); + } + } + Ok(Some(Dataset::with_rows(columns, rows))) + } + pub(crate) fn try_indexed_equi_join_with_right_cte( + &self, + left: &Dataset, + right_item: &FromItem, + constraint: &JoinConstraint, + kind: JoinKind, + ctes: &BTreeMap, + ) -> Result> { + let _ = self; + if !matches!(kind, JoinKind::Inner) { + return Ok(None); + } + let JoinConstraint::On(on) = constraint else { + return Ok(None); + }; + let Some(join_equalities) = simple_join_equalities(on) else { + return Ok(None); + }; + let FromItem::Table { + name: right_name, + alias: right_alias, + } = right_item + else { + return Ok(None); + }; + let Some(right_dataset) = ctes.get(right_name) else { + return Ok(None); + }; + + let right_binding = TableBindingRef { + name: right_name, + alias: right_alias, + }; + let mut left_probe_refs = Vec::with_capacity(join_equalities.len()); + let mut right_probe_refs = Vec::with_capacity(join_equalities.len()); + for (left_join_ref, right_join_ref) in join_equalities { + let (left_probe_ref, right_probe_ref) = + if matches_table_binding(right_binding, right_join_ref.table) { + (left_join_ref, right_join_ref) + } else if matches_table_binding(right_binding, left_join_ref.table) { + (right_join_ref, left_join_ref) + } else { + return Ok(None); + }; + left_probe_refs.push(left_probe_ref); + right_probe_refs.push(right_probe_ref); + } + + let mut left_join_indexes = Vec::with_capacity(left_probe_refs.len()); + for left_probe_ref in &left_probe_refs { + let Some(left_join_index) = + dataset_column_index(left, left_probe_ref.table, left_probe_ref.column) + else { + return Ok(None); + }; + left_join_indexes.push(left_join_index); + } + let mut right_join_indexes = Vec::with_capacity(right_probe_refs.len()); + let mut right_columns = right_dataset.columns.clone(); + if let Some(alias) = right_alias { + for column in &mut right_columns { + column.table = Some(alias.clone()); + } + } + for right_join_ref in &right_probe_refs { + let right_join_indexes_for_ref = right_columns + .iter() + .enumerate() + .filter(|(_, binding)| { + if !identifiers_equal(&binding.name, right_join_ref.column) { + return false; + } + if let Some(qualifier) = right_join_ref.table { + binding + .table + .as_deref() + .is_some_and(|table| identifiers_equal(table, qualifier)) + } else { + !binding.hidden + } + }) + .map(|(index, _)| index) + .collect::>(); + let [right_join_index] = right_join_indexes_for_ref.as_slice() else { + return Ok(None); + }; + right_join_indexes.push(*right_join_index); + } + + let mut hashed_right_rows: BTreeMap, Vec>> = BTreeMap::new(); + for right_row in right_dataset.rows.iter() { + let Some(join_key) = simple_join_key_from_indexes(right_row, &right_join_indexes)? + else { + continue; + }; + hashed_right_rows + .entry(join_key) + .or_default() + .push(right_row.clone()); + } + + let mut columns = left.columns.clone(); + columns.extend(right_columns); + let right_column_count = columns.len().saturating_sub(left.columns.len()); + + let mut rows = Vec::new(); + for left_row in left.rows.iter() { + let Some(join_key) = simple_join_key_from_indexes(left_row, &left_join_indexes)? else { + continue; + }; + if let Some(matching_rows) = hashed_right_rows.get(&join_key) { + for right_row in matching_rows { + let mut row = Vec::with_capacity(left_row.len() + right_column_count); + row.extend_from_slice(left_row); + row.extend_from_slice(right_row); + rows.push(row); + } + } + } + Ok(Some(Dataset::with_rows(columns, rows))) + } + #[allow(clippy::too_many_arguments)] + pub(crate) fn evaluate_join_with_lateral_right( + &self, + left: Dataset, + right_item: &FromItem, + kind: JoinKind, + constraint: &JoinConstraint, + params: &[Value], + ctes: &BTreeMap, + scope_dataset: &Dataset, + scope_row: &[Value], + ) -> Result { + if matches!(kind, JoinKind::Right | JoinKind::Full) { + return Err(DbError::sql( + "LATERAL is only supported with INNER, LEFT, and CROSS joins", + )); + } + + let mut columns = left.columns.clone(); + let mut rows = Vec::new(); + for left_row in left.rows.iter() { + let left_single = Dataset::with_rows(left.columns.clone(), vec![left_row.clone()]); + let scope_with_left = + augment_dataset_with_outer_scope(left_single.clone(), scope_dataset, scope_row); + let scope_values = scope_with_left + .rows + .first() + .map(Vec::as_slice) + .unwrap_or(&[]); + let right = self.evaluate_from_item_in_scope( + right_item, + params, + ctes, + &scope_with_left, + scope_values, + )?; + let joined = + nested_loop_join(left_single, right, kind, constraint, self, params, ctes)?; + columns = joined.columns.clone(); + rows.extend(joined.into_rows()); + } + Ok(Dataset::with_rows(columns, rows)) + } +} diff --git a/crates/decentdb/src/exec/manifest.rs b/crates/decentdb/src/exec/manifest.rs new file mode 100644 index 00000000..f47f808c --- /dev/null +++ b/crates/decentdb/src/exec/manifest.rs @@ -0,0 +1,861 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct DensePagedRowDirectory { + pub(crate) start_row_id: i64, + pub(crate) locators: Vec, + /// Cumulative exclusive row positions for each physical chunk. + pub(crate) chunk_ends: Vec, +} + +impl DensePagedRowDirectory { + pub(crate) fn empty(chunk_count: usize) -> Result { + let mut chunk_ends = Vec::new(); + try_reserve_paged_directory(&mut chunk_ends, chunk_count, "dense chunk ranges")?; + chunk_ends.resize(chunk_count, 0); + Ok(Self { + start_row_id: 0, + locators: Vec::new(), + chunk_ends, + }) + } + + pub(crate) fn len(&self) -> usize { + self.locators.len() + } + + fn is_empty(&self) -> bool { + self.locators.is_empty() + } + + pub(crate) fn first_row_id(&self) -> Option { + (!self.is_empty()).then_some(self.start_row_id) + } + + pub(crate) fn row_id_at(&self, position: usize) -> Option { + if position >= self.len() { + return None; + } + let position = i128::try_from(position).ok()?; + i64::try_from(i128::from(self.start_row_id) + position).ok() + } + + pub(crate) fn position_for_row_id(&self, row_id: i64) -> Option { + let offset = i128::from(row_id) - i128::from(self.start_row_id); + if offset < 0 { + return None; + } + usize::try_from(offset) + .ok() + .filter(|position| *position < self.len()) + } + + pub(crate) fn chunk_index_at(&self, position: usize) -> Option { + if position >= self.len() { + return None; + } + let chunk_index = self.chunk_ends.partition_point(|end| *end <= position); + (chunk_index < self.chunk_ends.len()).then_some(chunk_index) + } + + pub(crate) fn entry_at(&self, position: usize) -> Result> { + let Some(row_id) = self.row_id_at(position) else { + return Ok(None); + }; + let chunk_index = self + .chunk_index_at(position) + .ok_or_else(|| DbError::corruption("dense paged row position has no owning chunk"))?; + let locator = *self.locators.get(position).ok_or_else(|| { + DbError::corruption("dense paged row locator position exceeded directory length") + })?; + Ok(Some(TablePageEntry { + row_id, + chunk_index: u32::try_from(chunk_index) + .map_err(|_| DbError::constraint("table chunk index exceeds u32"))?, + is_overlay: false, + locator, + })) + } + + pub(crate) fn try_prepare_append( + &mut self, + row_id: i64, + chunk_index: usize, + is_overlay: bool, + ) -> Result> { + if is_overlay { + return Ok(None); + } + let next_position = self.len(); + if !self.is_empty() + && self + .row_id_at(next_position.saturating_sub(1)) + .and_then(|last| last.checked_add(1)) + != Some(row_id) + { + return Ok(None); + } + + let add_chunk = if chunk_index >= self.chunk_ends.len() { + if chunk_index != self.chunk_ends.len() { + return Ok(None); + } + u32::try_from(chunk_index) + .map_err(|_| DbError::constraint("table chunk index exceeds u32"))?; + true + } else if chunk_index + 1 != self.chunk_ends.len() { + return Ok(None); + } else { + false + }; + + if self.locators.len() == self.locators.capacity() { + try_reserve_paged_directory_amortized(&mut self.locators, 1, "dense row locators")?; + } + if add_chunk { + try_reserve_paged_directory(&mut self.chunk_ends, 1, "dense chunk ranges")?; + } + Ok(Some(add_chunk)) + } + + #[cfg(test)] + pub(crate) fn try_append( + &mut self, + row_id: i64, + chunk_index: usize, + is_overlay: bool, + locator: RowLocatorV1, + ) -> Result { + let Some(add_chunk) = self.try_prepare_append(row_id, chunk_index, is_overlay)? else { + return Ok(false); + }; + self.append_prepared(row_id, chunk_index, add_chunk, locator)?; + Ok(true) + } + + fn append_prepared( + &mut self, + row_id: i64, + chunk_index: usize, + add_chunk: bool, + locator: RowLocatorV1, + ) -> Result<()> { + let next_position = self.len(); + if self.is_empty() { + self.start_row_id = row_id; + } + if add_chunk { + self.chunk_ends.push(next_position); + } + self.locators.push(locator); + let Some(chunk_end) = self.chunk_ends.get_mut(chunk_index) else { + return Err(DbError::corruption( + "dense paged append chunk range is missing", + )); + }; + *chunk_end = self.locators.len(); + Ok(()) + } + + pub(crate) fn to_sparse(&self) -> Result> { + let mut entries = Vec::new(); + try_reserve_paged_directory(&mut entries, self.len(), "sparse paged row entries")?; + for position in 0..self.len() { + entries.push(self.entry_at(position)?.ok_or_else(|| { + DbError::corruption("dense paged row directory ended before its locator count") + })?); + } + Ok(entries) + } + + pub(crate) fn approximate_heap_bytes(&self) -> usize { + self.locators + .capacity() + .saturating_mul(std::mem::size_of::()) + .saturating_add( + self.chunk_ends + .capacity() + .saturating_mul(std::mem::size_of::()), + ) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct TablePageManifest { + pub(crate) chunks: Arc>, + pub(crate) rows: Arc, + pub(crate) tombstoned_row_ids: Arc>, +} + +impl TablePageManifest { + pub(crate) fn from_payload(payload: Arc>) -> Result { + let row_count = if payload.is_empty() { + 0 + } else { + read_table_payload_row_count_from_bytes(payload.as_slice())? + }; + let chunk = TablePageManifestChunk { + pointer: OverflowPointer { + head_page_id: 0, + logical_len: 0, + flags: 0, + }, + checksum: 0, + row_count, + payload, + tombstoned_row_ids: Arc::new(BTreeSet::new()), + overlay_pointer: None, + overlay_checksum: None, + overlay_payload: None, + }; + Self::from_chunks(vec![chunk]) + } + + pub(crate) fn from_chunks(chunks: Vec) -> Result { + let tombstoned_row_ids = chunks + .iter() + .flat_map(|chunk| chunk.tombstoned_row_ids.iter().copied()) + .collect::>(); + + if let Some(directory) = try_build_dense_paged_row_directory(&chunks)? { + return Ok(Self { + chunks: Arc::new(chunks), + rows: Arc::new(TablePageDirectory::Dense(directory)), + tombstoned_row_ids: Arc::new(tombstoned_row_ids), + }); + } + + // Collect tombstoned row IDs into a set per chunk + let chunk_tombstones: Vec> = chunks + .iter() + .map(|c| c.tombstoned_row_ids.iter().copied().collect()) + .collect(); + + // Collect overlay row IDs per chunk into a set + let chunk_overlay_row_ids: Vec> = chunks + .iter() + .map(|c| { + let mut set = BTreeSet::new(); + if let Some(overlay_payload) = &c.overlay_payload { + if !overlay_payload.is_empty() { + let mut cursor = Cursor::new(overlay_payload.as_slice()); + let magic = cursor + .read_slice(TABLE_PAYLOAD_MAGIC.len()) + .unwrap_or_default(); + if magic == *TABLE_PAYLOAD_MAGIC { + let row_count = cursor.read_u32().unwrap_or(0) as usize; + for _ in 0..row_count { + let row_id = cursor.read_i64().unwrap_or(0); + let row_bytes_len = cursor.read_u32().unwrap_or(0) as usize; + if cursor.read_slice(row_bytes_len).is_err() { + break; + } + set.insert(row_id); + } + } + } + } + set + }) + .collect(); + + let expected_rows = chunks.iter().try_fold(0usize, |total, chunk| { + total + .checked_add(chunk.row_count) + .ok_or_else(|| DbError::constraint("paged table row count overflow")) + })?; + let mut rows = Vec::new(); + try_reserve_paged_directory(&mut rows, expected_rows, "sparse paged row entries")?; + let mut base_row_entries = Vec::new(); + let mut overlay_row_entries = Vec::new(); + + for (chunk_index, chunk) in chunks.iter().enumerate() { + base_row_entries.clear(); + overlay_row_entries.clear(); + + let tombstones = &chunk_tombstones[chunk_index]; + let overlay_ids = &chunk_overlay_row_ids[chunk_index]; + + if !chunk.payload.is_empty() { + let mut cursor = Cursor::new(chunk.payload.as_slice()); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = + split_table_payload_row_len(cursor.read_u32()?); + let row_bytes_offset = cursor.offset; + if is_tombstone { + cursor.read_slice(row_bytes_len)?; + continue; + } + #[cfg(debug_assertions)] + { + let row_bytes = cursor.read_slice(row_bytes_len)?; + Row::decode(row_bytes)?; + } + #[cfg(not(debug_assertions))] + { + cursor.read_slice(row_bytes_len)?; + } + if tombstones.contains(&row_id) || overlay_ids.contains(&row_id) { + continue; // skip tombstoned and overlaid base rows + } + base_row_entries.push(TablePageEntry { + row_id, + chunk_index: u32::try_from(chunk_index) + .map_err(|_| DbError::constraint("table chunk index exceeds u32"))?, + is_overlay: false, + locator: RowLocatorV1 { + byte_offset: u32::try_from(row_bytes_offset).map_err(|_| { + DbError::constraint("row locator offset exceeds u32") + })?, + byte_len: u32::try_from(row_bytes_len).map_err(|_| { + DbError::constraint("row locator length exceeds u32") + })?, + }, + }); + } + } + + if let Some(overlay_payload) = &chunk.overlay_payload { + if !overlay_payload.is_empty() { + let mut cursor = Cursor::new(overlay_payload.as_slice()); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = + split_table_payload_row_len(cursor.read_u32()?); + let row_bytes_offset = cursor.offset; + if is_tombstone { + cursor.read_slice(row_bytes_len)?; + continue; + } + #[cfg(debug_assertions)] + { + let row_bytes = cursor.read_slice(row_bytes_len)?; + Row::decode(row_bytes)?; + } + #[cfg(not(debug_assertions))] + { + cursor.read_slice(row_bytes_len)?; + } + overlay_row_entries.push(TablePageEntry { + row_id, + chunk_index: u32::try_from(chunk_index).map_err(|_| { + DbError::constraint("table chunk index exceeds u32") + })?, + is_overlay: true, + locator: RowLocatorV1 { + byte_offset: u32::try_from(row_bytes_offset).map_err(|_| { + DbError::constraint("row locator offset exceeds u32") + })?, + byte_len: u32::try_from(row_bytes_len).map_err(|_| { + DbError::constraint("row locator length exceeds u32") + })?, + }, + }); + } + } + } + + // Add base entries first, then overlay entries. Since overlay row_ids + // are not in base_row_entries, the merged list has unique row_ids per chunk. + rows.extend_from_slice(&base_row_entries); + rows.extend_from_slice(&overlay_row_entries); + } + + // Now rows are globally sorted by (chunk_index, row_id), but + // row_by_id needs them sorted by row_id. Sort and verify no duplicates. + rows.sort_by_key(|entry| entry.row_id); + + // Sanity check: after deduplication, there should be no duplicate row_ids + // because overlay_ids were used to skip overlaid base rows. + #[cfg(debug_assertions)] + { + for window in rows.windows(2) { + assert_ne!( + window[0].row_id, window[1].row_id, + "duplicate row_id in TablePageManifest rows" + ); + } + } + + Ok(Self { + chunks: Arc::new(chunks), + rows: Arc::new(TablePageDirectory::Sparse(rows)), + tombstoned_row_ids: Arc::new(tombstoned_row_ids), + }) + } + + pub(crate) fn from_rows(rows: &[StoredRow], page_size: u32) -> Result { + let chunks = encode_paged_table_chunks_from_rows(rows, page_size)? + .into_iter() + .map(|chunk| TablePageManifestChunk { + pointer: OverflowPointer { + head_page_id: 0, + logical_len: 0, + flags: 0, + }, + checksum: chunk.checksum, + row_count: chunk.row_count, + payload: Arc::new(chunk.payload), + tombstoned_row_ids: Arc::new(BTreeSet::new()), + overlay_pointer: None, + overlay_checksum: None, + overlay_payload: None, + }) + .collect(); + Self::from_chunks(chunks) + } + + pub(crate) fn row_count(&self) -> usize { + self.rows.len() + } + + pub(crate) fn append_row(&mut self, row: &StoredRow, page_size: u32) -> Result<()> { + let mut encoded_values = Vec::with_capacity(64); + self.append_row_with_scratch(row, page_size, &mut encoded_values) + } + + pub(crate) fn append_row_with_scratch( + &mut self, + row: &StoredRow, + page_size: u32, + encoded_values: &mut Vec, + ) -> Result<()> { + let prepared = self.try_prepare_append_row_with_scratch(row, page_size, encoded_values)?; + self.append_prepared_row_with_scratch(row, page_size, encoded_values, prepared) + } + + pub(crate) fn try_prepare_append_row_with_scratch( + &mut self, + row: &StoredRow, + page_size: u32, + encoded_values: &mut Vec, + ) -> Result { + Row::encode_values_into(&row.values, encoded_values)?; + let encoded_row_len = 8usize + .saturating_add(4) + .saturating_add(encoded_values.len()); + let (planned_chunk_index, planned_is_overlay) = + self.planned_append_target(row.row_id, encoded_row_len, page_size)?; + let entry_chunk_index = u32::try_from(planned_chunk_index) + .map_err(|_| DbError::constraint("table chunk index exceeds u32"))?; + let directory = Arc::make_mut(&mut self.rows).try_prepare_append( + row.row_id, + planned_chunk_index, + planned_is_overlay, + )?; + Ok(PreparedTablePageAppend { + chunk_index: planned_chunk_index, + entry_chunk_index, + is_overlay: planned_is_overlay, + directory, + }) + } + + fn planned_append_target( + &self, + row_id: i64, + encoded_row_len: usize, + page_size: u32, + ) -> Result<(usize, bool)> { + #[cfg(test)] + PAGED_ROW_APPEND_PLAN_COUNT.with(|count| count.set(count.get().saturating_add(1))); + if self.tombstoned_row_ids.contains(&row_id) { + let chunk_index = self + .chunks + .iter() + .position(|chunk| chunk.tombstoned_row_ids.contains(&row_id)) + .ok_or_else(|| { + DbError::corruption( + "paged table tombstone index referenced a missing chunk tombstone", + ) + })?; + return Ok((chunk_index, true)); + } + let target_chunk_bytes = paged_table_target_chunk_bytes(page_size); + let chunk_index = if self.chunks.last().is_some_and(|chunk| { + chunk.payload.len().saturating_add(encoded_row_len) <= target_chunk_bytes + }) { + self.chunks.len() - 1 + } else { + self.chunks.len() + }; + Ok((chunk_index, false)) + } + + pub(crate) fn append_prepared_row_with_scratch( + &mut self, + row: &StoredRow, + page_size: u32, + encoded_values: &[u8], + prepared: PreparedTablePageAppend, + ) -> Result<()> { + let encoded_row_len = 8usize + .saturating_add(4) + .saturating_add(encoded_values.len()); + let target_chunk_bytes = paged_table_target_chunk_bytes(page_size); + + let chunks = Arc::make_mut(&mut self.chunks); + let locator = if prepared.is_overlay { + let chunk_index = prepared.chunk_index; + let chunk = chunks + .get_mut(chunk_index) + .ok_or_else(|| DbError::internal("paged append chunk index was out of bounds"))?; + let overlay_payload = chunk.overlay_payload.get_or_insert_with(|| { + let mut payload = Vec::with_capacity( + target_chunk_bytes.max(TABLE_PAYLOAD_MAGIC.len() + 4 + encoded_row_len), + ); + payload.extend_from_slice(TABLE_PAYLOAD_MAGIC); + payload.extend_from_slice(&0_u32.to_le_bytes()); + Arc::new(payload) + }); + let locator = append_encoded_table_payload_row( + Arc::make_mut(overlay_payload), + row.row_id, + encoded_values, + )?; + chunk.overlay_checksum = None; + chunk.row_count = chunk + .row_count + .checked_add(1) + .ok_or_else(|| DbError::constraint("paged table chunk row count overflow"))?; + locator + } else { + let chunk_index = prepared.chunk_index; + if chunk_index == chunks.len() { + let mut payload = + Vec::with_capacity(target_chunk_bytes.max(TABLE_PAYLOAD_MAGIC.len() + 4)); + payload.extend_from_slice(TABLE_PAYLOAD_MAGIC); + payload.extend_from_slice(&0_u32.to_le_bytes()); + chunks.push(TablePageManifestChunk { + pointer: OverflowPointer { + head_page_id: 0, + logical_len: 0, + flags: 0, + }, + checksum: 0, + row_count: 0, + payload: Arc::new(payload), + tombstoned_row_ids: Arc::new(BTreeSet::new()), + overlay_pointer: None, + overlay_checksum: None, + overlay_payload: None, + }); + } + + let chunk = chunks + .get_mut(chunk_index) + .ok_or_else(|| DbError::internal("paged append chunk index was out of bounds"))?; + let locator = append_encoded_table_payload_row( + Arc::make_mut(&mut chunk.payload), + row.row_id, + encoded_values, + )?; + chunk.pointer = OverflowPointer { + head_page_id: 0, + logical_len: 0, + flags: 0, + }; + chunk.checksum = 0; + chunk.row_count = chunk + .row_count + .checked_add(1) + .ok_or_else(|| DbError::constraint("paged table chunk row count overflow"))?; + locator + }; + + let entry = TablePageEntry { + row_id: row.row_id, + chunk_index: prepared.entry_chunk_index, + is_overlay: prepared.is_overlay, + locator, + }; + let directory = Arc::make_mut(&mut self.rows); + let rows = match (directory, prepared.directory) { + ( + TablePageDirectory::Dense(rows), + PreparedTablePageDirectoryAppend::Dense { add_chunk }, + ) => { + debug_assert!(!prepared.is_overlay); + rows.append_prepared(row.row_id, prepared.chunk_index, add_chunk, locator)?; + return Ok(()); + } + (TablePageDirectory::Sparse(rows), PreparedTablePageDirectoryAppend::Sparse) => rows, + _ => { + return Err(DbError::corruption( + "paged row directory changed after append preparation", + )); + } + }; + if rows + .last() + .is_none_or(|existing| existing.row_id < entry.row_id) + { + rows.push(entry); + return Ok(()); + } + match rows.binary_search_by_key(&entry.row_id, |existing| existing.row_id) { + Ok(_) => Err(DbError::constraint( + "duplicate row id in paged table append", + )), + Err(position) => { + rows.insert(position, entry); + Ok(()) + } + } + } + + pub(crate) fn row_by_id(&self, row_id: i64) -> Result>> { + let Some((position, _)) = self.rows.entry_for_row_id(row_id)? else { + return Ok(None); + }; + self.row_at_position(position) + } + + pub(crate) fn row_ids_in_range(&self, low: i64, high: i64) -> Vec { + self.rows.row_ids_in_range(low, high) + } + + /// Returns the chunk index owning `row_id`, if present. Used by the bulk + /// delete manifest rebuild to avoid decoding base payloads. + pub(crate) fn chunk_index_for_row_id(&self, row_id: i64) -> Option { + self.rows + .entry_for_row_id(row_id) + .ok() + .flatten() + .map(|(_, entry)| entry.chunk_index as usize) + } + + pub(crate) fn projected_values_by_id( + &self, + row_id: i64, + projection_indexes: &[usize], + ) -> Result>> { + let Some((position, _)) = self.rows.entry_for_row_id(row_id)? else { + return Ok(None); + }; + self.projected_values_at_position(position, projection_indexes) + } + + fn row_bytes_for_entry<'a>( + &'a self, + entry: TablePageEntry, + chunk: &'a TablePageManifestChunk, + ) -> Result> { + if entry.is_overlay { + let payload = chunk + .overlay_payload + .as_ref() + .ok_or_else(|| DbError::corruption("paged table overlay chunk is missing"))?; + return Self::row_bytes_from_locator(payload.as_slice(), entry.locator).map(Some); + } + + if !self.tombstoned_row_ids.is_empty() && chunk.tombstoned_row_ids.contains(&entry.row_id) { + let Some(overlay_payload) = &chunk.overlay_payload else { + return Ok(None); + }; + return Self::row_bytes_from_tombstoned_base(overlay_payload.as_slice(), entry.row_id) + .map(Some); + } + + let start = entry.locator.byte_offset as usize; + let end = start + .checked_add(entry.locator.byte_len as usize) + .ok_or_else(|| DbError::corruption("paged row locator exceeded address space"))?; + let row_bytes = chunk + .payload + .as_slice() + .get(start..end) + .ok_or_else(|| DbError::corruption("paged row locator exceeded payload length"))?; + Ok(Some(row_bytes)) + } + + pub(crate) fn row_bytes_from_locator(payload: &[u8], locator: RowLocatorV1) -> Result<&[u8]> { + let start = locator.byte_offset as usize; + let end = start + .checked_add(locator.byte_len as usize) + .ok_or_else(|| DbError::corruption("paged row locator exceeded address space"))?; + payload + .get(start..end) + .ok_or_else(|| DbError::corruption("paged row locator exceeded payload length")) + } + + fn row_bytes_from_tombstoned_base(overlay_payload: &[u8], row_id: i64) -> Result<&[u8]> { + if overlay_payload.is_empty() { + return Err(DbError::corruption( + "paged table overlay row is missing from overlay payload", + )); + } + let mut cursor = Cursor::new(overlay_payload); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + let mut matched_row_bytes = None; + for _ in 0..row_count { + let current_row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes = cursor.read_slice(row_bytes_len)?; + if is_tombstone { + continue; + } + if current_row_id == row_id { + matched_row_bytes = Some(row_bytes); + } + } + matched_row_bytes.ok_or_else(|| { + DbError::corruption("paged table overlay row is missing from overlay payload") + }) + } + + pub(crate) fn row_at_position(&self, position: usize) -> Result>> { + let Some(entry) = self.rows.entry_at(position)? else { + return Ok(None); + }; + let chunk = self.chunks.get(entry.chunk_index as usize).ok_or_else(|| { + DbError::corruption("paged table chunk index exceeded chunk list length") + })?; + let Some(row_bytes) = self.row_bytes_for_entry(entry, chunk)? else { + return Ok(None); + }; + let row = Row::decode(row_bytes)?; + Ok(Some(TableRowRef::Decoded(StoredRow { + row_id: entry.row_id, + values: row.into_values(), + }))) + } + + fn projected_values_at_position( + &self, + position: usize, + projection_indexes: &[usize], + ) -> Result>> { + let Some(entry) = self.rows.entry_at(position)? else { + return Ok(None); + }; + let chunk = self.chunks.get(entry.chunk_index as usize).ok_or_else(|| { + DbError::corruption("paged table chunk index exceeded chunk list length") + })?; + let Some(row_bytes) = self.row_bytes_for_entry(entry, chunk)? else { + return Ok(None); + }; + Row::decode_projection_sorted_unique_with_overflow::( + row_bytes, + None, + projection_indexes, + ) + .map(Some) + } + + pub(crate) fn full_query_row_by_id(&self, row_id: i64) -> Result> { + let Some((_, entry)) = self.rows.entry_for_row_id(row_id)? else { + return Ok(None); + }; + let chunk = self.chunks.get(entry.chunk_index as usize).ok_or_else(|| { + DbError::corruption("paged table chunk index exceeded chunk list length") + })?; + let Some(row_bytes) = self.row_bytes_for_entry(entry, chunk)? else { + return Ok(None); + }; + Row::decode(row_bytes) + .map(Row::into_values) + .map(QueryRow::new) + .map(Some) + } + + pub(crate) fn visit_int64_column_values( + &self, + column_index: usize, + mut visitor: F, + ) -> Result<()> + where + F: FnMut(i64, Option) -> Result<()>, + { + for entry in self.rows.iter() { + let entry = entry?; + let chunk = self.chunks.get(entry.chunk_index as usize).ok_or_else(|| { + DbError::corruption("paged table chunk index exceeded chunk list length") + })?; + let Some(row_bytes) = self.row_bytes_for_entry(entry, chunk)? else { + continue; + }; + visitor(entry.row_id, Row::decode_int64_at(row_bytes, column_index)?)?; + } + Ok(()) + } + + pub(crate) fn visit_float64_column_values( + &self, + column_index: usize, + mut visitor: F, + ) -> Result<()> + where + F: FnMut(i64, Option) -> Result<()>, + { + for entry in self.rows.iter() { + let entry = entry?; + let chunk = self.chunks.get(entry.chunk_index as usize).ok_or_else(|| { + DbError::corruption("paged table chunk index exceeded chunk list length") + })?; + let Some(row_bytes) = self.row_bytes_for_entry(entry, chunk)? else { + continue; + }; + visitor( + entry.row_id, + Row::decode_float64_at(row_bytes, column_index)?, + )?; + } + Ok(()) + } + + pub(crate) fn rows(&self) -> TablePageRowIter<'_> { + TablePageRowIter { + manifest: self, + position: 0, + } + } + + pub(crate) fn approximate_heap_bytes(&self) -> usize { + let chunks_bytes = self + .chunks + .capacity() + .saturating_mul(std::mem::size_of::()); + let chunk_payload_bytes = self.chunks.iter().fold(0usize, |bytes, chunk| { + bytes + .saturating_add(chunk.payload.capacity()) + .saturating_add( + chunk + .overlay_payload + .as_ref() + .map_or(0, |payload| payload.capacity()), + ) + .saturating_add( + chunk + .tombstoned_row_ids + .len() + .saturating_mul(std::mem::size_of::()), + ) + }); + chunks_bytes + .saturating_add(chunk_payload_bytes) + .saturating_add( + self.tombstoned_row_ids + .len() + .saturating_mul(std::mem::size_of::()), + ) + .saturating_add(self.rows.approximate_heap_bytes()) + } +} diff --git a/crates/decentdb/src/exec/mod.rs b/crates/decentdb/src/exec/mod.rs index b7f0f613..29b421e6 100644 --- a/crates/decentdb/src/exec/mod.rs +++ b/crates/decentdb/src/exec/mod.rs @@ -23,6 +23,49 @@ mod runtime_unit_tests; pub(crate) mod cte; mod expressions; + +pub(crate) mod bench_queries; +pub(crate) mod codec; +pub(crate) mod deferred; +pub(crate) mod evaluate; +pub(crate) mod grouped; +pub(crate) mod indexes; +pub(crate) mod joins; +pub(crate) mod manifest; +pub(crate) mod paged_tables; +pub(crate) mod runtime_keys; +pub(crate) mod simple_queries; +pub(crate) mod table_data; + +pub(crate) mod runtime_eval; +#[allow(unused_imports)] +pub(crate) use runtime_eval::*; + +#[allow(unused_imports)] +pub(crate) use bench_queries::*; +#[allow(unused_imports)] +pub(crate) use codec::*; +#[allow(unused_imports)] +pub(crate) use deferred::*; +#[allow(unused_imports)] +pub(crate) use evaluate::*; +#[allow(unused_imports)] +pub(crate) use grouped::*; +#[allow(unused_imports)] +pub(crate) use indexes::*; +#[allow(unused_imports)] +pub(crate) use joins::*; +#[allow(unused_imports)] +pub(crate) use manifest::*; +#[allow(unused_imports)] +pub(crate) use paged_tables::*; +#[allow(unused_imports)] +pub(crate) use runtime_keys::*; +#[allow(unused_imports)] +pub(crate) use simple_queries::*; +#[allow(unused_imports)] +pub(crate) use table_data::*; + use expressions::*; use std::borrow::Cow; @@ -267,7 +310,7 @@ pub(crate) struct StoredRow { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -struct RowLocatorV1 { +pub(crate) struct RowLocatorV1 { byte_offset: u32, byte_len: u32, } @@ -281,7 +324,7 @@ struct DeferredCompressedLookupCacheKey { } #[derive(Debug)] -struct DeferredCompressedLookupCacheEntry { +pub(crate) struct DeferredCompressedLookupCacheEntry { payload: Arc>, row_locators: HashMap, } @@ -340,7 +383,7 @@ struct DeferredRuntimeBtreeIndexCache { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -struct RowLocatorV2 { +pub(crate) struct RowLocatorV2 { chunk_index: u32, byte_offset: u32, byte_len: u32, @@ -348,7 +391,7 @@ struct RowLocatorV2 { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum DecodedRowLocator { +pub(crate) enum DecodedRowLocator { V1(RowLocatorV1), V2(RowLocatorV2), } @@ -414,7 +457,7 @@ impl DeferredPagedRowLocators { } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -struct CachedPagedChunkPayloadKey { +pub(crate) struct CachedPagedChunkPayloadKey { head_page_id: PageId, logical_len: u32, flags: u8, @@ -433,7 +476,7 @@ impl CachedPagedChunkPayloadKey { } #[derive(Debug)] -struct DeferredPagedRowLocatorCache { +pub(crate) struct DeferredPagedRowLocatorCache { manifest_pointer: OverflowPointer, manifest_checksum: u32, locators: DeferredPagedRowLocators, @@ -465,463 +508,6 @@ impl DeferredPagedRowLocatorCache { } } -#[derive(Debug)] -pub(crate) struct TableData { - pub(crate) rows: Arc>, - tombstoned_row_ids: BTreeSet, - rows_sorted_by_id: bool, - cached_heap_bytes: usize, -} - -impl Default for TableData { - fn default() -> Self { - Self { - rows: Arc::new(Vec::new()), - tombstoned_row_ids: BTreeSet::new(), - rows_sorted_by_id: true, - cached_heap_bytes: 0, - } - } -} - -impl Clone for TableData { - fn clone(&self) -> Self { - Self { - rows: Arc::clone(&self.rows), - tombstoned_row_ids: self.tombstoned_row_ids.clone(), - rows_sorted_by_id: self.rows_sorted_by_id, - cached_heap_bytes: self.cached_heap_bytes, - } - } -} - -impl PartialEq for TableData { - fn eq(&self, other: &Self) -> bool { - self.rows == other.rows - && self.tombstoned_row_ids == other.tombstoned_row_ids - && self.rows_sorted_by_id == other.rows_sorted_by_id - } -} - -impl TableData { - pub(crate) fn from_rows(rows: Vec) -> Self { - let rows_sorted_by_id = rows.windows(2).all(|pair| pair[0].row_id <= pair[1].row_id); - let mut data = Self { - rows: Arc::new(rows), - tombstoned_row_ids: BTreeSet::new(), - rows_sorted_by_id, - cached_heap_bytes: 0, - }; - data.cached_heap_bytes = data.compute_heap_bytes(); - data - } - - pub(crate) fn row_count(&self) -> usize { - self.rows - .len() - .saturating_sub(self.tombstoned_row_ids.len()) - } - - fn is_row_tombstoned(&self, row_id: i64) -> bool { - self.tombstoned_row_ids.contains(&row_id) - } - - pub(crate) fn has_tombstoned_rows(&self) -> bool { - !self.tombstoned_row_ids.is_empty() - } - - pub(crate) fn visible_rows(&self) -> impl Iterator { - let has_tombstones = !self.tombstoned_row_ids.is_empty(); - self.rows - .iter() - .filter(move |row| !has_tombstones || !self.is_row_tombstoned(row.row_id)) - } - - fn mark_row_deleted(&mut self, row_id: i64) -> bool { - if self.row_index_by_id(row_id).is_some() { - self.tombstoned_row_ids.insert(row_id) - } else { - false - } - } - - pub(crate) fn mark_rows_deleted<'a, I>(&mut self, row_ids: I) -> usize - where - I: IntoIterator, - { - row_ids - .into_iter() - .filter(|row_id| self.mark_row_deleted(**row_id)) - .count() - } - - pub(crate) fn mark_existing_row_set_deleted(&mut self, row_ids: &BTreeSet) -> usize { - if row_ids.is_empty() { - return 0; - } - let before = self.tombstoned_row_ids.len(); - if self.tombstoned_row_ids.is_empty() { - self.tombstoned_row_ids = row_ids.clone(); - } else { - self.tombstoned_row_ids.extend(row_ids.iter().copied()); - } - self.tombstoned_row_ids.len().saturating_sub(before) - } - - #[cfg(test)] - pub(crate) fn reserve_rows(&mut self, additional: usize) { - let rows = Arc::make_mut(&mut self.rows); - let old_capacity = rows.capacity(); - rows.reserve(additional); - self.cached_heap_bytes = self.cached_heap_bytes.saturating_add( - rows.capacity() - .saturating_sub(old_capacity) - .saturating_mul(std::mem::size_of::()), - ); - } - - pub(crate) fn clear_rows(&mut self) { - self.rows = Arc::new(Vec::new()); - self.tombstoned_row_ids.clear(); - self.rows_sorted_by_id = true; - self.cached_heap_bytes = 0; - } - - fn shrink_to_fit_if_unique(&mut self) -> usize { - let Some(rows) = Arc::get_mut(&mut self.rows) else { - return 0; - }; - let old_capacity = rows.capacity(); - rows.shrink_to_fit(); - let freed = old_capacity - .saturating_sub(rows.capacity()) - .saturating_mul(std::mem::size_of::()); - self.cached_heap_bytes = self.cached_heap_bytes.saturating_sub(freed); - freed - } - - pub(crate) fn mutate_visible_rows(&mut self, mut f: F) -> Result<()> - where - F: FnMut(&mut StoredRow) -> Result<()>, - { - let rows = Arc::make_mut(&mut self.rows); - for row in rows.iter_mut() { - if !self.tombstoned_row_ids.contains(&row.row_id) { - f(row)?; - } - } - self.cached_heap_bytes = self.compute_heap_bytes(); - Ok(()) - } - - pub(super) fn row_index_by_id(&self, row_id: i64) -> Option { - if !self.tombstoned_row_ids.is_empty() && self.is_row_tombstoned(row_id) { - return None; - } - if let Some(index) = row_id - .checked_sub(1) - .and_then(|value| usize::try_from(value).ok()) - { - if let Some(row) = self.rows.get(index) { - if row.row_id == row_id { - return Some(index); - } - } - } - - if self.rows_sorted_by_id { - if let Ok(index) = self.rows.binary_search_by_key(&row_id, |row| row.row_id) { - return Some(index); - } - } - - self.rows.iter().position(|row| row.row_id == row_id) - } - - pub(crate) fn row_ids_in_range(&self, low: i64, high: i64) -> Vec { - if low > high { - return Vec::new(); - } - if self.rows_sorted_by_id { - let rows = self.rows.as_ref(); - let start = rows.partition_point(|row| row.row_id < low); - let end = start + rows[start..].partition_point(|row| row.row_id <= high); - if self.tombstoned_row_ids.is_empty() { - return rows[start..end].iter().map(|row| row.row_id).collect(); - } - return rows[start..end] - .iter() - .filter_map(|row| { - if self.is_row_tombstoned(row.row_id) { - None - } else { - Some(row.row_id) - } - }) - .collect(); - } - self.rows - .iter() - .filter_map(|row| { - if row.row_id >= low && row.row_id <= high && !self.is_row_tombstoned(row.row_id) { - Some(row.row_id) - } else { - None - } - }) - .collect() - } - - pub(super) fn row_by_id(&self, row_id: i64) -> Option<&StoredRow> { - self.row_index_by_id(row_id) - .and_then(|index| self.rows.get(index)) - } - - fn projected_values_by_id( - &self, - row_id: i64, - projection_indexes: &[usize], - ) -> Result>> { - Ok(self - .row_by_id(row_id) - .map(|row| project_simple_projection_value_vec(&row.values, projection_indexes))) - } - - fn projected_query_row_by_id( - &self, - row_id: i64, - projection_indexes: &[usize], - ) -> Result> { - Ok(self - .row_by_id(row_id) - .map(|row| project_simple_projection_values(&row.values, projection_indexes))) - } - - fn full_query_row_by_id(&self, row_id: i64) -> Option { - self.row_by_id(row_id) - .map(|row| QueryRow::new(row.values.clone())) - } - - fn projected_query_rows_in_id_range( - &self, - low: i64, - high_exclusive: i64, - limit: usize, - offset: usize, - projection_indexes: &[usize], - ) -> Option> { - if !self.rows_sorted_by_id || high_exclusive <= low { - return None; - } - let rows = self.rows.as_ref(); - let start = rows.partition_point(|row| row.row_id < low); - let end = start + rows[start..].partition_point(|row| row.row_id < high_exclusive); - let mut skipped = 0usize; - let mut projected = Vec::with_capacity(limit.min(end.saturating_sub(start))); - for row in &rows[start..end] { - if self.is_row_tombstoned(row.row_id) { - continue; - } - if skipped < offset { - skipped += 1; - continue; - } - if projected.len() >= limit { - break; - } - projected.push(project_simple_projection_values( - &row.values, - projection_indexes, - )); - } - Some(projected) - } - - fn visit_int64_column_values(&self, column_index: usize, mut visitor: F) -> Result<()> - where - F: FnMut(i64, Option) -> Result<()>, - { - for row in self.visible_rows() { - let value = int64_column_value(row.values.get(column_index))?; - visitor(row.row_id, value)?; - } - Ok(()) - } - - fn visit_float64_column_values(&self, column_index: usize, mut visitor: F) -> Result<()> - where - F: FnMut(i64, Option) -> Result<()>, - { - for row in self.visible_rows() { - let value = float64_column_value(row.values.get(column_index))?; - visitor(row.row_id, value)?; - } - Ok(()) - } - - /// Approximate heap residency of this table's row vector. Includes - /// `Vec` capacity plus each row's `Vec` capacity plus - /// each `Value`'s heap allocations. Excludes the `TableData` struct - /// itself. Used by storage instrumentation (ADR 0143 Phase A). This value - /// is cached and maintained by row mutation helpers. - #[must_use] - pub(crate) fn approximate_heap_bytes(&self) -> usize { - self.cached_heap_bytes - } - - fn compute_heap_bytes(&self) -> usize { - let row_struct = std::mem::size_of::(); - let mut total = self.rows.capacity() * row_struct; - for row in self.rows.iter() { - total += Self::row_heap_bytes(row); - } - total - } - - fn row_heap_bytes(row: &StoredRow) -> usize { - let value_struct = std::mem::size_of::(); - row.values.capacity() * value_struct - + row - .values - .iter() - .map(Value::approximate_heap_bytes) - .sum::() - } - - pub(crate) fn push_row(&mut self, row: StoredRow) { - if self.tombstoned_row_ids.is_empty() { - self.push_fresh_row(row); - return; - } - if self.tombstoned_row_ids.remove(&row.row_id) { - if let Some(index) = self - .rows - .iter() - .position(|candidate| candidate.row_id == row.row_id) - { - let rows = Arc::make_mut(&mut self.rows); - let old_heap_bytes = Self::row_heap_bytes(&rows[index]); - rows[index] = row; - let new_heap_bytes = Self::row_heap_bytes(&rows[index]); - self.cached_heap_bytes = self - .cached_heap_bytes - .saturating_sub(old_heap_bytes) - .saturating_add(new_heap_bytes); - return; - } - } - self.push_fresh_row(row); - } - - fn push_fresh_row(&mut self, row: StoredRow) { - let rows = Arc::make_mut(&mut self.rows); - let old_capacity = rows.capacity(); - if rows.len() == old_capacity { - let additional = if old_capacity < 1024 { - old_capacity.max(8) - } else { - old_capacity / 2 - }; - rows.reserve_exact(additional); - } - let row_heap_bytes = Self::row_heap_bytes(&row); - if rows - .last() - .is_some_and(|previous| previous.row_id > row.row_id) - { - self.rows_sorted_by_id = false; - } - rows.push(row); - self.cached_heap_bytes = self - .cached_heap_bytes - .saturating_add(row_heap_bytes) - .saturating_add( - rows.capacity() - .saturating_sub(old_capacity) - .saturating_mul(std::mem::size_of::()), - ); - } - - #[cfg(test)] - pub(crate) fn remove_row(&mut self, row_index: usize) -> StoredRow { - let rows = Arc::make_mut(&mut self.rows); - let row = rows.remove(row_index); - self.tombstoned_row_ids.remove(&row.row_id); - self.cached_heap_bytes = self - .cached_heap_bytes - .saturating_sub(Self::row_heap_bytes(&row)); - row - } - - #[cfg(test)] - pub(crate) fn retain_rows(&mut self, mut keep: F) - where - F: FnMut(&StoredRow) -> bool, - { - let mut removed_heap_bytes = 0usize; - let rows = Arc::make_mut(&mut self.rows); - rows.retain(|row| { - let retain = keep(row); - if !retain { - removed_heap_bytes = removed_heap_bytes.saturating_add(Self::row_heap_bytes(row)); - self.tombstoned_row_ids.remove(&row.row_id); - } - retain - }); - self.cached_heap_bytes = self.cached_heap_bytes.saturating_sub(removed_heap_bytes); - } - - pub(crate) fn replace_value( - &mut self, - row_index: usize, - column_index: usize, - value: Value, - ) -> Option<()> { - if self - .rows - .get(row_index) - .is_some_and(|row| self.is_row_tombstoned(row.row_id)) - { - return None; - } - let rows = Arc::make_mut(&mut self.rows); - let row = rows.get_mut(row_index)?; - let slot = row.values.get_mut(column_index)?; - let old_heap_bytes = slot.approximate_heap_bytes(); - *slot = value; - let new_heap_bytes = slot.approximate_heap_bytes(); - self.cached_heap_bytes = self - .cached_heap_bytes - .saturating_sub(old_heap_bytes) - .saturating_add(new_heap_bytes); - Some(()) - } - - pub(crate) fn replace_row_values( - &mut self, - row_index: usize, - values: Vec, - ) -> Option<()> { - if self - .rows - .get(row_index) - .is_some_and(|row| self.is_row_tombstoned(row.row_id)) - { - return None; - } - let rows = Arc::make_mut(&mut self.rows); - let row = rows.get_mut(row_index)?; - let old_heap_bytes = Self::row_heap_bytes(row); - row.values = values; - let new_heap_bytes = Self::row_heap_bytes(row); - self.cached_heap_bytes = self - .cached_heap_bytes - .saturating_sub(old_heap_bytes) - .saturating_add(new_heap_bytes); - Some(()) - } -} - #[derive(Debug)] pub(crate) enum TableRowRef<'a> { Resident(&'a StoredRow), @@ -967,7 +553,7 @@ fn float64_column_value(value: Option<&Value>) -> Result> { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -struct TablePageEntry { +pub(crate) struct TablePageEntry { row_id: i64, chunk_index: u32, is_overlay: bool, @@ -975,241 +561,61 @@ struct TablePageEntry { } #[derive(Clone, Debug, PartialEq)] -struct DensePagedRowDirectory { - start_row_id: i64, - locators: Vec, - /// Cumulative exclusive row positions for each physical chunk. - chunk_ends: Vec, -} - -impl DensePagedRowDirectory { - fn empty(chunk_count: usize) -> Result { - let mut chunk_ends = Vec::new(); - try_reserve_paged_directory(&mut chunk_ends, chunk_count, "dense chunk ranges")?; - chunk_ends.resize(chunk_count, 0); - Ok(Self { - start_row_id: 0, - locators: Vec::new(), - chunk_ends, - }) - } - - fn len(&self) -> usize { - self.locators.len() - } +pub(crate) enum TablePageDirectory { + Dense(DensePagedRowDirectory), + Sparse(Vec), +} - fn is_empty(&self) -> bool { - self.locators.is_empty() - } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum PreparedTablePageDirectoryAppend { + Dense { add_chunk: bool }, + Sparse, +} - fn first_row_id(&self) -> Option { - (!self.is_empty()).then_some(self.start_row_id) - } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct PreparedTablePageAppend { + chunk_index: usize, + entry_chunk_index: u32, + is_overlay: bool, + directory: PreparedTablePageDirectoryAppend, +} - fn row_id_at(&self, position: usize) -> Option { - if position >= self.len() { - return None; +impl TablePageDirectory { + fn len(&self) -> usize { + match self { + Self::Dense(directory) => directory.len(), + Self::Sparse(entries) => entries.len(), } - let position = i128::try_from(position).ok()?; - i64::try_from(i128::from(self.start_row_id) + position).ok() } - fn position_for_row_id(&self, row_id: i64) -> Option { - let offset = i128::from(row_id) - i128::from(self.start_row_id); - if offset < 0 { - return None; + fn entry_at(&self, position: usize) -> Result> { + match self { + Self::Dense(directory) => directory.entry_at(position), + Self::Sparse(entries) => Ok(entries.get(position).copied()), } - usize::try_from(offset) - .ok() - .filter(|position| *position < self.len()) } - fn chunk_index_at(&self, position: usize) -> Option { - if position >= self.len() { - return None; + fn position_for_row_id(&self, row_id: i64) -> Option { + match self { + Self::Dense(directory) => directory.position_for_row_id(row_id), + Self::Sparse(entries) => entries + .binary_search_by_key(&row_id, |entry| entry.row_id) + .ok() + .or_else(|| entries.iter().position(|entry| entry.row_id == row_id)), } - let chunk_index = self.chunk_ends.partition_point(|end| *end <= position); - (chunk_index < self.chunk_ends.len()).then_some(chunk_index) } - fn entry_at(&self, position: usize) -> Result> { - let Some(row_id) = self.row_id_at(position) else { + fn entry_for_row_id(&self, row_id: i64) -> Result> { + let Some(position) = self.position_for_row_id(row_id) else { return Ok(None); }; - let chunk_index = self - .chunk_index_at(position) - .ok_or_else(|| DbError::corruption("dense paged row position has no owning chunk"))?; - let locator = *self.locators.get(position).ok_or_else(|| { - DbError::corruption("dense paged row locator position exceeded directory length") - })?; - Ok(Some(TablePageEntry { - row_id, - chunk_index: u32::try_from(chunk_index) - .map_err(|_| DbError::constraint("table chunk index exceeds u32"))?, - is_overlay: false, - locator, - })) + self.entry_at(position) + .map(|entry| entry.map(|entry| (position, entry))) } - fn try_prepare_append( - &mut self, - row_id: i64, - chunk_index: usize, - is_overlay: bool, - ) -> Result> { - if is_overlay { - return Ok(None); - } - let next_position = self.len(); - if !self.is_empty() - && self - .row_id_at(next_position.saturating_sub(1)) - .and_then(|last| last.checked_add(1)) - != Some(row_id) - { - return Ok(None); - } - - let add_chunk = if chunk_index >= self.chunk_ends.len() { - if chunk_index != self.chunk_ends.len() { - return Ok(None); - } - u32::try_from(chunk_index) - .map_err(|_| DbError::constraint("table chunk index exceeds u32"))?; - true - } else if chunk_index + 1 != self.chunk_ends.len() { - return Ok(None); - } else { - false - }; - - if self.locators.len() == self.locators.capacity() { - try_reserve_paged_directory_amortized(&mut self.locators, 1, "dense row locators")?; - } - if add_chunk { - try_reserve_paged_directory(&mut self.chunk_ends, 1, "dense chunk ranges")?; - } - Ok(Some(add_chunk)) - } - - #[cfg(test)] - fn try_append( - &mut self, - row_id: i64, - chunk_index: usize, - is_overlay: bool, - locator: RowLocatorV1, - ) -> Result { - let Some(add_chunk) = self.try_prepare_append(row_id, chunk_index, is_overlay)? else { - return Ok(false); - }; - self.append_prepared(row_id, chunk_index, add_chunk, locator)?; - Ok(true) - } - - fn append_prepared( - &mut self, - row_id: i64, - chunk_index: usize, - add_chunk: bool, - locator: RowLocatorV1, - ) -> Result<()> { - let next_position = self.len(); - if self.is_empty() { - self.start_row_id = row_id; - } - if add_chunk { - self.chunk_ends.push(next_position); - } - self.locators.push(locator); - let Some(chunk_end) = self.chunk_ends.get_mut(chunk_index) else { - return Err(DbError::corruption( - "dense paged append chunk range is missing", - )); - }; - *chunk_end = self.locators.len(); - Ok(()) - } - - fn to_sparse(&self) -> Result> { - let mut entries = Vec::new(); - try_reserve_paged_directory(&mut entries, self.len(), "sparse paged row entries")?; - for position in 0..self.len() { - entries.push(self.entry_at(position)?.ok_or_else(|| { - DbError::corruption("dense paged row directory ended before its locator count") - })?); - } - Ok(entries) - } - - fn approximate_heap_bytes(&self) -> usize { - self.locators - .capacity() - .saturating_mul(std::mem::size_of::()) - .saturating_add( - self.chunk_ends - .capacity() - .saturating_mul(std::mem::size_of::()), - ) - } -} - -#[derive(Clone, Debug, PartialEq)] -enum TablePageDirectory { - Dense(DensePagedRowDirectory), - Sparse(Vec), -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum PreparedTablePageDirectoryAppend { - Dense { add_chunk: bool }, - Sparse, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -struct PreparedTablePageAppend { - chunk_index: usize, - entry_chunk_index: u32, - is_overlay: bool, - directory: PreparedTablePageDirectoryAppend, -} - -impl TablePageDirectory { - fn len(&self) -> usize { - match self { - Self::Dense(directory) => directory.len(), - Self::Sparse(entries) => entries.len(), - } - } - - fn entry_at(&self, position: usize) -> Result> { - match self { - Self::Dense(directory) => directory.entry_at(position), - Self::Sparse(entries) => Ok(entries.get(position).copied()), - } - } - - fn position_for_row_id(&self, row_id: i64) -> Option { - match self { - Self::Dense(directory) => directory.position_for_row_id(row_id), - Self::Sparse(entries) => entries - .binary_search_by_key(&row_id, |entry| entry.row_id) - .ok() - .or_else(|| entries.iter().position(|entry| entry.row_id == row_id)), - } - } - - fn entry_for_row_id(&self, row_id: i64) -> Result> { - let Some(position) = self.position_for_row_id(row_id) else { - return Ok(None); - }; - self.entry_at(position) - .map(|entry| entry.map(|entry| (position, entry))) - } - - fn row_ids_in_range(&self, low: i64, high: i64) -> Vec { - if low > high { - return Vec::new(); + fn row_ids_in_range(&self, low: i64, high: i64) -> Vec { + if low > high { + return Vec::new(); } match self { Self::Dense(directory) => { @@ -1394,11 +800,6 @@ fn reset_paged_row_append_plan_count() { PAGED_ROW_APPEND_PLAN_COUNT.with(|count| count.set(0)); } -#[cfg(test)] -fn paged_row_append_plan_count() -> u64 { - PAGED_ROW_APPEND_PLAN_COUNT.with(std::cell::Cell::get) -} - #[derive(Clone, Debug, PartialEq)] pub(crate) struct TablePageManifestChunk { pub(crate) pointer: OverflowPointer, @@ -1411,15 +812,8 @@ pub(crate) struct TablePageManifestChunk { pub(crate) overlay_payload: Option>>, } -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct TablePageManifest { - chunks: Arc>, - rows: Arc, - tombstoned_row_ids: Arc>, -} - #[derive(Clone, Debug, Eq, PartialEq)] -struct EncodedPagedTableChunk { +pub(crate) struct EncodedPagedTableChunk { payload: Vec, checksum: u32, row_count: usize, @@ -1508,48020 +902,12745 @@ fn try_build_dense_paged_row_directory( Ok(Some(directory)) } -impl TablePageManifest { - fn from_payload(payload: Arc>) -> Result { - let row_count = if payload.is_empty() { - 0 - } else { - read_table_payload_row_count_from_bytes(payload.as_slice())? - }; - let chunk = TablePageManifestChunk { - pointer: OverflowPointer { - head_page_id: 0, - logical_len: 0, - flags: 0, - }, - checksum: 0, - row_count, - payload, - tombstoned_row_ids: Arc::new(BTreeSet::new()), - overlay_pointer: None, - overlay_checksum: None, - overlay_payload: None, - }; - Self::from_chunks(vec![chunk]) +fn table_page_entries_for_chunk( + chunk_index: usize, + chunk: &TablePageManifestChunk, +) -> Result> { + let mut overlay_ids = if chunk + .overlay_payload + .as_ref() + .is_some_and(|payload| !payload.is_empty()) + { + Some(BTreeSet::new()) + } else { + None + }; + if let Some(overlay_payload) = &chunk.overlay_payload { + if !overlay_payload.is_empty() { + let mut cursor = Cursor::new(overlay_payload.as_slice()); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + cursor.read_slice(row_bytes_len)?; + if is_tombstone { + continue; + } + if let Some(overlay_ids) = overlay_ids.as_mut() { + overlay_ids.insert(row_id); + } + } + } } - fn from_chunks(chunks: Vec) -> Result { - let tombstoned_row_ids = chunks - .iter() - .flat_map(|chunk| chunk.tombstoned_row_ids.iter().copied()) - .collect::>(); - - if let Some(directory) = try_build_dense_paged_row_directory(&chunks)? { - return Ok(Self { - chunks: Arc::new(chunks), - rows: Arc::new(TablePageDirectory::Dense(directory)), - tombstoned_row_ids: Arc::new(tombstoned_row_ids), - }); + let mut entries = Vec::new(); + let has_tombstones = !chunk.tombstoned_row_ids.is_empty(); + let overlay_ids = overlay_ids.as_ref(); + if !chunk.payload.is_empty() { + let mut cursor = Cursor::new(chunk.payload.as_slice()); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); } - - // Collect tombstoned row IDs into a set per chunk - let chunk_tombstones: Vec> = chunks - .iter() - .map(|c| c.tombstoned_row_ids.iter().copied().collect()) - .collect(); - - // Collect overlay row IDs per chunk into a set - let chunk_overlay_row_ids: Vec> = chunks - .iter() - .map(|c| { - let mut set = BTreeSet::new(); - if let Some(overlay_payload) = &c.overlay_payload { - if !overlay_payload.is_empty() { - let mut cursor = Cursor::new(overlay_payload.as_slice()); - let magic = cursor - .read_slice(TABLE_PAYLOAD_MAGIC.len()) - .unwrap_or_default(); - if magic == *TABLE_PAYLOAD_MAGIC { - let row_count = cursor.read_u32().unwrap_or(0) as usize; - for _ in 0..row_count { - let row_id = cursor.read_i64().unwrap_or(0); - let row_bytes_len = cursor.read_u32().unwrap_or(0) as usize; - if cursor.read_slice(row_bytes_len).is_err() { - break; - } - set.insert(row_id); - } - } - } - } - set - }) - .collect(); - - let expected_rows = chunks.iter().try_fold(0usize, |total, chunk| { - total - .checked_add(chunk.row_count) - .ok_or_else(|| DbError::constraint("paged table row count overflow")) - })?; - let mut rows = Vec::new(); - try_reserve_paged_directory(&mut rows, expected_rows, "sparse paged row entries")?; - let mut base_row_entries = Vec::new(); - let mut overlay_row_entries = Vec::new(); - - for (chunk_index, chunk) in chunks.iter().enumerate() { - base_row_entries.clear(); - overlay_row_entries.clear(); - - let tombstones = &chunk_tombstones[chunk_index]; - let overlay_ids = &chunk_overlay_row_ids[chunk_index]; - - if !chunk.payload.is_empty() { - let mut cursor = Cursor::new(chunk.payload.as_slice()); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = - split_table_payload_row_len(cursor.read_u32()?); - let row_bytes_offset = cursor.offset; - if is_tombstone { - cursor.read_slice(row_bytes_len)?; - continue; - } - #[cfg(debug_assertions)] - { - let row_bytes = cursor.read_slice(row_bytes_len)?; - Row::decode(row_bytes)?; - } - #[cfg(not(debug_assertions))] - { - cursor.read_slice(row_bytes_len)?; - } - if tombstones.contains(&row_id) || overlay_ids.contains(&row_id) { - continue; // skip tombstoned and overlaid base rows - } - base_row_entries.push(TablePageEntry { - row_id, - chunk_index: u32::try_from(chunk_index) - .map_err(|_| DbError::constraint("table chunk index exceeds u32"))?, - is_overlay: false, - locator: RowLocatorV1 { - byte_offset: u32::try_from(row_bytes_offset).map_err(|_| { - DbError::constraint("row locator offset exceeds u32") - })?, - byte_len: u32::try_from(row_bytes_len).map_err(|_| { - DbError::constraint("row locator length exceeds u32") - })?, - }, - }); + let row_count = cursor.read_u32()? as usize; + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes_offset = cursor.offset; + let _row_bytes = cursor.read_slice(row_bytes_len)?; + if is_tombstone { + continue; + } + if has_tombstones && chunk.tombstoned_row_ids.contains(&row_id) { + continue; + } + if let Some(ids) = overlay_ids { + if ids.contains(&row_id) { + continue; } } + entries.push(TablePageEntry { + row_id, + chunk_index: u32::try_from(chunk_index) + .map_err(|_| DbError::constraint("table chunk index exceeds u32"))?, + is_overlay: false, + locator: RowLocatorV1 { + byte_offset: u32::try_from(row_bytes_offset) + .map_err(|_| DbError::constraint("row locator offset exceeds u32"))?, + byte_len: u32::try_from(row_bytes_len) + .map_err(|_| DbError::constraint("row locator length exceeds u32"))?, + }, + }); + } + } - if let Some(overlay_payload) = &chunk.overlay_payload { - if !overlay_payload.is_empty() { - let mut cursor = Cursor::new(overlay_payload.as_slice()); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = - split_table_payload_row_len(cursor.read_u32()?); - let row_bytes_offset = cursor.offset; - if is_tombstone { - cursor.read_slice(row_bytes_len)?; - continue; - } - #[cfg(debug_assertions)] - { - let row_bytes = cursor.read_slice(row_bytes_len)?; - Row::decode(row_bytes)?; - } - #[cfg(not(debug_assertions))] - { - cursor.read_slice(row_bytes_len)?; - } - overlay_row_entries.push(TablePageEntry { - row_id, - chunk_index: u32::try_from(chunk_index).map_err(|_| { - DbError::constraint("table chunk index exceeds u32") - })?, - is_overlay: true, - locator: RowLocatorV1 { - byte_offset: u32::try_from(row_bytes_offset).map_err(|_| { - DbError::constraint("row locator offset exceeds u32") - })?, - byte_len: u32::try_from(row_bytes_len).map_err(|_| { - DbError::constraint("row locator length exceeds u32") - })?, - }, - }); - } + if let Some(overlay_payload) = &chunk.overlay_payload { + if !overlay_payload.is_empty() { + let mut cursor = Cursor::new(overlay_payload.as_slice()); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes_offset = cursor.offset; + let _row_bytes = cursor.read_slice(row_bytes_len)?; + if is_tombstone { + continue; } + entries.push(TablePageEntry { + row_id, + chunk_index: u32::try_from(chunk_index) + .map_err(|_| DbError::constraint("table chunk index exceeds u32"))?, + is_overlay: true, + locator: RowLocatorV1 { + byte_offset: u32::try_from(row_bytes_offset) + .map_err(|_| DbError::constraint("row locator offset exceeds u32"))?, + byte_len: u32::try_from(row_bytes_len) + .map_err(|_| DbError::constraint("row locator length exceeds u32"))?, + }, + }); } - - // Add base entries first, then overlay entries. Since overlay row_ids - // are not in base_row_entries, the merged list has unique row_ids per chunk. - rows.extend_from_slice(&base_row_entries); - rows.extend_from_slice(&overlay_row_entries); } + } - // Now rows are globally sorted by (chunk_index, row_id), but - // row_by_id needs them sorted by row_id. Sort and verify no duplicates. - rows.sort_by_key(|entry| entry.row_id); - - // Sanity check: after deduplication, there should be no duplicate row_ids - // because overlay_ids were used to skip overlaid base rows. - #[cfg(debug_assertions)] - { - for window in rows.windows(2) { - assert_ne!( - window[0].row_id, window[1].row_id, - "duplicate row_id in TablePageManifest rows" - ); - } - } + Ok(entries) +} - Ok(Self { - chunks: Arc::new(chunks), - rows: Arc::new(TablePageDirectory::Sparse(rows)), - tombstoned_row_ids: Arc::new(tombstoned_row_ids), - }) - } +fn append_encoded_table_payload_row( + payload: &mut Vec, + row_id: i64, + encoded_values: &[u8], +) -> Result { + let physical_row_count = read_table_payload_row_count_from_bytes(payload)?; + encode_i64(payload, row_id); + encode_u32( + payload, + u32::try_from(encoded_values.len()) + .map_err(|_| DbError::constraint("row payload length exceeds u32"))?, + ); + let row_bytes_offset = payload.len(); + payload.extend_from_slice(encoded_values); + let next_physical_row_count = physical_row_count + .checked_add(1) + .ok_or_else(|| DbError::constraint("paged table chunk row count overflow"))?; + payload[TABLE_PAYLOAD_MAGIC.len()..TABLE_PAYLOAD_MAGIC.len() + 4].copy_from_slice( + &u32::try_from(next_physical_row_count) + .map_err(|_| DbError::constraint("paged table chunk row count exceeds u32"))? + .to_le_bytes(), + ); + Ok(RowLocatorV1 { + byte_offset: u32::try_from(row_bytes_offset) + .map_err(|_| DbError::constraint("row locator offset exceeds u32"))?, + byte_len: u32::try_from(encoded_values.len()) + .map_err(|_| DbError::constraint("row locator length exceeds u32"))?, + }) +} - fn from_rows(rows: &[StoredRow], page_size: u32) -> Result { - let chunks = encode_paged_table_chunks_from_rows(rows, page_size)? - .into_iter() - .map(|chunk| TablePageManifestChunk { - pointer: OverflowPointer { - head_page_id: 0, - logical_len: 0, - flags: 0, - }, - checksum: chunk.checksum, - row_count: chunk.row_count, - payload: Arc::new(chunk.payload), - tombstoned_row_ids: Arc::new(BTreeSet::new()), - overlay_pointer: None, - overlay_checksum: None, - overlay_payload: None, - }) - .collect(); - Self::from_chunks(chunks) +fn try_apply_single_paged_row_update_to_manifest( + manifest: &TablePageManifest, + row_id: i64, + next_values: &[Value], +) -> Result> { + let Some((_, entry)) = manifest.rows.entry_for_row_id(row_id)? else { + return Ok(None); + }; + if entry.is_overlay { + return Ok(None); } - - fn row_count(&self) -> usize { - self.rows.len() + let chunk_index = usize::try_from(entry.chunk_index) + .map_err(|_| DbError::corruption("paged table chunk index exceeded chunk list length"))?; + let mut updated_manifest = manifest.clone(); + Arc::make_mut(&mut updated_manifest.rows).sparse_mut()?; + let chunks = Arc::make_mut(&mut updated_manifest.chunks); + let tombstoned_row_ids = Arc::make_mut(&mut updated_manifest.tombstoned_row_ids); + let chunk = chunks + .get_mut(chunk_index) + .ok_or_else(|| DbError::corruption("paged table chunk index exceeded chunk list length"))?; + if chunk.tombstoned_row_ids.contains(&row_id) { + return Ok(None); } - fn append_row(&mut self, row: &StoredRow, page_size: u32) -> Result<()> { - let mut encoded_values = Vec::with_capacity(64); - self.append_row_with_scratch(row, page_size, &mut encoded_values) - } + let overlay_payload = chunk.overlay_payload.get_or_insert_with(|| { + let mut payload = Vec::with_capacity(TABLE_PAYLOAD_MAGIC.len() + 4 + 128); + payload.extend_from_slice(TABLE_PAYLOAD_MAGIC); + payload.extend_from_slice(&0_u32.to_le_bytes()); + Arc::new(payload) + }); + let mut encoded_values = Vec::with_capacity(128); + Row::encode_values_into(next_values, &mut encoded_values)?; + append_encoded_table_payload_row(Arc::make_mut(overlay_payload), row_id, &encoded_values)?; + Arc::make_mut(&mut chunk.tombstoned_row_ids).insert(row_id); + chunk.overlay_pointer = None; + chunk.overlay_checksum = None; + tombstoned_row_ids.insert(row_id); - fn append_row_with_scratch( - &mut self, - row: &StoredRow, - page_size: u32, - encoded_values: &mut Vec, - ) -> Result<()> { - let prepared = self.try_prepare_append_row_with_scratch(row, page_size, encoded_values)?; - self.append_prepared_row_with_scratch(row, page_size, encoded_values, prepared) - } + Ok(Some(updated_manifest)) +} - fn try_prepare_append_row_with_scratch( - &mut self, - row: &StoredRow, - page_size: u32, - encoded_values: &mut Vec, - ) -> Result { - Row::encode_values_into(&row.values, encoded_values)?; - let encoded_row_len = 8usize - .saturating_add(4) - .saturating_add(encoded_values.len()); - let (planned_chunk_index, planned_is_overlay) = - self.planned_append_target(row.row_id, encoded_row_len, page_size)?; - let entry_chunk_index = u32::try_from(planned_chunk_index) - .map_err(|_| DbError::constraint("table chunk index exceeds u32"))?; - let directory = Arc::make_mut(&mut self.rows).try_prepare_append( - row.row_id, - planned_chunk_index, - planned_is_overlay, - )?; - Ok(PreparedTablePageAppend { - chunk_index: planned_chunk_index, - entry_chunk_index, - is_overlay: planned_is_overlay, - directory, - }) +impl From for TableRowSource { + fn from(data: TableData) -> Self { + Self::Resident(Arc::new(data)) } +} - fn planned_append_target( - &self, - row_id: i64, - encoded_row_len: usize, - page_size: u32, - ) -> Result<(usize, bool)> { - #[cfg(test)] - PAGED_ROW_APPEND_PLAN_COUNT.with(|count| count.set(count.get().saturating_add(1))); - if self.tombstoned_row_ids.contains(&row_id) { - let chunk_index = self - .chunks - .iter() - .position(|chunk| chunk.tombstoned_row_ids.contains(&row_id)) - .ok_or_else(|| { - DbError::corruption( - "paged table tombstone index referenced a missing chunk tombstone", - ) - })?; - return Ok((chunk_index, true)); - } - let target_chunk_bytes = paged_table_target_chunk_bytes(page_size); - let chunk_index = if self.chunks.last().is_some_and(|chunk| { - chunk.payload.len().saturating_add(encoded_row_len) <= target_chunk_bytes - }) { - self.chunks.len() - 1 - } else { - self.chunks.len() - }; - Ok((chunk_index, false)) +impl From for TableRowSource { + fn from(manifest: TablePageManifest) -> Self { + Self::Paged(Arc::new(manifest)) } +} - fn append_prepared_row_with_scratch( - &mut self, - row: &StoredRow, - page_size: u32, - encoded_values: &[u8], - prepared: PreparedTablePageAppend, - ) -> Result<()> { - let encoded_row_len = 8usize - .saturating_add(4) - .saturating_add(encoded_values.len()); - let target_chunk_bytes = paged_table_target_chunk_bytes(page_size); - - let chunks = Arc::make_mut(&mut self.chunks); - let locator = if prepared.is_overlay { - let chunk_index = prepared.chunk_index; - let chunk = chunks - .get_mut(chunk_index) - .ok_or_else(|| DbError::internal("paged append chunk index was out of bounds"))?; - let overlay_payload = chunk.overlay_payload.get_or_insert_with(|| { - let mut payload = Vec::with_capacity( - target_chunk_bytes.max(TABLE_PAYLOAD_MAGIC.len() + 4 + encoded_row_len), - ); - payload.extend_from_slice(TABLE_PAYLOAD_MAGIC); - payload.extend_from_slice(&0_u32.to_le_bytes()); - Arc::new(payload) - }); - let locator = append_encoded_table_payload_row( - Arc::make_mut(overlay_payload), - row.row_id, - encoded_values, - )?; - chunk.overlay_checksum = None; - chunk.row_count = chunk - .row_count - .checked_add(1) - .ok_or_else(|| DbError::constraint("paged table chunk row count overflow"))?; - locator - } else { - let chunk_index = prepared.chunk_index; - if chunk_index == chunks.len() { - let mut payload = - Vec::with_capacity(target_chunk_bytes.max(TABLE_PAYLOAD_MAGIC.len() + 4)); - payload.extend_from_slice(TABLE_PAYLOAD_MAGIC); - payload.extend_from_slice(&0_u32.to_le_bytes()); - chunks.push(TablePageManifestChunk { - pointer: OverflowPointer { - head_page_id: 0, - logical_len: 0, - flags: 0, - }, - checksum: 0, - row_count: 0, - payload: Arc::new(payload), - tombstoned_row_ids: Arc::new(BTreeSet::new()), - overlay_pointer: None, - overlay_checksum: None, - overlay_payload: None, - }); - } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct PersistedTableState { + pub(crate) pointer: OverflowPointer, + pub(crate) checksum: u32, + pub(crate) row_count: usize, + pub(crate) tail: OverflowTailInfo, + pub(crate) pk_index_root: Option, +} - let chunk = chunks - .get_mut(chunk_index) - .ok_or_else(|| DbError::internal("paged append chunk index was out of bounds"))?; - let locator = append_encoded_table_payload_row( - Arc::make_mut(&mut chunk.payload), - row.row_id, - encoded_values, - )?; - chunk.pointer = OverflowPointer { +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct PersistedTableChunkState { + pub(crate) pointer: OverflowPointer, + pub(crate) checksum: u32, + pub(crate) row_count: usize, + pub(crate) tombstoned_row_ids: Vec, + pub(crate) overlay_pointer: Option, + pub(crate) overlay_checksum: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct PersistedPagedTableManifest { + pub(crate) chunks: Vec, +} + +impl Default for PersistedTableState { + fn default() -> Self { + Self { + pointer: OverflowPointer { head_page_id: 0, logical_len: 0, flags: 0, - }; - chunk.checksum = 0; - chunk.row_count = chunk - .row_count - .checked_add(1) - .ok_or_else(|| DbError::constraint("paged table chunk row count overflow"))?; - locator - }; - - let entry = TablePageEntry { - row_id: row.row_id, - chunk_index: prepared.entry_chunk_index, - is_overlay: prepared.is_overlay, - locator, - }; - let directory = Arc::make_mut(&mut self.rows); - let rows = match (directory, prepared.directory) { - ( - TablePageDirectory::Dense(rows), - PreparedTablePageDirectoryAppend::Dense { add_chunk }, - ) => { - debug_assert!(!prepared.is_overlay); - rows.append_prepared(row.row_id, prepared.chunk_index, add_chunk, locator)?; - return Ok(()); - } - (TablePageDirectory::Sparse(rows), PreparedTablePageDirectoryAppend::Sparse) => rows, - _ => { - return Err(DbError::corruption( - "paged row directory changed after append preparation", - )); - } - }; - if rows - .last() - .is_none_or(|existing| existing.row_id < entry.row_id) - { - rows.push(entry); - return Ok(()); - } - match rows.binary_search_by_key(&entry.row_id, |existing| existing.row_id) { - Ok(_) => Err(DbError::constraint( - "duplicate row id in paged table append", - )), - Err(position) => { - rows.insert(position, entry); - Ok(()) - } + }, + checksum: 0, + row_count: 0, + tail: OverflowTailInfo::default(), + pk_index_root: None, } } +} - fn row_by_id(&self, row_id: i64) -> Result>> { - let Some((position, _)) = self.rows.entry_for_row_id(row_id)? else { - return Ok(None); - }; - self.row_at_position(position) +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum RuntimeBtreeKey { + Encoded(RuntimeEncodedKey), + Int64(i64), + Uuid([u8; 16]), +} + +#[derive(Default)] +pub(crate) struct Int64IdentityHasher(u64); + +impl Hasher for Int64IdentityHasher { + fn finish(&self) -> u64 { + self.0 } - pub(crate) fn row_ids_in_range(&self, low: i64, high: i64) -> Vec { - self.rows.row_ids_in_range(low, high) + fn write(&mut self, bytes: &[u8]) { + // Runtime INT64 index keys are hashed via write_i64/write_u64. This fallback + // preserves determinism for any incidental byte-oriented hashing. + let mut hash = 0_u64; + for (shift, byte) in bytes.iter().copied().take(8).enumerate() { + hash |= u64::from(byte) << (shift * 8); + } + self.0 = hash; } - /// Returns the chunk index owning `row_id`, if present. Used by the bulk - /// delete manifest rebuild to avoid decoding base payloads. - fn chunk_index_for_row_id(&self, row_id: i64) -> Option { - self.rows - .entry_for_row_id(row_id) - .ok() - .flatten() - .map(|(_, entry)| entry.chunk_index as usize) + fn write_i64(&mut self, value: i64) { + self.0 = value as u64; } - fn projected_values_by_id( - &self, - row_id: i64, - projection_indexes: &[usize], - ) -> Result>> { - let Some((position, _)) = self.rows.entry_for_row_id(row_id)? else { - return Ok(None); - }; - self.projected_values_at_position(position, projection_indexes) + fn write_u64(&mut self, value: u64) { + self.0 = value; } +} - fn row_bytes_for_entry<'a>( - &'a self, - entry: TablePageEntry, - chunk: &'a TablePageManifestChunk, - ) -> Result> { - if entry.is_overlay { - let payload = chunk - .overlay_payload - .as_ref() - .ok_or_else(|| DbError::corruption("paged table overlay chunk is missing"))?; - return Self::row_bytes_from_locator(payload.as_slice(), entry.locator).map(Some); - } +type Int64HashBuilder = BuildHasherDefault; +type Int64Map = HashMap; - if !self.tombstoned_row_ids.is_empty() && chunk.tombstoned_row_ids.contains(&entry.row_id) { - let Some(overlay_payload) = &chunk.overlay_payload else { - return Ok(None); - }; - return Self::row_bytes_from_tombstoned_base(overlay_payload.as_slice(), entry.row_id) - .map(Some); - } - - let start = entry.locator.byte_offset as usize; - let end = start - .checked_add(entry.locator.byte_len as usize) - .ok_or_else(|| DbError::corruption("paged row locator exceeded address space"))?; - let row_bytes = chunk - .payload - .as_slice() - .get(start..end) - .ok_or_else(|| DbError::corruption("paged row locator exceeded payload length"))?; - Ok(Some(row_bytes)) - } - - fn row_bytes_from_locator(payload: &[u8], locator: RowLocatorV1) -> Result<&[u8]> { - let start = locator.byte_offset as usize; - let end = start - .checked_add(locator.byte_len as usize) - .ok_or_else(|| DbError::corruption("paged row locator exceeded address space"))?; - payload - .get(start..end) - .ok_or_else(|| DbError::corruption("paged row locator exceeded payload length")) +impl From> for UniqueInt64Keys { + fn from(keys: Int64Map) -> Self { + Self::Sparse(keys) } +} - fn row_bytes_from_tombstoned_base(overlay_payload: &[u8], row_id: i64) -> Result<&[u8]> { - if overlay_payload.is_empty() { - return Err(DbError::corruption( - "paged table overlay row is missing from overlay payload", - )); - } - let mut cursor = Cursor::new(overlay_payload); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - let mut matched_row_bytes = None; - for _ in 0..row_count { - let current_row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes = cursor.read_slice(row_bytes_len)?; - if is_tombstone { - continue; - } - if current_row_id == row_id { - matched_row_bytes = Some(row_bytes); - } - } - matched_row_bytes.ok_or_else(|| { - DbError::corruption("paged table overlay row is missing from overlay payload") - }) - } +/// Row IDs stored beneath one key in a non-unique typed `INT64` index. +/// +/// Foreign-key-like indexes commonly receive monotonically increasing row IDs +/// grouped by key. Representing those postings as an inline singleton or a +/// contiguous range avoids one heap allocation per distinct key. Irregular +/// insertion order falls back to the same `Vec` semantics used previously. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum RuntimeInt64RowIds { + One(i64), + Contiguous { start: i64, len: usize }, + Many(Vec), +} - fn row_at_position(&self, position: usize) -> Result>> { - let Some(entry) = self.rows.entry_at(position)? else { - return Ok(None); - }; - let chunk = self.chunks.get(entry.chunk_index as usize).ok_or_else(|| { - DbError::corruption("paged table chunk index exceeded chunk list length") - })?; - let Some(row_bytes) = self.row_bytes_for_entry(entry, chunk)? else { - return Ok(None); - }; - let row = Row::decode(row_bytes)?; - Ok(Some(TableRowRef::Decoded(StoredRow { - row_id: entry.row_id, - values: row.into_values(), - }))) +impl RuntimeInt64RowIds { + fn one(row_id: i64) -> Self { + Self::One(row_id) } - fn projected_values_at_position( - &self, - position: usize, - projection_indexes: &[usize], - ) -> Result>> { - let Some(entry) = self.rows.entry_at(position)? else { - return Ok(None); - }; - let chunk = self.chunks.get(entry.chunk_index as usize).ok_or_else(|| { - DbError::corruption("paged table chunk index exceeded chunk list length") - })?; - let Some(row_bytes) = self.row_bytes_for_entry(entry, chunk)? else { - return Ok(None); - }; - Row::decode_projection_sorted_unique_with_overflow::( - row_bytes, - None, - projection_indexes, - ) - .map(Some) - } - - fn full_query_row_by_id(&self, row_id: i64) -> Result> { - let Some((_, entry)) = self.rows.entry_for_row_id(row_id)? else { - return Ok(None); - }; - let chunk = self.chunks.get(entry.chunk_index as usize).ok_or_else(|| { - DbError::corruption("paged table chunk index exceeded chunk list length") - })?; - let Some(row_bytes) = self.row_bytes_for_entry(entry, chunk)? else { - return Ok(None); - }; - Row::decode(row_bytes) - .map(Row::into_values) - .map(QueryRow::new) - .map(Some) - } - - fn visit_int64_column_values(&self, column_index: usize, mut visitor: F) -> Result<()> - where - F: FnMut(i64, Option) -> Result<()>, - { - for entry in self.rows.iter() { - let entry = entry?; - let chunk = self.chunks.get(entry.chunk_index as usize).ok_or_else(|| { - DbError::corruption("paged table chunk index exceeded chunk list length") - })?; - let Some(row_bytes) = self.row_bytes_for_entry(entry, chunk)? else { - continue; - }; - visitor(entry.row_id, Row::decode_int64_at(row_bytes, column_index)?)?; - } - Ok(()) - } - - fn visit_float64_column_values(&self, column_index: usize, mut visitor: F) -> Result<()> - where - F: FnMut(i64, Option) -> Result<()>, - { - for entry in self.rows.iter() { - let entry = entry?; - let chunk = self.chunks.get(entry.chunk_index as usize).ok_or_else(|| { - DbError::corruption("paged table chunk index exceeded chunk list length") - })?; - let Some(row_bytes) = self.row_bytes_for_entry(entry, chunk)? else { - continue; - }; - visitor( - entry.row_id, - Row::decode_float64_at(row_bytes, column_index)?, - )?; + fn len(&self) -> usize { + match self { + Self::One(_) => 1, + Self::Contiguous { len, .. } => *len, + Self::Many(row_ids) => row_ids.len(), } - Ok(()) } - fn rows(&self) -> TablePageRowIter<'_> { - TablePageRowIter { - manifest: self, - position: 0, - } + fn is_empty(&self) -> bool { + matches!(self, Self::Many(row_ids) if row_ids.is_empty()) } - fn approximate_heap_bytes(&self) -> usize { - let chunks_bytes = self - .chunks - .capacity() - .saturating_mul(std::mem::size_of::()); - let chunk_payload_bytes = self.chunks.iter().fold(0usize, |bytes, chunk| { - bytes - .saturating_add(chunk.payload.capacity()) - .saturating_add( - chunk - .overlay_payload - .as_ref() - .map_or(0, |payload| payload.capacity()), - ) - .saturating_add( - chunk - .tombstoned_row_ids - .len() - .saturating_mul(std::mem::size_of::()), - ) - }); - chunks_bytes - .saturating_add(chunk_payload_bytes) - .saturating_add( - self.tombstoned_row_ids - .len() - .saturating_mul(std::mem::size_of::()), - ) - .saturating_add(self.rows.approximate_heap_bytes()) + fn value_at(start: i64, offset: usize) -> Option { + let offset = i128::try_from(offset).ok()?; + i64::try_from(i128::from(start) + offset).ok() } -} -fn table_page_entries_for_chunk( - chunk_index: usize, - chunk: &TablePageManifestChunk, -) -> Result> { - let mut overlay_ids = if chunk - .overlay_payload - .as_ref() - .is_some_and(|payload| !payload.is_empty()) - { - Some(BTreeSet::new()) - } else { - None - }; - if let Some(overlay_payload) = &chunk.overlay_payload { - if !overlay_payload.is_empty() { - let mut cursor = Cursor::new(overlay_payload.as_slice()); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - cursor.read_slice(row_bytes_len)?; - if is_tombstone { - continue; - } - if let Some(overlay_ids) = overlay_ids.as_mut() { - overlay_ids.insert(row_id); + fn from_vec(row_ids: Vec) -> Self { + match row_ids.as_slice() { + [] => Self::Many(row_ids), + [row_id] => Self::One(*row_id), + [start, rest @ ..] + if rest.iter().copied().enumerate().all(|(offset, row_id)| { + Self::value_at(*start, offset.saturating_add(1)) == Some(row_id) + }) => + { + Self::Contiguous { + start: *start, + len: row_ids.len(), } } + _ => Self::Many(row_ids), } } - let mut entries = Vec::new(); - let has_tombstones = !chunk.tombstoned_row_ids.is_empty(); - let overlay_ids = overlay_ids.as_ref(); - if !chunk.payload.is_empty() { - let mut cursor = Cursor::new(chunk.payload.as_slice()); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes_offset = cursor.offset; - let _row_bytes = cursor.read_slice(row_bytes_len)?; - if is_tombstone { - continue; + fn push(&mut self, row_id: i64) { + match self { + Self::One(first_row_id) if first_row_id.checked_add(1) == Some(row_id) => { + *self = Self::Contiguous { + start: *first_row_id, + len: 2, + }; } - if has_tombstones && chunk.tombstoned_row_ids.contains(&row_id) { - continue; + Self::One(first_row_id) => { + let mut row_ids = Vec::with_capacity(4); + row_ids.push(*first_row_id); + row_ids.push(row_id); + *self = Self::Many(row_ids); } - if let Some(ids) = overlay_ids { - if ids.contains(&row_id) { - continue; + Self::Contiguous { start, len } if Self::value_at(*start, *len) == Some(row_id) => { + *len = len.saturating_add(1); + } + Self::Contiguous { start, len } => { + let start = *start; + let len = *len; + let mut row_ids = Vec::with_capacity(len.saturating_add(1)); + for offset in 0..len { + if let Some(existing) = Self::value_at(start, offset) { + row_ids.push(existing); + } } + row_ids.push(row_id); + *self = Self::Many(row_ids); } - entries.push(TablePageEntry { - row_id, - chunk_index: u32::try_from(chunk_index) - .map_err(|_| DbError::constraint("table chunk index exceeds u32"))?, - is_overlay: false, - locator: RowLocatorV1 { - byte_offset: u32::try_from(row_bytes_offset) - .map_err(|_| DbError::constraint("row locator offset exceeds u32"))?, - byte_len: u32::try_from(row_bytes_len) - .map_err(|_| DbError::constraint("row locator length exceeds u32"))?, - }, - }); + Self::Many(row_ids) => row_ids.push(row_id), } } - if let Some(overlay_payload) = &chunk.overlay_payload { - if !overlay_payload.is_empty() { - let mut cursor = Cursor::new(overlay_payload.as_slice()); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes_offset = cursor.offset; - let _row_bytes = cursor.read_slice(row_bytes_len)?; - if is_tombstone { - continue; - } - entries.push(TablePageEntry { - row_id, - chunk_index: u32::try_from(chunk_index) - .map_err(|_| DbError::constraint("table chunk index exceeds u32"))?, - is_overlay: true, - locator: RowLocatorV1 { - byte_offset: u32::try_from(row_bytes_offset) - .map_err(|_| DbError::constraint("row locator offset exceeds u32"))?, - byte_len: u32::try_from(row_bytes_len) - .map_err(|_| DbError::constraint("row locator length exceeds u32"))?, - }, - }); + fn contains(&self, row_id: &i64) -> bool { + match self { + Self::One(existing) => existing == row_id, + Self::Contiguous { start, len } => { + UniqueInt64Keys::dense_contains(*start, *len, *row_id) } + Self::Many(row_ids) => row_ids.contains(row_id), } } - Ok(entries) -} - -fn append_encoded_table_payload_row( - payload: &mut Vec, - row_id: i64, - encoded_values: &[u8], -) -> Result { - let physical_row_count = read_table_payload_row_count_from_bytes(payload)?; - encode_i64(payload, row_id); - encode_u32( - payload, - u32::try_from(encoded_values.len()) - .map_err(|_| DbError::constraint("row payload length exceeds u32"))?, - ); - let row_bytes_offset = payload.len(); - payload.extend_from_slice(encoded_values); - let next_physical_row_count = physical_row_count - .checked_add(1) - .ok_or_else(|| DbError::constraint("paged table chunk row count overflow"))?; - payload[TABLE_PAYLOAD_MAGIC.len()..TABLE_PAYLOAD_MAGIC.len() + 4].copy_from_slice( - &u32::try_from(next_physical_row_count) - .map_err(|_| DbError::constraint("paged table chunk row count exceeds u32"))? - .to_le_bytes(), - ); - Ok(RowLocatorV1 { - byte_offset: u32::try_from(row_bytes_offset) - .map_err(|_| DbError::constraint("row locator offset exceeds u32"))?, - byte_len: u32::try_from(encoded_values.len()) - .map_err(|_| DbError::constraint("row locator length exceeds u32"))?, - }) -} - -fn try_apply_paged_row_changes_to_manifest_update_only( - manifest: &TablePageManifest, - row_changes: &BTreeMap>>, -) -> Result> { - if row_changes.is_empty() { - return Ok(None); - } - - let mut planned_changes = Vec::with_capacity(row_changes.len()); - for (row_id, change) in row_changes { - let Some(next_values) = change.as_ref() else { - return Ok(None); - }; - let Some((_, entry)) = manifest.rows.entry_for_row_id(*row_id)? else { - return Ok(None); - }; - if entry.is_overlay { - return Ok(None); + fn iter(&self) -> RuntimeInt64RowIdsIter<'_> { + match self { + Self::One(row_id) => RuntimeInt64RowIdsIter::One(Some(*row_id)), + Self::Contiguous { start, len } => RuntimeInt64RowIdsIter::Contiguous { + start: *start, + offset: 0, + len: *len, + }, + Self::Many(row_ids) => RuntimeInt64RowIdsIter::Many(row_ids.iter()), } - let chunk_index = usize::try_from(entry.chunk_index).map_err(|_| { - DbError::corruption("paged table chunk index exceeded chunk list length") - })?; - planned_changes.push((*row_id, chunk_index, next_values.clone())); } - let mut updated_manifest = manifest.clone(); - Arc::make_mut(&mut updated_manifest.rows).sparse_mut()?; - let chunks = Arc::make_mut(&mut updated_manifest.chunks); - let tombstoned_row_ids = Arc::make_mut(&mut updated_manifest.tombstoned_row_ids); - - for (row_id, chunk_index, next_values) in planned_changes { - let chunk = chunks.get_mut(chunk_index).ok_or_else(|| { - DbError::corruption("paged table chunk index exceeded chunk list length") - })?; - if chunk.tombstoned_row_ids.contains(&row_id) { - return Ok(None); - } - - let overlay_payload = chunk.overlay_payload.get_or_insert_with(|| { - let mut payload = Vec::with_capacity(TABLE_PAYLOAD_MAGIC.len() + 4 + 128); - payload.extend_from_slice(TABLE_PAYLOAD_MAGIC); - payload.extend_from_slice(&0_u32.to_le_bytes()); - Arc::new(payload) - }); - let mut encoded_values = Vec::with_capacity(128); - Row::encode_values_into(&next_values, &mut encoded_values)?; - append_encoded_table_payload_row(Arc::make_mut(overlay_payload), row_id, &encoded_values)?; - Arc::make_mut(&mut chunk.tombstoned_row_ids).insert(row_id); - chunk.overlay_pointer = None; - chunk.overlay_checksum = None; - tombstoned_row_ids.insert(row_id); + fn to_vec(&self) -> Vec { + self.iter().collect() } - Ok(Some(updated_manifest)) -} - -fn try_apply_single_paged_row_update_to_manifest( - manifest: &TablePageManifest, - row_id: i64, - next_values: &[Value], -) -> Result> { - let Some((_, entry)) = manifest.rows.entry_for_row_id(row_id)? else { - return Ok(None); - }; - if entry.is_overlay { - return Ok(None); - } - let chunk_index = usize::try_from(entry.chunk_index) - .map_err(|_| DbError::corruption("paged table chunk index exceeded chunk list length"))?; - let mut updated_manifest = manifest.clone(); - Arc::make_mut(&mut updated_manifest.rows).sparse_mut()?; - let chunks = Arc::make_mut(&mut updated_manifest.chunks); - let tombstoned_row_ids = Arc::make_mut(&mut updated_manifest.tombstoned_row_ids); - let chunk = chunks - .get_mut(chunk_index) - .ok_or_else(|| DbError::corruption("paged table chunk index exceeded chunk list length"))?; - if chunk.tombstoned_row_ids.contains(&row_id) { - return Ok(None); - } - - let overlay_payload = chunk.overlay_payload.get_or_insert_with(|| { - let mut payload = Vec::with_capacity(TABLE_PAYLOAD_MAGIC.len() + 4 + 128); - payload.extend_from_slice(TABLE_PAYLOAD_MAGIC); - payload.extend_from_slice(&0_u32.to_le_bytes()); - Arc::new(payload) - }); - let mut encoded_values = Vec::with_capacity(128); - Row::encode_values_into(next_values, &mut encoded_values)?; - append_encoded_table_payload_row(Arc::make_mut(overlay_payload), row_id, &encoded_values)?; - Arc::make_mut(&mut chunk.tombstoned_row_ids).insert(row_id); - chunk.overlay_pointer = None; - chunk.overlay_checksum = None; - tombstoned_row_ids.insert(row_id); - - Ok(Some(updated_manifest)) -} - -pub(crate) enum TableRowIter<'a> { - Empty(std::iter::Empty>>), - Resident(TableDataRowIter<'a>), - Paged(TablePageRowIter<'a>), -} - -impl<'a> Iterator for TableRowIter<'a> { - type Item = Result>; - - fn next(&mut self) -> Option { + fn retain(&mut self, mut retain: impl FnMut(&i64) -> bool) { match self { - Self::Empty(iter) => iter.next(), - Self::Resident(iter) => iter.next().map(|row| Ok(TableRowRef::Resident(row))), - Self::Paged(iter) => iter.next(), + Self::One(row_id) => { + if !retain(row_id) { + *self = Self::Many(Vec::new()); + } + } + Self::Contiguous { .. } => { + let retained = self + .iter() + .filter(|row_id| retain(row_id)) + .collect::>(); + *self = Self::from_vec(retained); + } + Self::Many(row_ids) => row_ids.retain(retain), } } - fn size_hint(&self) -> (usize, Option) { - match self { - Self::Empty(iter) => iter.size_hint(), - Self::Resident(iter) => iter.size_hint(), - Self::Paged(iter) => iter.size_hint(), + fn shrink_to_fit(&mut self) -> usize { + let Self::Many(row_ids) = self else { + return 0; + }; + let old_capacity = row_ids.capacity(); + let compact = Self::from_vec(std::mem::take(row_ids)); + if matches!(compact, Self::Many(_)) { + *self = compact; + let Self::Many(row_ids) = self else { + return 0; + }; + row_ids.shrink_to_fit(); + return old_capacity + .saturating_sub(row_ids.capacity()) + .saturating_mul(std::mem::size_of::()); } + *self = compact; + old_capacity.saturating_mul(std::mem::size_of::()) } } -impl ExactSizeIterator for TableRowIter<'_> { - fn len(&self) -> usize { - match self { - Self::Empty(iter) => iter.len(), - Self::Resident(iter) => iter.len(), - Self::Paged(iter) => iter.len(), - } +pub(crate) fn contiguous_row_ids(start: i64, len: usize) -> RuntimeInt64RowIdsIter<'static> { + RuntimeInt64RowIdsIter::Contiguous { + start, + offset: 0, + len, } } -pub(crate) struct TableDataRowIter<'a> { - rows: std::slice::Iter<'a, StoredRow>, - tombstoned_row_ids: &'a BTreeSet, - remaining: usize, -} +impl<'a> IntoIterator for &'a RuntimeInt64RowIds { + type Item = i64; + type IntoIter = RuntimeInt64RowIdsIter<'a>; -impl<'a> TableDataRowIter<'a> { - fn new(data: &'a TableData) -> Self { - Self { - rows: data.rows.iter(), - tombstoned_row_ids: &data.tombstoned_row_ids, - remaining: data.row_count(), - } + fn into_iter(self) -> Self::IntoIter { + self.iter() } } -impl<'a> Iterator for TableDataRowIter<'a> { - type Item = &'a StoredRow; - - fn next(&mut self) -> Option { - for row in self.rows.by_ref() { - if self.tombstoned_row_ids.contains(&row.row_id) { - continue; - } - self.remaining = self.remaining.saturating_sub(1); - return Some(row); +impl From>> for NonUniqueInt64Keys { + fn from(keys: Int64Map>) -> Self { + let mut postings = + Int64Map::with_capacity_and_hasher(keys.capacity(), Int64HashBuilder::default()); + for (key, row_ids) in keys { + postings.insert(key, RuntimeInt64RowIds::from_vec(row_ids)); } - self.remaining = 0; - None - } - - fn size_hint(&self) -> (usize, Option) { - (self.remaining, Some(self.remaining)) - } -} - -impl ExactSizeIterator for TableDataRowIter<'_> { - fn len(&self) -> usize { - self.remaining - } -} - -impl<'a> TableRowIter<'a> { - fn empty() -> Self { - Self::Empty(std::iter::empty()) + Self::Sparse(postings) } } -pub(crate) struct TablePageRowIter<'a> { - manifest: &'a TablePageManifest, - position: usize, -} - -impl<'a> Iterator for TablePageRowIter<'a> { - type Item = Result>; - - fn next(&mut self) -> Option { - let position = self.position; - if position >= self.manifest.row_count() { - return None; - } - self.position += 1; - Some(self.manifest.row_at_position(position).and_then(|row| { - row.ok_or_else(|| { - DbError::corruption("paged row iterator advanced beyond manifest bounds") - }) - })) - } +// Keep every posting object no larger than the Vec it replaces on all +// supported pointer widths, including wasm32. This intentionally lives in +// production code so cross-target checks enforce the layout invariant. +const _: () = + assert!(std::mem::size_of::() == std::mem::size_of::>()); - fn size_hint(&self) -> (usize, Option) { - let remaining = self.manifest.row_count().saturating_sub(self.position); - (remaining, Some(remaining)) - } -} +impl<'a> IntoIterator for &'a RuntimeEncodedRowIds { + type Item = &'a i64; + type IntoIter = std::slice::Iter<'a, i64>; -impl ExactSizeIterator for TablePageRowIter<'_> { - fn len(&self) -> usize { - self.manifest.row_count().saturating_sub(self.position) + fn into_iter(self) -> Self::IntoIter { + self.as_slice().iter() } } -#[derive(Clone, Copy)] -enum VisibleTableRowSource<'a> { - Temp(&'a TableData), - Base(&'a TableRowSource), +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum RuntimeRowIdSet<'a> { + Empty, + Single(i64), + Contiguous { start: i64, len: usize }, + Many(&'a [i64]), + Owned(Vec), } -impl<'a> VisibleTableRowSource<'a> { - fn rows(&self) -> TableRowIter<'a> { +impl RuntimeRowIdSet<'_> { + #[must_use] + pub(crate) fn len(&self) -> usize { match self { - Self::Temp(data) => TableRowIter::Resident(TableDataRowIter::new(data)), - Self::Base(source) => source.rows(), + Self::Empty => 0, + Self::Single(_) => 1, + Self::Contiguous { len, .. } => *len, + Self::Many(values) => values.len(), + Self::Owned(values) => values.len(), } } - fn row_count(&self) -> usize { - match self { - Self::Temp(data) => data.row_count(), - Self::Base(source) => source.row_count(), - } + #[must_use] + pub(crate) fn is_empty(&self) -> bool { + matches!(self, Self::Empty) } - fn has_tombstoned_rows(&self) -> bool { + pub(crate) fn for_each(&self, mut f: impl FnMut(i64)) { match self { - Self::Temp(data) => data.has_tombstoned_rows(), - Self::Base(source) => source.has_tombstoned_rows(), + Self::Empty => {} + Self::Single(row_id) => f(*row_id), + Self::Contiguous { start, len } => { + for offset in 0..*len { + if let Some(row_id) = RuntimeInt64RowIds::value_at(*start, offset) { + f(row_id); + } + } + } + Self::Many(values) => { + for row_id in *values { + f(*row_id); + } + } + Self::Owned(values) => { + for row_id in values { + f(*row_id); + } + } } } - fn row_by_id(&self, row_id: i64) -> Result>> { + fn visit_until(self, mut visitor: impl FnMut(i64) -> Result) -> Result { match self { - Self::Temp(data) => Ok(data.row_by_id(row_id).map(TableRowRef::Resident)), - Self::Base(source) => source.row_by_id(row_id), + Self::Empty => Ok(false), + Self::Single(row_id) => visitor(row_id), + Self::Contiguous { start, len } => { + for offset in 0..len { + let Some(row_id) = RuntimeInt64RowIds::value_at(start, offset) else { + break; + }; + if visitor(row_id)? { + return Ok(true); + } + } + Ok(false) + } + Self::Many(values) => { + for row_id in values { + if visitor(*row_id)? { + return Ok(true); + } + } + Ok(false) + } + Self::Owned(values) => { + for row_id in values { + if visitor(row_id)? { + return Ok(true); + } + } + Ok(false) + } } } +} - fn row_ids_in_range(&self, low: i64, high: i64) -> Vec { - if low > high { - return Vec::new(); +fn visible_row_id_set_count( + row_source: VisibleTableRowSource<'_>, + row_ids: RuntimeRowIdSet<'_>, +) -> Result { + if !row_source.has_tombstoned_rows() { + return Ok(row_ids.len()); + } + let mut count = 0usize; + let mut error = None; + row_ids.for_each(|row_id| { + if error.is_some() { + return; } - match self { - Self::Temp(data) => data - .rows - .iter() - .filter_map(|row| { - if row.row_id >= low - && row.row_id <= high - && !data.is_row_tombstoned(row.row_id) - { - Some(row.row_id) - } else { - None - } - }) - .collect(), - Self::Base(source) => source.row_ids_in_range(low, high), + match row_source.row_by_id(row_id) { + Ok(Some(_)) => count += 1, + Ok(None) => {} + Err(err) => error = Some(err), } + }); + if let Some(error) = error { + return Err(error); } + Ok(count) +} - fn projected_values_by_id( - &self, - row_id: i64, - projection_indexes: &[usize], - ) -> Result>> { - match self { - Self::Temp(data) => data.projected_values_by_id(row_id, projection_indexes), - Self::Base(source) => source.projected_values_by_id(row_id, projection_indexes), +#[derive(Clone, Debug)] +pub(crate) struct RuntimeCoveringPayloads { + columns: Vec, + rows: BTreeMap>, +} + +impl RuntimeCoveringPayloads { + fn new(columns: Vec) -> Self { + Self { + columns, + rows: BTreeMap::new(), } } - fn projected_query_row_by_id( - &self, - row_id: i64, - projection_indexes: &[usize], - ) -> Result> { - match self { - Self::Temp(data) => data.projected_query_row_by_id(row_id, projection_indexes), - Self::Base(source) => source.projected_query_row_by_id(row_id, projection_indexes), - } - } - - fn full_query_row_by_id(&self, row_id: i64) -> Result> { - match self { - Self::Temp(data) => Ok(data.full_query_row_by_id(row_id)), - Self::Base(source) => source.full_query_row_by_id(row_id), - } + fn column_position(&self, column_name: &str) -> Option { + self.columns + .iter() + .position(|candidate| identifiers_equal(candidate, column_name)) } - fn projected_query_rows_in_id_range( - &self, - low: i64, - high_exclusive: i64, - limit: usize, - offset: usize, - projection_indexes: &[usize], - ) -> Option> { - match self { - Self::Temp(data) => data.projected_query_rows_in_id_range( - low, - high_exclusive, - limit, - offset, - projection_indexes, - ), - Self::Base(source) => source.projected_query_rows_in_id_range( - low, - high_exclusive, - limit, - offset, - projection_indexes, - ), - } + fn insert_row_values(&mut self, row_id: i64, values: Vec) { + self.rows.insert(row_id, values); } - fn row_at_position(&self, position: usize) -> Result>> { - match self { - Self::Temp(data) => Ok(data.visible_rows().nth(position).map(TableRowRef::Resident)), - Self::Base(source) => source.row_at_position(position), - } + fn remove_row_id(&mut self, row_id: i64) { + self.rows.remove(&row_id); } - fn visit_int64_column_values(&self, column_index: usize, visitor: F) -> Result<()> - where - F: FnMut(i64, Option) -> Result<()>, - { - match self { - Self::Temp(data) => data.visit_int64_column_values(column_index, visitor), - Self::Base(source) => source.visit_int64_column_values(column_index, visitor), + fn shrink_to_fit(&mut self) -> usize { + let mut freed = 0usize; + let old_columns_capacity = self.columns.capacity(); + self.columns.shrink_to_fit(); + freed = freed.saturating_add( + old_columns_capacity + .saturating_sub(self.columns.capacity()) + .saturating_mul(std::mem::size_of::()), + ); + for values in self.rows.values_mut() { + let old_capacity = values.capacity(); + values.shrink_to_fit(); + freed = freed.saturating_add( + old_capacity + .saturating_sub(values.capacity()) + .saturating_mul(std::mem::size_of::()), + ); } + freed } - fn visit_float64_column_values(&self, column_index: usize, visitor: F) -> Result<()> - where - F: FnMut(i64, Option) -> Result<()>, - { - match self { - Self::Temp(data) => data.visit_float64_column_values(column_index, visitor), - Self::Base(source) => source.visit_float64_column_values(column_index, visitor), + fn project_row(&self, row_id: i64, offsets: &[usize]) -> Option { + let values = self.rows.get(&row_id)?; + let mut projected = Vec::with_capacity(offsets.len()); + for offset in offsets { + projected.push(values.get(*offset)?.clone()); } + Some(QueryRow::new(projected)) } } -#[derive(Clone, Debug, PartialEq)] -pub(crate) enum TableRowSource { - Resident(Arc), - Paged(Arc), +#[derive(Clone, Debug)] +pub(crate) enum RuntimeIndex { + Btree { + keys: RuntimeBtreeKeys, + covering: Option, + }, + Trigram { + index: TrigramIndex, + }, + Spatial { + index: SpatialRuntimeIndex, + }, + FullText { + index: FullTextIndex, + }, } -impl TableRowSource { - pub(crate) fn rows(&self) -> TableRowIter<'_> { +impl RuntimeIndex { + fn shrink_to_fit_if_unique(&mut self) -> usize { match self { - Self::Resident(data) => TableRowIter::Resident(TableDataRowIter::new(data)), - Self::Paged(manifest) => TableRowIter::Paged(manifest.rows()), + Self::Btree { keys, covering } => keys.shrink_to_fit_if_unique().saturating_add( + covering + .as_mut() + .map_or(0, RuntimeCoveringPayloads::shrink_to_fit), + ), + Self::Trigram { .. } | Self::Spatial { .. } | Self::FullText { .. } => 0, } } +} - pub(crate) fn resident_data(&self) -> &TableData { - match self { - Self::Resident(data) => data.as_ref(), - // Invariant: this method is only called for resident row sources. - Self::Paged(_) => unreachable!("paged row sources are not resident table data"), - } +fn runtime_index_entry_count(index: &RuntimeIndex) -> usize { + match index { + RuntimeIndex::Btree { keys, .. } => keys.total_row_id_count(), + RuntimeIndex::Trigram { index } => index.entry_count(), + RuntimeIndex::Spatial { index } => index.len(), + RuntimeIndex::FullText { index } => index.entry_count(), } +} - fn resident_data_mut(&mut self) -> &mut TableData { - match self { - Self::Resident(data) => Arc::make_mut(data), - // Invariant: this method is only called for mutable resident row sources. - Self::Paged(_) => unreachable!("paged row sources are not mutable resident table data"), - } - } +#[derive(Debug)] +pub(super) enum PendingIndexInsert { + Btree { + name: String, + key: RuntimeBtreeKey, + row_id: i64, + covering_values: Option>, + }, + Trigram { + name: String, + row_id: u64, + text: String, + }, + Spatial { + name: String, + row_id: i64, + value: SpatialValue, + }, + FullText { + name: String, + row_id: u64, + fields: Vec>, + }, +} - pub(crate) fn row_count(&self) -> usize { - match self { - Self::Resident(data) => data.row_count(), - Self::Paged(manifest) => manifest.row_count(), - } - } +#[derive(Debug)] +pub(crate) struct EngineRuntime { + pub(crate) catalog: Arc, + pub(crate) tables: Arc>, + pub(crate) temp_tables: Arc>, + pub(crate) temp_table_data: Arc>>, + pub(crate) temp_views: Arc>, + pub(crate) temp_indexes: Arc>, + pub(crate) temp_schema_cookie: u32, + pub(crate) indexes: Arc>>, + pub(crate) persisted_tables: Arc>, + deferred_paged_row_locator_caches: Arc>>, + view_query_cache: Arc>, + resident_tombstone_locators: Arc>>>, + /// Tables whose row data has not yet been loaded from storage. + /// Populated during `decode_manifest_payload` and cleared by + /// `load_deferred_tables`. + pub(crate) deferred_tables: Arc>, + pub(crate) dirty_tables: Arc>, + pub(crate) paged_mutations: BTreeMap, + /// Per-session cache; capped at `cached_payloads_max_entries`. Eviction is LRU. + payload_cache: Arc>, + root_state: Option, + pub(crate) index_state_epoch: u64, + pub(crate) paged_row_storage: bool, + manifest_template: Option, + overflow_chain_caches: BTreeMap, + manifest_chain_cache: Option, + sync_capture_active: bool, + pub(crate) sync_mutations: Vec, + reactive_capture_active: bool, + pub(crate) reactive_mutations: Vec, + pub(crate) extension_trust_anchors: Arc>, + pub(crate) extension_unsigned_development_mode: bool, + pub(crate) audit_context: Arc>, + pub(crate) tracing: Option>, + fts_eval_context: Arc>, +} - pub(crate) fn has_tombstoned_rows(&self) -> bool { - match self { - Self::Resident(data) => data.has_tombstoned_rows(), - Self::Paged(manifest) => !manifest.tombstoned_row_ids.is_empty(), - } - } +#[derive(Clone, Debug, Default)] +struct FtsEvalContext { + scores: BTreeMap<(String, i64), f64>, +} - pub(crate) fn row_by_id(&self, row_id: i64) -> Result>> { - match self { - Self::Resident(data) => Ok(data.row_by_id(row_id).map(TableRowRef::Resident)), - Self::Paged(manifest) => manifest.row_by_id(row_id), - } - } +#[derive(Debug)] +pub(crate) struct PayloadCache { + entries: HashMap, + next_touch_gen: u64, + max_entries: usize, +} - pub(crate) fn row_ids_in_range(&self, low: i64, high: i64) -> Vec { - if low > high { - return Vec::new(); - } - match self { - Self::Resident(data) => data.row_ids_in_range(low, high), - Self::Paged(manifest) => manifest.row_ids_in_range(low, high), - } - } +#[derive(Debug)] +struct PayloadCacheEntry { + payload: Arc>, + last_touch_gen: u64, +} - fn projected_values_by_id( - &self, - row_id: i64, - projection_indexes: &[usize], - ) -> Result>> { - match self { - Self::Resident(data) => data.projected_values_by_id(row_id, projection_indexes), - Self::Paged(manifest) => manifest.projected_values_by_id(row_id, projection_indexes), +impl PayloadCache { + fn new(max_entries: usize) -> Self { + Self { + entries: HashMap::new(), + next_touch_gen: 0, + max_entries, } } - fn projected_query_row_by_id( - &self, - row_id: i64, - projection_indexes: &[usize], - ) -> Result> { - match self { - Self::Resident(data) => data.projected_query_row_by_id(row_id, projection_indexes), - Self::Paged(manifest) => manifest - .projected_values_by_id(row_id, projection_indexes) - .map(|values| values.map(QueryRow::new)), - } + fn set_max_entries(&mut self, max_entries: usize) { + self.max_entries = max_entries; + self.evict_excess(); } - fn full_query_row_by_id(&self, row_id: i64) -> Result> { - match self { - Self::Resident(data) => Ok(data.full_query_row_by_id(row_id)), - Self::Paged(manifest) => manifest.full_query_row_by_id(row_id), - } + fn get(&mut self, table_name: &str) -> Option>> { + let payload = Arc::clone(&self.entries.get(table_name)?.payload); + self.touch(table_name); + Some(payload) } - fn projected_query_rows_in_id_range( - &self, - low: i64, - high_exclusive: i64, - limit: usize, - offset: usize, - projection_indexes: &[usize], - ) -> Option> { - match self { - Self::Resident(data) => data.projected_query_rows_in_id_range( - low, - high_exclusive, - limit, - offset, - projection_indexes, - ), - Self::Paged(_) => None, - } + fn take(&mut self, table_name: &str) -> Option>> { + self.entries.remove(table_name).map(|entry| entry.payload) } - fn row_at_position(&self, position: usize) -> Result>> { - match self { - Self::Resident(data) => { - Ok(data.visible_rows().nth(position).map(TableRowRef::Resident)) - } - Self::Paged(manifest) => manifest.row_at_position(position), + fn insert(&mut self, table_name: String, payload: Arc>) { + if self.max_entries == 0 { + return; } + let last_touch_gen = self.advance_touch_gen(); + self.entries.insert( + table_name, + PayloadCacheEntry { + payload, + last_touch_gen, + }, + ); + self.evict_excess(); } - fn visit_int64_column_values(&self, column_index: usize, visitor: F) -> Result<()> - where - F: FnMut(i64, Option) -> Result<()>, - { - match self { - Self::Resident(data) => data.visit_int64_column_values(column_index, visitor), - Self::Paged(manifest) => manifest.visit_int64_column_values(column_index, visitor), - } + fn remove(&mut self, table_name: &str) { + self.entries.remove(table_name); } - fn visit_float64_column_values(&self, column_index: usize, visitor: F) -> Result<()> - where - F: FnMut(i64, Option) -> Result<()>, - { - match self { - Self::Resident(data) => data.visit_float64_column_values(column_index, visitor), - Self::Paged(manifest) => manifest.visit_float64_column_values(column_index, visitor), + fn touch(&mut self, table_name: &str) { + let last_touch_gen = self.advance_touch_gen(); + if let Some(entry) = self.entries.get_mut(table_name) { + entry.last_touch_gen = last_touch_gen; } } - fn approximate_heap_bytes(&self) -> usize { - match self { - Self::Resident(data) => data.approximate_heap_bytes(), - Self::Paged(manifest) => manifest.approximate_heap_bytes(), + fn evict_excess(&mut self) { + while self.entries.len() > self.max_entries { + if let Some(evicted) = self.oldest_key().cloned() { + self.entries.remove(&evicted); + } else { + break; + } } } - fn shrink_resident_to_fit_if_unique(&mut self) -> usize { - match self { - Self::Resident(data) => Arc::get_mut(data) - .map(TableData::shrink_to_fit_if_unique) - .unwrap_or(0), - Self::Paged(_) => 0, - } + fn advance_touch_gen(&mut self) -> u64 { + let touch_gen = self.next_touch_gen; + self.next_touch_gen = self.next_touch_gen.wrapping_add(1); + touch_gen } - fn paged_manifest(&self) -> Option<&TablePageManifest> { - match self { - Self::Resident(_) => None, - Self::Paged(manifest) => Some(manifest), - } + fn oldest_key(&self) -> Option<&String> { + self.entries + .iter() + .min_by(|(_, left), (_, right)| { + if touch_gen_older(left.last_touch_gen, right.last_touch_gen) { + std::cmp::Ordering::Less + } else if touch_gen_older(right.last_touch_gen, left.last_touch_gen) { + std::cmp::Ordering::Greater + } else { + std::cmp::Ordering::Equal + } + }) + .map(|(key, _)| key) } -} -impl From for TableRowSource { - fn from(data: TableData) -> Self { - Self::Resident(Arc::new(data)) + #[cfg(test)] + fn len(&self) -> usize { + self.entries.len() } -} -impl From for TableRowSource { - fn from(manifest: TablePageManifest) -> Self { - Self::Paged(Arc::new(manifest)) + #[cfg(test)] + fn contains_key(&self, table_name: &str) -> bool { + self.entries.contains_key(table_name) + } + + #[cfg(test)] + fn last_touch_gen(&self, table_name: &str) -> Option { + self.entries + .get(table_name) + .map(|entry| entry.last_touch_gen) } } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) struct PersistedTableState { - pub(crate) pointer: OverflowPointer, - pub(crate) checksum: u32, - pub(crate) row_count: usize, - pub(crate) tail: OverflowTailInfo, - pub(crate) pk_index_root: Option, +fn touch_gen_older(left: u64, right: u64) -> bool { + left != right && left.wrapping_sub(right) > (u64::MAX / 2) } -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct PersistedTableChunkState { - pub(crate) pointer: OverflowPointer, - pub(crate) checksum: u32, - pub(crate) row_count: usize, - pub(crate) tombstoned_row_ids: Vec, - pub(crate) overlay_pointer: Option, - pub(crate) overlay_checksum: Option, +#[derive(Clone, Debug, PartialEq, Default)] +pub(crate) struct PagedMutationDelta { + pub(crate) append_count: usize, + pub(crate) updated_rows: BTreeMap>, + pub(crate) deleted_rows: BTreeSet, + pub(crate) original_rows: BTreeMap>, } -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct PersistedPagedTableManifest { - pub(crate) chunks: Vec, +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct BulkLoadOptions { + pub batch_size: usize, + pub sync_interval: usize, + pub disable_indexes: bool, + pub checkpoint_on_complete: bool, } -impl Default for PersistedTableState { +impl Default for BulkLoadOptions { fn default() -> Self { Self { - pointer: OverflowPointer { - head_page_id: 0, - logical_len: 0, - flags: 0, - }, - checksum: 0, - row_count: 0, - tail: OverflowTailInfo::default(), - pk_index_root: None, + batch_size: 1_000, + sync_interval: 1_000, + disable_indexes: false, + checkpoint_on_complete: true, } } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum RuntimeBtreeKey { - Encoded(RuntimeEncodedKey), - Int64(i64), - Uuid([u8; 16]), -} - -#[derive(Default)] -pub(crate) struct Int64IdentityHasher(u64); - -impl Hasher for Int64IdentityHasher { - fn finish(&self) -> u64 { - self.0 - } - - fn write(&mut self, bytes: &[u8]) { - // Runtime INT64 index keys are hashed via write_i64/write_u64. This fallback - // preserves determinism for any incidental byte-oriented hashing. - let mut hash = 0_u64; - for (shift, byte) in bytes.iter().copied().take(8).enumerate() { - hash |= u64::from(byte) << (shift * 8); +impl Clone for EngineRuntime { + fn clone(&self) -> Self { + Self { + catalog: Arc::clone(&self.catalog), + tables: Arc::clone(&self.tables), + temp_tables: Arc::clone(&self.temp_tables), + temp_table_data: Arc::clone(&self.temp_table_data), + temp_views: Arc::clone(&self.temp_views), + temp_indexes: Arc::clone(&self.temp_indexes), + temp_schema_cookie: self.temp_schema_cookie, + indexes: Arc::clone(&self.indexes), + persisted_tables: Arc::clone(&self.persisted_tables), + deferred_paged_row_locator_caches: Arc::clone(&self.deferred_paged_row_locator_caches), + view_query_cache: Arc::clone(&self.view_query_cache), + resident_tombstone_locators: Arc::clone(&self.resident_tombstone_locators), + deferred_tables: Arc::clone(&self.deferred_tables), + // Preserve dirty state so that multi-statement transactions + // (clone-and-replace) do not lose modifications from earlier + // statements. `persist_to_db` clears dirty state after a + // successful persist, so autocommit paths are unaffected. + dirty_tables: Arc::clone(&self.dirty_tables), + // Escalate paged_mutations to full dirty on clone: the + // subsequent generic execution path may modify the same rows + // in ways that invalidate the splice assumption. + paged_mutations: BTreeMap::new(), + payload_cache: Arc::clone(&self.payload_cache), + root_state: self.root_state, + index_state_epoch: self.index_state_epoch, + paged_row_storage: self.paged_row_storage, + // These caches are keyed by persisted overflow pointers and remain + // valid across clone-and-replace write transactions until the + // corresponding table is rewritten. + manifest_template: None, + overflow_chain_caches: self.overflow_chain_caches.clone(), + manifest_chain_cache: None, + sync_capture_active: self.sync_capture_active, + sync_mutations: self.sync_mutations.clone(), + reactive_capture_active: self.reactive_capture_active, + reactive_mutations: self.reactive_mutations.clone(), + extension_trust_anchors: Arc::clone(&self.extension_trust_anchors), + extension_unsigned_development_mode: self.extension_unsigned_development_mode, + audit_context: Arc::clone(&self.audit_context), + tracing: self.tracing.as_ref().map(Arc::clone), + fts_eval_context: Arc::clone(&self.fts_eval_context), } - self.0 = hash; } +} - fn write_i64(&mut self, value: i64) { - self.0 = value as u64; - } +pub(crate) struct SimpleRowIdProjectionRequest<'a> { + pub(crate) table_name: &'a str, + pub(crate) projection_columns: &'a [&'a str], + pub(crate) filter_column: &'a str, + pub(crate) lookup_row_id: i64, + pub(crate) pager: &'a PagerHandle, + pub(crate) wal: &'a WalHandle, + pub(crate) snapshot_lsn: u64, + pub(crate) use_persistent_pk_index: bool, +} - fn write_u64(&mut self, value: u64) { - self.0 = value; - } +pub(crate) struct ResolvedSimpleRowIdProjectionRequest<'a> { + pub(crate) table_name: &'a str, + pub(crate) projection_indexes: &'a [usize], + pub(crate) column_names: Arc<[String]>, + pub(crate) lookup_row_id: i64, + pub(crate) pager: &'a PagerHandle, + pub(crate) wal: &'a WalHandle, + pub(crate) snapshot_lsn: u64, + pub(crate) use_persistent_pk_index: bool, } -type Int64HashBuilder = BuildHasherDefault; -type Int64Map = HashMap; +pub(crate) struct ResolvedSimpleOrderedRowIdProjectionRequest<'a> { + pub(crate) table_name: &'a str, + pub(crate) order_column: &'a str, + pub(crate) projection_indexes: &'a [usize], + pub(crate) column_names: Arc<[String]>, + pub(crate) limit: Option, + pub(crate) offset: usize, + pub(crate) descending: bool, +} -/// Runtime storage for a unique typed `INT64` index. -/// -/// Integer primary keys commonly map a contiguous key range to identical row -/// IDs. Keeping that relation as a range avoids allocating and populating a -/// hash-map entry for every row while retaining a conservative sparse fallback -/// for every other unique integer index shape (ADR 0203). -#[derive(Clone, Debug)] -pub(crate) enum UniqueInt64Keys { - DenseIdentity { start: i64, len: usize }, - Sparse(Int64Map), +pub(crate) struct ResolvedSimpleRowIdRangeProjectionRequest<'a> { + pub(crate) table_name: &'a str, + pub(crate) projection_indexes: &'a [usize], + pub(crate) column_names: Arc<[String]>, + pub(crate) filter_column: &'a str, + pub(crate) lower_bound: Option, + pub(crate) upper_bound: Option, + pub(crate) limit: Option, + pub(crate) pager: &'a PagerHandle, + pub(crate) wal: &'a WalHandle, + pub(crate) snapshot_lsn: u64, + pub(crate) use_persistent_pk_index: bool, } -impl Default for UniqueInt64Keys { - fn default() -> Self { - Self::new() - } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum SimpleJoinProjectionSide { + Left, + Right, } -impl From> for UniqueInt64Keys { - fn from(keys: Int64Map) -> Self { - Self::Sparse(keys) - } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct ResolvedSimpleJoinProjection { + pub(crate) side: SimpleJoinProjectionSide, + pub(crate) index: usize, } -impl UniqueInt64Keys { - fn new() -> Self { - Self::DenseIdentity { start: 0, len: 0 } - } +pub(crate) struct ResolvedSimpleRowIdJoinProjectionRequest<'a> { + pub(crate) left_table_name: &'a str, + pub(crate) right_table_name: &'a str, + pub(crate) left_projection_indexes: &'a [usize], + pub(crate) right_projection_indexes: &'a [usize], + pub(crate) projections: &'a [ResolvedSimpleJoinProjection], + pub(crate) column_names: Arc<[String]>, + pub(crate) lookup_row_id: i64, + pub(crate) pager: &'a PagerHandle, + pub(crate) wal: &'a WalHandle, + pub(crate) snapshot_lsn: u64, + pub(crate) use_persistent_pk_index: bool, +} - fn dense_value_at(start: i64, offset: usize) -> Option { - let offset = i128::try_from(offset).ok()?; - i64::try_from(i128::from(start) + offset).ok() - } +struct ValidatedSimpleRowIdProjectionRequest<'a> { + table_schema: &'a TableSchema, + projection_indexes: &'a [usize], + column_names: Arc<[String]>, + lookup_row_id: i64, + pager: &'a PagerHandle, + wal: &'a WalHandle, + snapshot_lsn: u64, + use_persistent_pk_index: bool, +} - fn dense_contains(start: i64, len: usize, key: i64) -> bool { - let offset = i128::from(key) - i128::from(start); - offset >= 0 && usize::try_from(offset).is_ok_and(|offset| offset < len) +impl EngineRuntime { + #[must_use] + pub(crate) fn empty(schema_cookie: u32) -> Self { + let config = crate::config::DbConfig { + paged_row_storage: false, + ..crate::config::DbConfig::default() + }; + Self::from_config(schema_cookie, &config) } - fn len(&self) -> usize { - match self { - Self::DenseIdentity { len, .. } => *len, - Self::Sparse(keys) => keys.len(), + #[must_use] + pub(crate) fn from_config(schema_cookie: u32, config: &crate::config::DbConfig) -> Self { + Self { + catalog: Arc::new(CatalogState::empty(schema_cookie)), + tables: Arc::new(BTreeMap::new()), + temp_tables: Arc::new(BTreeMap::new()), + temp_table_data: Arc::new(BTreeMap::new()), + temp_views: Arc::new(BTreeMap::new()), + temp_indexes: Arc::new(BTreeMap::new()), + temp_schema_cookie: 0, + indexes: Arc::new(BTreeMap::new()), + persisted_tables: Arc::new(BTreeMap::new()), + deferred_paged_row_locator_caches: Arc::new(BTreeMap::new()), + view_query_cache: Arc::new(Mutex::new(ViewQueryCache::default())), + resident_tombstone_locators: Arc::new(BTreeMap::new()), + deferred_tables: Arc::new(BTreeSet::new()), + dirty_tables: Arc::new(BTreeSet::new()), + paged_mutations: BTreeMap::new(), + payload_cache: Arc::new(Mutex::new(PayloadCache::new( + config.cached_payloads_max_entries, + ))), + root_state: None, + index_state_epoch: 0, + paged_row_storage: config.paged_row_storage, + manifest_template: None, + overflow_chain_caches: BTreeMap::new(), + manifest_chain_cache: None, + sync_capture_active: false, + sync_mutations: Vec::new(), + reactive_capture_active: false, + reactive_mutations: Vec::new(), + extension_trust_anchors: Arc::new(config.extension_trust_anchors.clone()), + extension_unsigned_development_mode: config.extension_unsigned_development_mode, + audit_context: Arc::new(Mutex::new(crate::security::AuditContext::default())), + tracing: None, + fts_eval_context: Arc::new(Mutex::new(FtsEvalContext::default())), } } - fn get(&self, key: &i64) -> Option { - match self { - Self::DenseIdentity { start, len } if Self::dense_contains(*start, *len, *key) => { - Some(*key) - } - Self::DenseIdentity { .. } => None, - Self::Sparse(keys) => keys.get(key).copied(), - } + pub(crate) fn set_audit_context_handle( + &mut self, + handle: Arc>, + ) { + self.audit_context = handle; } - fn iter(&self) -> UniqueInt64KeysIter<'_> { - match self { - Self::DenseIdentity { start, len } => UniqueInt64KeysIter::Dense { - start: *start, - offset: 0, - len: *len, - }, - Self::Sparse(keys) => UniqueInt64KeysIter::Sparse(keys.iter()), + pub(crate) fn set_sync_capture_active(&mut self, active: bool) { + self.sync_capture_active = active; + if !active { + self.sync_mutations.clear(); } } - fn materialize_sparse(&mut self) { - let Self::DenseIdentity { start, len } = self else { - return; - }; - let start = *start; - let len = *len; - let mut keys = HashMap::with_capacity_and_hasher(len, Int64HashBuilder::default()); - for offset in 0..len { - let Some(value) = Self::dense_value_at(start, offset) else { - debug_assert!(false, "dense INT64 identity range exceeded i64 bounds"); - break; - }; - keys.insert(value, value); + pub(crate) fn set_reactive_capture_active(&mut self, active: bool) { + self.reactive_capture_active = active; + if !active { + self.reactive_mutations.clear(); } - *self = Self::Sparse(keys); } - /// Insert a mapping and return the previous row ID, matching - /// `HashMap::insert` semantics. - fn insert(&mut self, key: i64, row_id: i64) -> Option { - match self { - Self::DenseIdentity { start, len } => { - if Self::dense_contains(*start, *len, key) { - if key == row_id { - return Some(key); - } - } else if key == row_id { - if *len == 0 { - *start = key; - *len = 1; - return None; - } - if Self::dense_value_at(*start, *len) == Some(key) { - *len = len.saturating_add(1); - return None; - } - if start.checked_sub(1) == Some(key) { - *start = key; - *len = len.saturating_add(1); - return None; - } - } - self.materialize_sparse(); - let Self::Sparse(keys) = self else { - return None; - }; - keys.insert(key, row_id) - } - Self::Sparse(keys) => keys.insert(key, row_id), - } + pub(crate) fn sync_capture_active(&self) -> bool { + self.sync_capture_active } - fn remove_row_id_mapping(&mut self, row_id: i64) { - match self { - Self::DenseIdentity { start, len } if Self::dense_contains(*start, *len, row_id) => { - if *len == 1 { - *len = 0; - } else if *start == row_id { - *start = start.saturating_add(1); - *len -= 1; - } else if Self::dense_value_at(*start, len.saturating_sub(1)) == Some(row_id) { - *len -= 1; - } else { - self.materialize_sparse(); - if let Self::Sparse(keys) = self { - keys.remove(&row_id); - } - } + pub(crate) fn set_tracing(&mut self, tracing: Arc) { + self.tracing = Some(tracing); + } + + #[allow(dead_code)] + pub(crate) fn drain_index_usage(&self) { + let events = crate::tracing::index_usage::drain_local_index_usage(); + if events.is_empty() { + return; + } + if let Some(tracing) = self.tracing.as_ref() { + for (table_name, index_name, index_kind, kind) in events { + tracing.record_index_usage(&table_name, &index_name, &index_kind, kind); } - Self::DenseIdentity { .. } => {} - Self::Sparse(keys) => keys.retain(|_, existing| *existing != row_id), } } - fn shrink_to_fit(&mut self) -> usize { - let Self::Sparse(keys) = self else { - return 0; - }; - let old_capacity = keys.capacity(); - keys.shrink_to_fit(); - old_capacity - .saturating_sub(keys.capacity()) - .saturating_mul(std::mem::size_of::<(i64, i64)>()) + pub(crate) fn mutation_capture_active(&self) -> bool { + self.sync_capture_active || self.reactive_capture_active } - #[cfg(test)] - pub(crate) fn is_dense_identity(&self) -> bool { - matches!(self, Self::DenseIdentity { .. }) + pub(crate) fn should_record_sync_mutation_for_table(&self, table: &TableSchema) -> bool { + (self.sync_capture_active || self.reactive_capture_active) + && !table.temporary + && !crate::sync::is_internal_table_name(&table.name) } -} -enum UniqueInt64KeysIter<'a> { - Dense { - start: i64, - offset: usize, - len: usize, - }, - Sparse(std::collections::hash_map::Iter<'a, i64, i64>), -} + pub(crate) fn record_sync_mutation( + &mut self, + table_name: &str, + operation: crate::sync::SyncOperation, + primary_key: serde_json::Value, + after: Option, + schema_cookie: u32, + ) { + if self.sync_capture_active { + self.sync_mutations.push(crate::sync::SyncMutation { + table: table_name.to_string(), + operation, + primary_key: primary_key.clone(), + after: after.clone(), + schema_cookie, + }); + } + if self.reactive_capture_active { + self.reactive_mutations + .push(crate::reactive::RowChange::new( + table_name.to_string(), + crate::reactive::row_operation_from_sync(operation), + primary_key, + None, + after, + )); + } + } -impl Iterator for UniqueInt64KeysIter<'_> { - type Item = (i64, i64); + pub(crate) fn take_sync_mutations(&mut self) -> Vec { + std::mem::take(&mut self.sync_mutations) + } - fn next(&mut self) -> Option { - match self { - Self::Dense { start, offset, len } => { - if *offset >= *len { - return None; - } - let value = UniqueInt64Keys::dense_value_at(*start, *offset)?; - *offset += 1; - Some((value, value)) - } - Self::Sparse(iter) => iter.next().map(|(key, row_id)| (*key, *row_id)), + pub(crate) fn take_reactive_mutations(&mut self) -> Vec { + std::mem::take(&mut self.reactive_mutations) + } + + pub(super) fn bump_temp_schema_cookie(&mut self) { + self.temp_schema_cookie = self.temp_schema_cookie.wrapping_add(1); + if self.temp_schema_cookie == 0 { + self.temp_schema_cookie = 1; } } - fn size_hint(&self) -> (usize, Option) { - match self { - Self::Dense { offset, len, .. } => { - let remaining = len.saturating_sub(*offset); - (remaining, Some(remaining)) + fn persistent_resolution_runtime(&self) -> Self { + let mut runtime = self.clone(); + runtime.temp_tables_mut().clear(); + runtime.temp_table_data_map_mut().clear(); + runtime.temp_views_mut().clear(); + runtime.temp_indexes_mut().clear(); + runtime.temp_schema_cookie = 0; + runtime + } + + fn cached_view_query(&self, view: &ViewSchema) -> Result> { + let key = ViewQueryCacheKey::new(view); + { + let cache = self + .view_query_cache + .lock() + .expect("view query cache lock should not be poisoned"); + if let Some(query) = cache.get(&key) { + return Ok(query); } - Self::Sparse(iter) => iter.size_hint(), } + + let view_statement = parse_sql_statement(&view.sql_text)?; + let Statement::Query(query) = view_statement else { + return Err(DbError::corruption(format!( + "view {} does not contain a SELECT statement", + view.name + ))); + }; + let query = Arc::new(query); + let mut cache = self + .view_query_cache + .lock() + .expect("view query cache lock should not be poisoned"); + Ok(cache.insert(key, query)) } -} -impl ExactSizeIterator for UniqueInt64KeysIter<'_> {} + fn cache_view_query(&self, view: &ViewSchema, query: Query) { + self.view_query_cache + .lock() + .expect("view query cache lock should not be poisoned") + .insert(ViewQueryCacheKey::new(view), Arc::new(query)); + } -/// Row IDs stored beneath one key in a non-unique typed `INT64` index. -/// -/// Foreign-key-like indexes commonly receive monotonically increasing row IDs -/// grouped by key. Representing those postings as an inline singleton or a -/// contiguous range avoids one heap allocation per distinct key. Irregular -/// insertion order falls back to the same `Vec` semantics used previously. -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum RuntimeInt64RowIds { - One(i64), - Contiguous { start: i64, len: usize }, - Many(Vec), -} + fn catalog_mut(&mut self) -> &mut CatalogState { + Arc::make_mut(&mut self.catalog) + } -impl RuntimeInt64RowIds { - fn one(row_id: i64) -> Self { - Self::One(row_id) + fn tables_mut(&mut self) -> &mut BTreeMap { + Arc::make_mut(&mut self.tables) } - fn len(&self) -> usize { - match self { - Self::One(_) => 1, - Self::Contiguous { len, .. } => *len, - Self::Many(row_ids) => row_ids.len(), - } + fn temp_table_data_map_mut(&mut self) -> &mut BTreeMap> { + Arc::make_mut(&mut self.temp_table_data) } - fn is_empty(&self) -> bool { - matches!(self, Self::Many(row_ids) if row_ids.is_empty()) + /// Per-entry COW: locates `name` in `tables` (case-insensitive) and + /// returns a `&mut TableData` for the targeted table, cloning only that + /// table's row vector when the per-entry `Arc` is shared. + /// This avoids deep-cloning every other table's rows during writes. + fn entry_table_data_mut(&mut self, name: &str) -> Option<&mut TableData> { + let canonical = map_key_ci(self.tables.as_ref(), name)?; + let map = self.tables_mut(); + let entry = map.get_mut(&canonical)?; + Some(entry.resident_data_mut()) } - fn value_at(start: i64, offset: usize) -> Option { - let offset = i128::try_from(offset).ok()?; - i64::try_from(i128::from(start) + offset).ok() + fn entry_table_row_source_mut(&mut self, name: &str) -> Option<&mut TableRowSource> { + let canonical = map_key_ci(self.tables.as_ref(), name)?; + self.tables_mut().get_mut(&canonical) } - fn from_vec(row_ids: Vec) -> Self { - match row_ids.as_slice() { - [] => Self::Many(row_ids), - [row_id] => Self::One(*row_id), - [start, rest @ ..] - if rest.iter().copied().enumerate().all(|(offset, row_id)| { - Self::value_at(*start, offset.saturating_add(1)) == Some(row_id) - }) => - { - Self::Contiguous { - start: *start, - len: row_ids.len(), - } - } - _ => Self::Many(row_ids), - } + fn entry_temp_table_data_mut(&mut self, name: &str) -> Option<&mut TableData> { + let canonical = map_key_ci(self.temp_table_data.as_ref(), name)?; + let map = self.temp_table_data_map_mut(); + let entry = map.get_mut(&canonical)?; + Some(Arc::make_mut(entry)) } - fn push(&mut self, row_id: i64) { - match self { - Self::One(first_row_id) if first_row_id.checked_add(1) == Some(row_id) => { - *self = Self::Contiguous { - start: *first_row_id, - len: 2, - }; - } - Self::One(first_row_id) => { - let mut row_ids = Vec::with_capacity(4); - row_ids.push(*first_row_id); - row_ids.push(row_id); - *self = Self::Many(row_ids); - } - Self::Contiguous { start, len } if Self::value_at(*start, *len) == Some(row_id) => { - *len = len.saturating_add(1); - } - Self::Contiguous { start, len } => { - let start = *start; - let len = *len; - let mut row_ids = Vec::with_capacity(len.saturating_add(1)); - for offset in 0..len { - if let Some(existing) = Self::value_at(start, offset) { - row_ids.push(existing); - } - } - row_ids.push(row_id); - *self = Self::Many(row_ids); - } - Self::Many(row_ids) => row_ids.push(row_id), - } - } - - fn contains(&self, row_id: &i64) -> bool { - match self { - Self::One(existing) => existing == row_id, - Self::Contiguous { start, len } => { - UniqueInt64Keys::dense_contains(*start, *len, *row_id) - } - Self::Many(row_ids) => row_ids.contains(row_id), - } + fn temp_tables_mut(&mut self) -> &mut BTreeMap { + Arc::make_mut(&mut self.temp_tables) } - fn iter(&self) -> RuntimeInt64RowIdsIter<'_> { - match self { - Self::One(row_id) => RuntimeInt64RowIdsIter::One(Some(*row_id)), - Self::Contiguous { start, len } => RuntimeInt64RowIdsIter::Contiguous { - start: *start, - offset: 0, - len: *len, - }, - Self::Many(row_ids) => RuntimeInt64RowIdsIter::Many(row_ids.iter()), - } + fn temp_views_mut(&mut self) -> &mut BTreeMap { + Arc::make_mut(&mut self.temp_views) } - fn to_vec(&self) -> Vec { - self.iter().collect() + fn temp_indexes_mut(&mut self) -> &mut BTreeMap { + Arc::make_mut(&mut self.temp_indexes) } - fn retain(&mut self, mut retain: impl FnMut(&i64) -> bool) { - match self { - Self::One(row_id) => { - if !retain(row_id) { - *self = Self::Many(Vec::new()); - } - } - Self::Contiguous { .. } => { - let retained = self - .iter() - .filter(|row_id| retain(row_id)) - .collect::>(); - *self = Self::from_vec(retained); - } - Self::Many(row_ids) => row_ids.retain(retain), - } + fn indexes_mut(&mut self) -> &mut BTreeMap> { + Arc::make_mut(&mut self.indexes) } - fn shrink_to_fit(&mut self) -> usize { - let Self::Many(row_ids) = self else { - return 0; - }; - let old_capacity = row_ids.capacity(); - let compact = Self::from_vec(std::mem::take(row_ids)); - if matches!(compact, Self::Many(_)) { - *self = compact; - let Self::Many(row_ids) = self else { - return 0; - }; - row_ids.shrink_to_fit(); - return old_capacity - .saturating_sub(row_ids.capacity()) - .saturating_mul(std::mem::size_of::()); - } - *self = compact; - old_capacity.saturating_mul(std::mem::size_of::()) + fn persisted_tables_mut(&mut self) -> &mut BTreeMap { + Arc::make_mut(&mut self.persisted_tables) } -} - -pub(crate) enum RuntimeInt64RowIdsIter<'a> { - One(Option), - Contiguous { - start: i64, - offset: usize, - len: usize, - }, - Many(std::slice::Iter<'a, i64>), -} -impl Iterator for RuntimeInt64RowIdsIter<'_> { - type Item = i64; - - fn next(&mut self) -> Option { - match self { - Self::One(row_id) => row_id.take(), - Self::Contiguous { start, offset, len } => { - if *offset >= *len { - return None; - } - let row_id = RuntimeInt64RowIds::value_at(*start, *offset)?; - *offset += 1; - Some(row_id) - } - Self::Many(row_ids) => row_ids.next().copied(), - } + fn resident_tombstone_locators_mut(&mut self) -> &mut BTreeMap>> { + Arc::make_mut(&mut self.resident_tombstone_locators) } - fn size_hint(&self) -> (usize, Option) { - let remaining = match self { - Self::One(row_id) => usize::from(row_id.is_some()), - Self::Contiguous { offset, len, .. } => len.saturating_sub(*offset), - Self::Many(row_ids) => row_ids.len(), - }; - (remaining, Some(remaining)) + fn dirty_tables_mut(&mut self) -> &mut BTreeSet { + Arc::make_mut(&mut self.dirty_tables) } -} - -impl ExactSizeIterator for RuntimeInt64RowIdsIter<'_> {} -pub(crate) fn contiguous_row_ids(start: i64, len: usize) -> RuntimeInt64RowIdsIter<'static> { - RuntimeInt64RowIdsIter::Contiguous { - start, - offset: 0, - len, + /// Read-only access to a runtime index by name (case-sensitive). + pub(crate) fn index(&self, name: &str) -> Option<&RuntimeIndex> { + self.indexes.get(name).map(|arc| arc.as_ref()) } -} - -impl<'a> IntoIterator for &'a RuntimeInt64RowIds { - type Item = i64; - type IntoIter = RuntimeInt64RowIdsIter<'a>; - fn into_iter(self) -> Self::IntoIter { - self.iter() + /// Targeted copy-on-write access to a single runtime index entry. + /// + /// Performs `Arc::make_mut` only on the targeted entry (and on the outer + /// map), so unrelated indexes are not cloned even when the runtime is + /// shared with concurrent readers. + pub(crate) fn index_mut(&mut self, name: &str) -> Option<&mut RuntimeIndex> { + let map = Arc::make_mut(&mut self.indexes); + map.get_mut(name).map(Arc::make_mut) } -} - -/// Runtime key-domain storage for a non-unique typed `INT64` index. -/// -/// Dense mode is intentionally conservative: insertions may repeat only the -/// current final key or append its immediate successor. Gaps and out-of-order -/// key insertion materialize a sparse identity-hashed map, ensuring arbitrary -/// workloads keep general hash-map behavior while grouped benchmark-shaped -/// foreign-key indexes avoid per-key hash buckets. -#[derive(Clone, Debug)] -pub(crate) enum NonUniqueInt64Keys { - Dense { - start: i64, - postings: Vec, - }, - Sparse(Int64Map), -} -impl Default for NonUniqueInt64Keys { - fn default() -> Self { - Self::new() + fn cached_payload(&mut self, table_name: &str) -> Option>> { + self.payload_cache + .lock() + .expect("payload cache lock should not be poisoned") + .get(table_name) } -} -impl From>> for NonUniqueInt64Keys { - fn from(keys: Int64Map>) -> Self { - let mut postings = - Int64Map::with_capacity_and_hasher(keys.capacity(), Int64HashBuilder::default()); - for (key, row_ids) in keys { - postings.insert(key, RuntimeInt64RowIds::from_vec(row_ids)); - } - Self::Sparse(postings) + fn cached_payload_take(&mut self, table_name: &str) -> Option>> { + self.payload_cache + .lock() + .expect("payload cache lock should not be poisoned") + .take(table_name) } -} -impl NonUniqueInt64Keys { - fn new() -> Self { - Self::Dense { - start: 0, - postings: Vec::new(), - } + fn cache_payload_insert(&mut self, table_name: String, payload: Arc>) { + self.payload_cache + .lock() + .expect("payload cache lock should not be poisoned") + .insert(table_name, payload); } - fn len(&self) -> usize { - match self { - Self::Dense { postings, .. } => postings.len(), - Self::Sparse(keys) => keys.len(), - } + fn cache_payload_remove(&mut self, table_name: &str) { + self.payload_cache + .lock() + .expect("payload cache lock should not be poisoned") + .remove(table_name); } - fn get(&self, key: &i64) -> Option<&RuntimeInt64RowIds> { - match self { - Self::Dense { start, postings } - if UniqueInt64Keys::dense_contains(*start, postings.len(), *key) => - { - let offset = usize::try_from(i128::from(*key) - i128::from(*start)).ok()?; - postings.get(offset) - } - Self::Dense { .. } => None, - Self::Sparse(keys) => keys.get(key), - } + fn cache_deferred_paged_row_locators( + &mut self, + table_name: &str, + state: PersistedTableState, + chunks: &[TablePageManifestChunk], + ) -> Result<()> { + let cache = build_deferred_paged_row_locator_cache(state, chunks)?; + self.deferred_paged_row_locator_caches_mut() + .insert(table_name.to_string(), Arc::new(cache)); + Ok(()) } - fn get_mut(&mut self, key: &i64) -> Option<&mut RuntimeInt64RowIds> { - match self { - Self::Dense { start, postings } - if UniqueInt64Keys::dense_contains(*start, postings.len(), *key) => - { - let offset = usize::try_from(i128::from(*key) - i128::from(*start)).ok()?; - postings.get_mut(offset) - } - Self::Dense { .. } => None, - Self::Sparse(keys) => keys.get_mut(key), - } + fn should_cache_deferred_paged_row_locators(&self, table_name: &str) -> bool { + let has_runtime_btree = self.catalog.indexes.values().any(|index| { + identifiers_equal(&index.table_name, table_name) + && index.fresh + && index.kind == IndexKind::Btree + && matches!(self.index(&index.name), Some(RuntimeIndex::Btree { .. })) + }); + has_runtime_btree + || self + .catalog + .table(table_name) + .and_then(row_id_alias_column_name) + .is_some() } - fn iter(&self) -> NonUniqueInt64KeysIter<'_> { - match self { - Self::Dense { start, postings } => NonUniqueInt64KeysIter::Dense { - start: *start, - offset: 0, - postings: postings.iter(), - }, - Self::Sparse(keys) => NonUniqueInt64KeysIter::Sparse(keys.iter()), + /// Returns per-table residency stats `(table_name, row_count, heap_bytes)` + /// for every fully-loaded table in this runtime. Deferred (not-yet-loaded) + /// tables are omitted. Used by `Db::inspect_storage_state_json` per + /// ADR 0143 Phase A. Result is sorted by table name for deterministic + /// output. + #[must_use] + #[allow(dead_code)] // exposed for follow-up Phase A JSON breakdown wiring + pub(crate) fn table_memory_breakdown(&self) -> Vec<(String, usize, usize)> { + let mut out = Vec::with_capacity(self.tables.len()); + for (name, data) in self.tables.iter() { + out.push(( + name.clone(), + data.row_count(), + data.approximate_heap_bytes(), + )); } + out } - fn values(&self) -> NonUniqueInt64Values<'_> { - match self { - Self::Dense { postings, .. } => NonUniqueInt64Values::Dense(postings.iter()), - Self::Sparse(keys) => NonUniqueInt64Values::Sparse(keys.values()), + /// Returns aggregate `(total_rows, total_heap_bytes, table_count, + /// deferred_table_count)` across all loaded tables. Deferred tables + /// contribute zero to the byte/row totals but are counted separately. + /// Per ADR 0143 Phase A. + #[must_use] + pub(crate) fn table_memory_totals(&self) -> (u64, u64, u32, u32) { + let mut rows: u64 = 0; + let mut bytes: u64 = 0; + for data in self.tables.values() { + rows = rows.saturating_add(data.row_count() as u64); + bytes = bytes.saturating_add(data.approximate_heap_bytes() as u64); } + let table_count = u32::try_from(self.tables.len()).unwrap_or(u32::MAX); + let deferred_count = u32::try_from(self.deferred_tables.len()).unwrap_or(u32::MAX); + (rows, bytes, table_count, deferred_count) } - fn materialize_sparse(&mut self) { - let Self::Dense { start, postings } = self else { - return; + pub(crate) fn persist_to_db(&mut self, db: &crate::db::Db) -> Result<()> { + let old_root = self.root_state; + let schema_cookie_changed = + old_root.is_none_or(|root| root.schema_cookie != self.catalog.schema_cookie); + let dirty_tables = if self.persisted_tables.is_empty() { + self.catalog.tables.keys().cloned().collect::>() + } else { + self.dirty_tables.iter().cloned().collect::>() }; - let start = *start; - let postings = std::mem::take(postings); - let mut keys = - Int64Map::with_capacity_and_hasher(postings.len(), Int64HashBuilder::default()); - for (offset, row_ids) in postings.into_iter().enumerate() { - let Some(key) = UniqueInt64Keys::dense_value_at(start, offset) else { - debug_assert!(false, "dense non-unique INT64 range exceeded i64 bounds"); - break; - }; - keys.insert(key, row_ids); - } - *self = Self::Sparse(keys); - } + let removed_tables = self + .persisted_tables + .keys() + .filter(|table_name| self.catalog.table(table_name).is_none()) + .cloned() + .collect::>(); - fn insert_row_id(&mut self, key: i64, row_id: i64) { - match self { - Self::Dense { start, postings } => { - if postings.is_empty() { - *start = key; - postings.push(RuntimeInt64RowIds::one(row_id)); - return; - } - let last_offset = postings.len().saturating_sub(1); - if UniqueInt64Keys::dense_value_at(*start, last_offset) == Some(key) { - if let Some(posting) = postings.last_mut() { - posting.push(row_id); - } - return; - } - if UniqueInt64Keys::dense_value_at(*start, postings.len()) == Some(key) { - postings.push(RuntimeInt64RowIds::one(row_id)); - return; - } - self.materialize_sparse(); - self.insert_row_id(key, row_id); - } - Self::Sparse(keys) => match keys.entry(key) { - std::collections::hash_map::Entry::Vacant(entry) => { - entry.insert(RuntimeInt64RowIds::one(row_id)); - } - std::collections::hash_map::Entry::Occupied(mut entry) => { - entry.get_mut().push(row_id); + { + let mut store = DbTxnPageStore { db }; + for table_name in dirty_tables { + let Some(table) = self.catalog.table(&table_name) else { + continue; + }; + let canonical_table_name = table.name.clone(); + let delta = self + .paged_mutations + .get(&canonical_table_name) + .cloned() + .unwrap_or_default(); + let previous_state = self + .persisted_tables + .get(&canonical_table_name) + .copied() + .unwrap_or_default(); + let previous_pointer = previous_state.pointer; + let resident_tombstone_locators = self + .resident_tombstone_locators + .get(&canonical_table_name) + .cloned(); + let row_source = + self.tables + .get(&canonical_table_name) + .cloned() + .ok_or_else(|| { + DbError::internal(format!("table data for {table_name} is missing")) + })?; + let mut use_paged_row_storage = + db.config().paged_row_storage || previous_pointer.is_table_paged_manifest(); + if db.config().paged_row_storage && !previous_pointer.is_table_paged_manifest() { + use_paged_row_storage = match &row_source { + TableRowSource::Resident(data) => resident_table_should_use_paged_storage( + data, + previous_state, + &delta, + db.config().page_size, + )?, + TableRowSource::Paged(manifest) => { + manifest.chunks.len() > 1 + || manifest.chunks.first().is_some_and(|chunk| { + chunk.payload.len() + > paged_table_target_chunk_bytes(db.config().page_size) + }) + } + }; } - }, - } - } - - fn remove_row_id_mapping(&mut self, row_id: i64) { - match self { - Self::Dense { start, postings } => { - for posting in postings.iter_mut() { - if posting.contains(&row_id) { - posting.retain(|existing| *existing != row_id); + let resident_update_only = !delta.updated_rows.is_empty() + && delta.deleted_rows.is_empty() + && delta.append_count == 0; + let resident_delete_only = delta.updated_rows.is_empty() + && !delta.deleted_rows.is_empty() + && delta.append_count == 0; + let cached_payload = if (!use_paged_row_storage + || !previous_pointer.is_table_paged_manifest()) + && !resident_update_only + && !resident_delete_only + { + self.cached_payload(&canonical_table_name) + } else { + None + }; + if let Some(manifest) = row_source.paged_manifest() { + self.overflow_chain_caches.remove(&canonical_table_name); + if delta.append_count > 0 + && delta.updated_rows.is_empty() + && delta.deleted_rows.is_empty() + && manifest.tombstoned_row_ids.is_empty() + && !db.config().persistent_pk_index + { + if let Some((new_state, persisted_chunks)) = + try_append_only_paged_table_from_manifest( + &mut store, + previous_state, + manifest, + )? + { + self.persisted_tables_mut() + .insert(canonical_table_name.clone(), new_state); + let pk_index_root = self + .refresh_paged_lookup_cache_and_pk_index_from_chunks( + db, + &canonical_table_name, + new_state, + &persisted_chunks, + )?; + replace_table_pk_index_root( + self, + db, + &canonical_table_name, + pk_index_root, + )?; + let persisted_manifest = table_page_manifest_with_persisted_chunks( + manifest, + &persisted_chunks, + ); + self.replace_table_row_source( + &canonical_table_name, + TableRowSource::Paged(Arc::new(persisted_manifest)), + )?; + self.cache_payload_remove(&canonical_table_name); + continue; + } } - } - while postings.last().is_some_and(RuntimeInt64RowIds::is_empty) { - postings.pop(); - } - while postings.first().is_some_and(RuntimeInt64RowIds::is_empty) { - postings.remove(0); - *start = start.saturating_add(1); - } - if postings.iter().any(RuntimeInt64RowIds::is_empty) { - self.materialize_sparse(); - if let Self::Sparse(keys) = self { - keys.retain(|_, posting| !posting.is_empty()); + let (new_state, persisted_chunks) = + rewrite_paged_table_from_manifest(&mut store, previous_state, manifest)?; + self.persisted_tables_mut() + .insert(canonical_table_name.clone(), new_state); + if new_state == previous_state { + if db.config().persistent_pk_index { + replace_table_pk_index_root( + self, + db, + &canonical_table_name, + previous_state.pk_index_root, + )?; + } + let persisted_manifest = + table_page_manifest_with_persisted_chunks(manifest, &persisted_chunks); + self.replace_table_row_source( + &canonical_table_name, + TableRowSource::Paged(Arc::new(persisted_manifest)), + )?; + self.cache_payload_remove(&canonical_table_name); + continue; } + let pk_index_root = self.refresh_paged_lookup_cache_and_pk_index_from_chunks( + db, + &canonical_table_name, + new_state, + &persisted_chunks, + )?; + replace_table_pk_index_root(self, db, &canonical_table_name, pk_index_root)?; + let persisted_manifest = + table_page_manifest_with_persisted_chunks(manifest, &persisted_chunks); + self.replace_table_row_source( + &canonical_table_name, + TableRowSource::Paged(Arc::new(persisted_manifest)), + )?; + self.cache_payload_remove(&canonical_table_name); + continue; } - } - Self::Sparse(keys) => { - for posting in keys.values_mut() { - if posting.contains(&row_id) { - posting.retain(|existing| *existing != row_id); + if use_paged_row_storage && previous_pointer.is_table_paged_manifest() { + self.overflow_chain_caches.remove(&canonical_table_name); + if delta.append_count > 0 + && delta.updated_rows.is_empty() + && delta.deleted_rows.is_empty() + && !row_source.has_tombstoned_rows() + { + let data = row_source.resident_data(); + let existing_count = data.rows.len().saturating_sub(delta.append_count); + let appended_chunks = encode_paged_table_chunks_from_rows( + &data.rows[existing_count..], + db.config().page_size, + )?; + let new_state = if !appended_chunks.is_empty() { + append_paged_table_chunks( + &mut store, + previous_state, + &appended_chunks, + data.row_count(), + )? + } else { + previous_state + }; + self.persisted_tables_mut() + .insert(canonical_table_name.clone(), new_state); + let pk_index_root = self.refresh_paged_lookup_cache_and_pk_index( + db, + &store, + &canonical_table_name, + new_state, + )?; + replace_table_pk_index_root( + self, + db, + &canonical_table_name, + pk_index_root, + )?; + self.cache_payload_remove(&canonical_table_name); + continue; } } - keys.retain(|_, posting| !posting.is_empty()); - } - } - } + let data = row_source.resident_data(); + if delta.append_count > 0 + && delta.updated_rows.is_empty() + && delta.deleted_rows.is_empty() + && previous_pointer.head_page_id != 0 + && !use_paged_row_storage + && !previous_pointer.is_compressed() + && !data.has_tombstoned_rows() + { + let existing_count = data.rows.len().saturating_sub(delta.append_count); + if existing_count <= data.rows.len() { + let appended_rows = encode_appended_table_rows(data, existing_count)?; + if !appended_rows.is_empty() { + if !db.config().persistent_pk_index { + let row_count = data.row_count(); + let row_count_bytes = u32::try_from(row_count) + .map_err(|_| { + DbError::constraint("table row count exceeds u32") + })? + .to_le_bytes(); + let (ptr, checksum, new_chain_cache, tail) = + append_uncompressed_with_first_page_patch( + &mut store, + previous_pointer, + TABLE_PAYLOAD_MAGIC.len(), + &row_count_bytes, + &appended_rows, + )?; + self.overflow_chain_caches + .insert(canonical_table_name.clone(), new_chain_cache); + let row_count = data.row_count(); + self.persisted_tables_mut().insert( + canonical_table_name.clone(), + PersistedTableState { + pointer: ptr, + checksum, + row_count, + tail, + pk_index_root: previous_state.pk_index_root, + }, + ); + replace_table_pk_index_root(self, db, &canonical_table_name, None)?; + self.cache_payload_remove(&canonical_table_name); + continue; + } - fn remove_empty_key(&mut self, key: i64) { - if self.get(&key).is_none_or(|posting| !posting.is_empty()) { - return; - } - match self { - Self::Dense { start, postings } => { - let Some(offset) = usize::try_from(i128::from(key) - i128::from(*start)).ok() - else { - return; - }; - if offset == postings.len().saturating_sub(1) { - postings.pop(); - } else if offset == 0 { - postings.remove(0); - *start = start.saturating_add(1); - } else { - self.materialize_sparse(); - if let Self::Sparse(keys) = self { - keys.remove(&key); + let new_payload = if let Some(cached) = cached_payload { + let previous = Arc::try_unwrap(cached) + .unwrap_or_else(|arc| arc.as_slice().to_vec()); + append_encoded_rows_to_table_payload( + previous, + data.row_count(), + &appended_rows, + )? + } else { + let previous_payload = read_overflow(&store, previous_pointer)?; + append_encoded_rows_to_table_payload( + previous_payload, + data.row_count(), + &appended_rows, + )? + }; + let checksum = crc32c_parts(&[new_payload.as_slice()]); + let ptr = rewrite_overflow( + &mut store, + previous_pointer, + &new_payload, + CompressionMode::Never, + )?; + let new_chain_cache = + build_overflow_chain_cache(&store, ptr.head_page_id)?; + let tail = + read_uncompressed_overflow_tail(&store, ptr)?.unwrap_or_default(); + self.overflow_chain_caches + .insert(canonical_table_name.clone(), new_chain_cache); + let row_count = data.row_count(); + self.persisted_tables_mut().insert( + canonical_table_name.clone(), + PersistedTableState { + pointer: ptr, + checksum, + row_count, + tail, + pk_index_root: previous_state.pk_index_root, + }, + ); + if db.config().persistent_pk_index { + let pk_index_root = + build_persistent_pk_index_root(db, new_payload.as_slice())?; + replace_table_pk_index_root( + self, + db, + &canonical_table_name, + pk_index_root, + )?; + } else { + replace_table_pk_index_root(self, db, &canonical_table_name, None)?; + } + self.cache_payload_insert( + canonical_table_name.clone(), + Arc::new(new_payload), + ); + continue; + } } } - } - Self::Sparse(keys) => { - keys.remove(&key); - } - } - } - - fn shrink_to_fit(&mut self) -> usize { - match self { - Self::Dense { postings, .. } => { - let old_capacity = postings.capacity(); - let mut freed = postings.iter_mut().fold(0usize, |freed, posting| { - freed.saturating_add(posting.shrink_to_fit()) - }); - postings.shrink_to_fit(); - freed = freed.saturating_add( - old_capacity - .saturating_sub(postings.capacity()) - .saturating_mul(std::mem::size_of::()), - ); - freed - } - Self::Sparse(keys) => { - let old_capacity = keys.capacity(); - let mut freed = keys.values_mut().fold(0usize, |freed, posting| { - freed.saturating_add(posting.shrink_to_fit()) - }); - keys.shrink_to_fit(); - freed = freed.saturating_add( - old_capacity - .saturating_sub(keys.capacity()) - .saturating_mul(std::mem::size_of::<(i64, RuntimeInt64RowIds)>()), - ); - freed - } - } - } - - #[cfg(test)] - pub(crate) fn is_dense(&self) -> bool { - matches!(self, Self::Dense { .. }) - } -} - -pub(crate) enum NonUniqueInt64KeysIter<'a> { - Dense { - start: i64, - offset: usize, - postings: std::slice::Iter<'a, RuntimeInt64RowIds>, - }, - Sparse(std::collections::hash_map::Iter<'a, i64, RuntimeInt64RowIds>), -} - -impl<'a> Iterator for NonUniqueInt64KeysIter<'a> { - type Item = (i64, &'a RuntimeInt64RowIds); - - fn next(&mut self) -> Option { - match self { - Self::Dense { - start, - offset, - postings, - } => { - let row_ids = postings.next()?; - let key = UniqueInt64Keys::dense_value_at(*start, *offset)?; - *offset += 1; - Some((key, row_ids)) - } - Self::Sparse(keys) => keys.next().map(|(key, row_ids)| (*key, row_ids)), - } - } - - fn size_hint(&self) -> (usize, Option) { - match self { - Self::Dense { postings, .. } => postings.size_hint(), - Self::Sparse(keys) => keys.size_hint(), - } - } -} - -impl ExactSizeIterator for NonUniqueInt64KeysIter<'_> {} - -pub(crate) enum NonUniqueInt64Values<'a> { - Dense(std::slice::Iter<'a, RuntimeInt64RowIds>), - Sparse(std::collections::hash_map::Values<'a, i64, RuntimeInt64RowIds>), -} - -impl<'a> Iterator for NonUniqueInt64Values<'a> { - type Item = &'a RuntimeInt64RowIds; - - fn next(&mut self) -> Option { - match self { - Self::Dense(postings) => postings.next(), - Self::Sparse(postings) => postings.next(), - } - } - - fn size_hint(&self) -> (usize, Option) { - match self { - Self::Dense(postings) => postings.size_hint(), - Self::Sparse(postings) => postings.size_hint(), - } - } -} - -impl ExactSizeIterator for NonUniqueInt64Values<'_> {} - -#[derive(Clone, Debug)] -pub(crate) enum RuntimeBtreeKeys { - UniqueEncoded(Arc>, BTreeSet), - NonUniqueEncoded(Arc, BTreeSet), - UniqueInt64(Arc, BTreeSet), - NonUniqueInt64(Arc, BTreeSet), - UniqueUuid(Arc>, BTreeSet), - NonUniqueUuid(Arc>>, BTreeSet), -} - -/// Non-unique encoded-key map plus exact state for whether any posting can -/// release capacity at commit. High-cardinality text indexes normally contain -/// only singleton values, so commit can skip an otherwise -/// linear scan over every key. -#[derive(Debug)] -pub(crate) struct RuntimeEncodedPostings { - entries: BTreeMap, - shrinkable_postings: usize, -} - -impl Clone for RuntimeEncodedPostings { - fn clone(&self) -> Self { - // Cloning a Vec is allowed to choose a capacity different from the - // source. Recompute rather than copying the count so Arc::make_mut's - // COW clone cannot leave shrink bookkeeping stale. - Self::new(self.entries.clone()) - } -} - -impl RuntimeEncodedPostings { - fn new(entries: BTreeMap) -> Self { - let shrinkable_postings = entries - .values() - .filter(|row_ids| row_ids.is_shrinkable()) - .count(); - Self { - entries, - shrinkable_postings, - } - } - - fn adjust_shrinkable_count(&mut self, was_shrinkable: bool, is_shrinkable: bool) { - match (was_shrinkable, is_shrinkable) { - (false, true) => self.shrinkable_postings = self.shrinkable_postings.saturating_add(1), - (true, false) => self.shrinkable_postings = self.shrinkable_postings.saturating_sub(1), - _ => {} - } - } - - fn insert_row_id(&mut self, key: RuntimeEncodedKey, row_id: i64) { - use std::collections::btree_map::Entry; - - let (was_shrinkable, is_shrinkable) = match self.entries.entry(key) { - Entry::Vacant(entry) => { - entry.insert(RuntimeEncodedRowIds::one(row_id)); - (false, false) - } - Entry::Occupied(mut entry) => { - let row_ids = entry.get_mut(); - let was_shrinkable = row_ids.is_shrinkable(); - row_ids.push(row_id); - (was_shrinkable, row_ids.is_shrinkable()) - } - }; - self.adjust_shrinkable_count(was_shrinkable, is_shrinkable); - } - - fn remove_row_id_everywhere(&mut self, row_id: i64) { - for row_ids in self.entries.values_mut() { - row_ids.retain(|existing| *existing != row_id); - } - self.entries.retain(|_, row_ids| !row_ids.is_empty()); - self.shrinkable_postings = self - .entries - .values() - .filter(|row_ids| row_ids.is_shrinkable()) - .count(); - } - - fn remove_row_id_for_key(&mut self, key: &[u8], row_id: i64) { - let Some(row_ids) = self.entries.get_mut(key) else { - return; - }; - let was_shrinkable = row_ids.is_shrinkable(); - row_ids.retain(|existing| *existing != row_id); - let is_empty = row_ids.is_empty(); - let is_shrinkable = !is_empty && row_ids.is_shrinkable(); - if is_empty { - self.entries.remove(key); - } - self.adjust_shrinkable_count(was_shrinkable, is_shrinkable); - } - - fn shrink_to_fit(&mut self) -> usize { - if self.shrinkable_postings == 0 { - return 0; - } - let freed = self.entries.values_mut().fold(0usize, |freed, row_ids| { - freed.saturating_add(row_ids.shrink_to_fit()) - }); - // Vec::shrink_to_fit is explicitly best-effort. Recompute from the - // allocator's actual post-shrink capacities so zero can never become a - // false-clean state that permanently suppresses later compaction. - self.shrinkable_postings = self - .entries - .values() - .filter(|row_ids| row_ids.is_shrinkable()) - .count(); - freed - } - - #[cfg(test)] - fn shrinkable_posting_count(&self) -> usize { - self.shrinkable_postings - } -} - -impl std::ops::Deref for RuntimeEncodedPostings { - type Target = BTreeMap; - - fn deref(&self) -> &Self::Target { - &self.entries - } -} - -/// Row IDs stored beneath one encoded key in a non-unique runtime index. -/// -/// Encoded indexes are commonly declared non-unique even when their data is -/// high-cardinality. On 64-bit targets, keeping the first row ID inline avoids -/// a heap allocation for every such key while preserving the insertion order -/// used by index scans; a second row promotes the singleton to the existing -/// `Vec` layout. Supported 32-bit targets use `Vec` directly because an -/// `i64`-carrying enum would exceed the former three-word object footprint. -#[cfg(target_pointer_width = "64")] -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum RuntimeEncodedRowIds { - One(i64), - Many(Vec), -} - -#[cfg(target_pointer_width = "32")] -#[derive(Clone, Debug, Eq, PartialEq)] -#[repr(transparent)] -pub(crate) struct RuntimeEncodedRowIds(Vec); -// Keep every posting object no larger than the Vec it replaces on all -// supported pointer widths, including wasm32. This intentionally lives in -// production code so cross-target checks enforce the layout invariant. -const _: () = - assert!(std::mem::size_of::() == std::mem::size_of::>()); + if use_paged_row_storage { + self.overflow_chain_caches.remove(&canonical_table_name); + let new_state = if delta.append_count > 0 + && delta.updated_rows.is_empty() + && delta.deleted_rows.is_empty() + && !data.has_tombstoned_rows() + { + let existing_count = data.rows.len().saturating_sub(delta.append_count); + let appended_chunks = encode_paged_table_chunks_from_rows( + &data.rows[existing_count..], + db.config().page_size, + )?; + if !appended_chunks.is_empty() { + append_paged_table_chunks( + &mut store, + previous_state, + &appended_chunks, + data.row_count(), + )? + } else { + rewrite_paged_table_from_resident( + &mut store, + previous_state, + data, + db.config().page_size, + )? + } + } else if delta.updated_rows.is_empty() && !delta.deleted_rows.is_empty() { + // Delete-only delta: avoid decoding values for chunks that + // contain no deleted rows by scanning row ids only. + rewrite_paged_table_from_resident_delete_only( + &mut store, + previous_state, + data, + db.config().page_size, + &delta.deleted_rows, + )? + } else { + rewrite_paged_table_from_resident( + &mut store, + previous_state, + data, + db.config().page_size, + )? + }; + self.persisted_tables_mut() + .insert(canonical_table_name.clone(), new_state); + let pk_index_root = self.refresh_paged_lookup_cache_and_pk_index( + db, + &store, + &canonical_table_name, + new_state, + )?; + replace_table_pk_index_root(self, db, &canonical_table_name, pk_index_root)?; + self.cache_payload_remove(&canonical_table_name); + continue; + } -impl RuntimeEncodedRowIds { - fn one(row_id: i64) -> Self { - #[cfg(target_pointer_width = "64")] - { - Self::One(row_id) - } - #[cfg(target_pointer_width = "32")] - { - Self(vec![row_id]) - } - } + // Choose the encoding path: + // 1. Row-update splice: only re-encode modified rows using cached payload + // 2. Row-delete splice: copy unchanged encoded rows from previous payload + // 3. Append-only: read old payload, append new rows + // 4. Full re-encode: encode every row from scratch + let mut resident_tombstone_locators_preserved = false; + let (payload, dirty_byte_ranges, pk_locator_preserved) = if !delta + .updated_rows + .is_empty() + && delta.deleted_rows.is_empty() + && delta.append_count == 0 + { + let mut dirty_indices = Vec::with_capacity(delta.updated_rows.len()); + for row_id in delta.updated_rows.keys() { + if let Some(idx) = data.row_index_by_id(*row_id) { + dirty_indices.push(idx); + } + } + dirty_indices.sort_unstable(); - #[cfg(test)] - fn many(row_ids: Vec) -> Self { - #[cfg(target_pointer_width = "64")] - { - Self::Many(row_ids) - } - #[cfg(target_pointer_width = "32")] - { - Self(row_ids) - } - } - - fn as_slice(&self) -> &[i64] { - #[cfg(target_pointer_width = "64")] - { - match self { - Self::One(row_id) => std::slice::from_ref(row_id), - Self::Many(row_ids) => row_ids.as_slice(), - } - } - #[cfg(target_pointer_width = "32")] - { - self.0.as_slice() - } - } - - fn push(&mut self, row_id: i64) { - #[cfg(target_pointer_width = "64")] - { - match self { - Self::One(first_row_id) => { - // Match Vec's small-allocation growth behavior so postings - // with a few duplicates do not immediately reallocate. - let mut row_ids = Vec::with_capacity(4); - row_ids.push(*first_row_id); - row_ids.push(row_id); - *self = Self::Many(row_ids); - } - Self::Many(row_ids) => row_ids.push(row_id), - } - } - #[cfg(target_pointer_width = "32")] - { - self.0.push(row_id); - } - } - - fn is_shrinkable(&self) -> bool { - #[cfg(target_pointer_width = "64")] - { - match self { - Self::One(_) => false, - Self::Many(row_ids) => row_ids.len() == 1 || row_ids.capacity() > row_ids.len(), - } - } - #[cfg(target_pointer_width = "32")] - { - self.0.capacity() > self.0.len() - } - } - - fn retain(&mut self, retain: impl FnMut(&i64) -> bool) { - #[cfg(target_pointer_width = "64")] - { - let mut retain = retain; - match self { - Self::One(row_id) => { - if !retain(row_id) { - // Empty postings are transient: every map-owning caller - // removes the entry immediately after retaining. - *self = Self::Many(Vec::new()); + if let Some(cached) = self.cached_payload_take(&canonical_table_name) { + match Arc::try_unwrap(cached) { + Ok(mut payload) => { + if let Some(dirty_range) = splice_updated_rows_payload_in_place( + &mut payload, + data, + &dirty_indices, + )? { + ( + payload, + single_dirty_range( + dirty_range.first_dirty_byte + ..dirty_range.last_dirty_byte, + ), + true, + ) + } else { + let splice = splice_updated_rows_payload( + payload.as_slice(), + data, + &dirty_indices, + )?; + let first = splice.first_dirty_byte; + let last = splice.last_dirty_byte; + ( + splice.payload, + single_dirty_range(first..last), + splice.pk_locator_preserved, + ) + } + } + Err(cached) => { + let splice = splice_updated_rows_payload( + cached.as_slice(), + data, + &dirty_indices, + )?; + let first = splice.first_dirty_byte; + let last = splice.last_dirty_byte; + ( + splice.payload, + single_dirty_range(first..last), + splice.pk_locator_preserved, + ) + } + } + } else if previous_pointer.head_page_id != 0 { + let mut payload = read_overflow(&store, previous_pointer)?; + if let Some(dirty_range) = splice_updated_rows_payload_in_place( + &mut payload, + data, + &dirty_indices, + )? { + ( + payload, + single_dirty_range( + dirty_range.first_dirty_byte..dirty_range.last_dirty_byte, + ), + true, + ) + } else { + let splice = splice_updated_rows_payload( + payload.as_slice(), + data, + &dirty_indices, + )?; + let first = splice.first_dirty_byte; + let last = splice.last_dirty_byte; + ( + splice.payload, + single_dirty_range(first..last), + splice.pk_locator_preserved, + ) + } + } else { + let payload = encode_table_payload(data)?; + let last = payload.len(); + (payload, single_dirty_range(0..last), false) } - } - Self::Many(row_ids) => row_ids.retain(retain), - } - } - #[cfg(target_pointer_width = "32")] - { - self.0.retain(retain); - } - } - - fn shrink_to_fit(&mut self) -> usize { - #[cfg(target_pointer_width = "64")] - { - let Self::Many(row_ids) = self else { - return 0; - }; - let old_capacity = row_ids.capacity(); - if row_ids.len() == 1 { - let row_id = row_ids[0]; - *self = Self::One(row_id); - return old_capacity.saturating_mul(std::mem::size_of::()); - } - row_ids.shrink_to_fit(); - old_capacity - .saturating_sub(row_ids.capacity()) - .saturating_mul(std::mem::size_of::()) - } - #[cfg(target_pointer_width = "32")] - { - let old_capacity = self.0.capacity(); - self.0.shrink_to_fit(); - old_capacity - .saturating_sub(self.0.capacity()) - .saturating_mul(std::mem::size_of::()) - } - } - - #[cfg(test)] - fn is_inline_singleton(&self) -> bool { - #[cfg(target_pointer_width = "64")] - { - matches!(self, Self::One(_)) - } - #[cfg(target_pointer_width = "32")] - { - false - } - } -} - -impl std::ops::Deref for RuntimeEncodedRowIds { - type Target = [i64]; - - fn deref(&self) -> &Self::Target { - self.as_slice() - } -} - -impl<'a> IntoIterator for &'a RuntimeEncodedRowIds { - type Item = &'a i64; - type IntoIter = std::slice::Iter<'a, i64>; - - fn into_iter(self) -> Self::IntoIter { - self.as_slice().iter() - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum RuntimeRowIdSet<'a> { - Empty, - Single(i64), - Contiguous { start: i64, len: usize }, - Many(&'a [i64]), - Owned(Vec), -} - -impl RuntimeRowIdSet<'_> { - #[must_use] - pub(crate) fn len(&self) -> usize { - match self { - Self::Empty => 0, - Self::Single(_) => 1, - Self::Contiguous { len, .. } => *len, - Self::Many(values) => values.len(), - Self::Owned(values) => values.len(), - } - } - - #[must_use] - pub(crate) fn is_empty(&self) -> bool { - matches!(self, Self::Empty) - } - - pub(crate) fn for_each(&self, mut f: impl FnMut(i64)) { - match self { - Self::Empty => {} - Self::Single(row_id) => f(*row_id), - Self::Contiguous { start, len } => { - for offset in 0..*len { - if let Some(row_id) = RuntimeInt64RowIds::value_at(*start, offset) { - f(row_id); + } else if !delta.deleted_rows.is_empty() + && delta.updated_rows.is_empty() + && delta.append_count == 0 + { + if !db.config().paged_row_storage + && !db.config().persistent_pk_index + && previous_pointer.head_page_id != 0 + && !previous_pointer.is_compressed() + { + if let (Some(cached), Some(locators)) = ( + self.cached_payload_take(&canonical_table_name), + resident_tombstone_locators.as_deref(), + ) { + let mut payload = Arc::try_unwrap(cached) + .unwrap_or_else(|arc| arc.as_slice().to_vec()); + if let Some((dirty_ranges, checksum)) = + tombstone_deleted_rows_cached_payload_by_locator( + &mut payload, + &delta.deleted_rows, + locators, + previous_state.checksum, + )? + { + let chain_cache = + match self.overflow_chain_caches.get(&canonical_table_name) { + Some(cache) => cache.clone(), + None => build_overflow_chain_cache( + &store, + previous_pointer.head_page_id, + )?, + }; + let (pointer, new_chain_cache, tail) = + rewrite_overflow_cached_with_dirty_byte_ranges( + &mut store, + previous_pointer, + &payload, + &chain_cache.page_ids, + 0, + Some(dirty_ranges.as_slice()), + )?; + self.overflow_chain_caches + .insert(canonical_table_name.clone(), new_chain_cache); + self.persisted_tables_mut().insert( + canonical_table_name.clone(), + PersistedTableState { + pointer, + checksum, + row_count: data.row_count(), + tail, + pk_index_root: previous_state.pk_index_root, + }, + ); + replace_table_pk_index_root(self, db, &canonical_table_name, None)?; + self.cache_payload_insert( + canonical_table_name.clone(), + Arc::new(payload), + ); + continue; + } + self.cache_payload_insert( + canonical_table_name.clone(), + Arc::new(payload), + ); + } } - } - } - Self::Many(values) => { - for row_id in *values { - f(*row_id); - } - } - Self::Owned(values) => { - for row_id in values { - f(*row_id); - } - } - } - } - - fn visit_until(self, mut visitor: impl FnMut(i64) -> Result) -> Result { - match self { - Self::Empty => Ok(false), - Self::Single(row_id) => visitor(row_id), - Self::Contiguous { start, len } => { - for offset in 0..len { - let Some(row_id) = RuntimeInt64RowIds::value_at(start, offset) else { - break; - }; - if visitor(row_id)? { - return Ok(true); - } - } - Ok(false) - } - Self::Many(values) => { - for row_id in values { - if visitor(*row_id)? { - return Ok(true); - } - } - Ok(false) - } - Self::Owned(values) => { - for row_id in values { - if visitor(row_id)? { - return Ok(true); - } - } - Ok(false) - } - } - } -} - -fn visible_row_id_set_count( - row_source: VisibleTableRowSource<'_>, - row_ids: RuntimeRowIdSet<'_>, -) -> Result { - if !row_source.has_tombstoned_rows() { - return Ok(row_ids.len()); - } - let mut count = 0usize; - let mut error = None; - row_ids.for_each(|row_id| { - if error.is_some() { - return; - } - match row_source.row_by_id(row_id) { - Ok(Some(_)) => count += 1, - Ok(None) => {} - Err(err) => error = Some(err), - } - }); - if let Some(error) = error { - return Err(error); - } - Ok(count) -} - -impl RuntimeBtreeKeys { - fn shrink_row_id_vecs<'a>(row_ids: impl Iterator>) -> usize { - let mut freed = 0usize; - for row_ids in row_ids { - let old_capacity = row_ids.capacity(); - row_ids.shrink_to_fit(); - freed = freed.saturating_add( - old_capacity - .saturating_sub(row_ids.capacity()) - .saturating_mul(std::mem::size_of::()), - ); - } - freed - } - - fn shrink_to_fit_if_unique(&mut self) -> usize { - match self { - Self::UniqueEncoded(_, _) - | Self::UniqueUuid(_, _) - | Self::NonUniqueEncoded(_, _) - | Self::NonUniqueUuid(_, _) => { - let mut freed = 0usize; - match self { - Self::NonUniqueEncoded(keys, _) => { - if let Some(keys) = Arc::get_mut(keys) { - freed = freed.saturating_add(keys.shrink_to_fit()); - } - } - Self::NonUniqueUuid(keys, _) => { - if let Some(keys) = Arc::get_mut(keys) { - freed = - freed.saturating_add(Self::shrink_row_id_vecs(keys.values_mut())); - } - } - _ => {} - } - freed - } - Self::UniqueInt64(keys, _) => Arc::get_mut(keys) - .map(UniqueInt64Keys::shrink_to_fit) - .unwrap_or(0), - Self::NonUniqueInt64(keys, _) => { - let Some(keys) = Arc::get_mut(keys) else { - return 0; - }; - keys.shrink_to_fit() - } - } - } - - fn push_non_unique_row_id(row_ids: &mut Vec, row_id: i64) { - // Keep Vec's geometric growth policy on the insert hot path. The - // explicit 1.5x `reserve_exact` policy caused several extra - // reallocations for the common 50-150-row posting lists while the - // post-commit shrink pass already recovers excess capacity. - row_ids.push(row_id); - } - - fn visible_single(row_id: i64, deleted_row_ids: &BTreeSet) -> RuntimeRowIdSet<'_> { - if deleted_row_ids.is_empty() { - return RuntimeRowIdSet::Single(row_id); - } - if deleted_row_ids.contains(&row_id) { - RuntimeRowIdSet::Empty - } else { - RuntimeRowIdSet::Single(row_id) - } - } - - fn visible_many<'a>( - row_ids: &'a [i64], - deleted_row_ids: &BTreeSet, - ) -> RuntimeRowIdSet<'a> { - if deleted_row_ids.is_empty() { - return RuntimeRowIdSet::Many(row_ids); - } - let mut first_deleted = None; - for (index, row_id) in row_ids.iter().copied().enumerate() { - if deleted_row_ids.contains(&row_id) { - first_deleted = Some(index); - break; - } - } - let Some(first_deleted) = first_deleted else { - return RuntimeRowIdSet::Many(row_ids); - }; - - let mut visible = Vec::new(); - for row_id in row_ids[..first_deleted].iter().copied() { - if !deleted_row_ids.contains(&row_id) { - visible.push(row_id); - } - } - for row_id in row_ids[first_deleted..].iter().copied() { - if !deleted_row_ids.contains(&row_id) { - visible.push(row_id); - } - } - if visible.is_empty() { - RuntimeRowIdSet::Empty - } else { - RuntimeRowIdSet::Owned(visible) - } - } - - fn visible_encoded_row_ids<'a>( - row_ids: &'a RuntimeEncodedRowIds, - deleted_row_ids: &'a BTreeSet, - ) -> RuntimeRowIdSet<'a> { - match row_ids.as_slice() { - [row_id] => Self::visible_single(*row_id, deleted_row_ids), - row_ids => Self::visible_many(row_ids, deleted_row_ids), - } - } - - fn visible_int64_row_ids<'a>( - row_ids: &'a RuntimeInt64RowIds, - deleted_row_ids: &'a BTreeSet, - ) -> RuntimeRowIdSet<'a> { - match row_ids { - RuntimeInt64RowIds::One(row_id) => Self::visible_single(*row_id, deleted_row_ids), - RuntimeInt64RowIds::Contiguous { start, len } => { - if deleted_row_ids.is_empty() { - return RuntimeRowIdSet::Contiguous { - start: *start, - len: *len, - }; - } - let Some(end) = len - .checked_sub(1) - .and_then(|offset| RuntimeInt64RowIds::value_at(*start, offset)) - else { - return RuntimeRowIdSet::Empty; - }; - if deleted_row_ids.range(*start..=end).next().is_none() { - return RuntimeRowIdSet::Contiguous { - start: *start, - len: *len, - }; - } - let visible = row_ids - .iter() - .filter(|row_id| !deleted_row_ids.contains(row_id)) - .collect::>(); - if visible.is_empty() { - RuntimeRowIdSet::Empty - } else { - RuntimeRowIdSet::Owned(visible) - } - } - RuntimeInt64RowIds::Many(row_ids) => Self::visible_many(row_ids, deleted_row_ids), - } - } - - fn row_ids_for_row_id(&self, row_id: i64) -> RuntimeRowIdSet<'_> { - match self { - Self::UniqueInt64(keys, deleted) => { - keys.get(&row_id).map_or(RuntimeRowIdSet::Empty, |row_id| { - Self::visible_single(row_id, deleted) - }) - } - Self::NonUniqueInt64(keys, deleted) => keys - .get(&row_id) - .map(|row_ids| Self::visible_int64_row_ids(row_ids, deleted)) - .unwrap_or(RuntimeRowIdSet::Empty), - Self::UniqueEncoded(..) - | Self::NonUniqueEncoded(..) - | Self::UniqueUuid(..) - | Self::NonUniqueUuid(..) => RuntimeRowIdSet::Empty, - } - } - - fn row_id_set_for_key(&self, key: &RuntimeBtreeKey) -> RuntimeRowIdSet<'_> { - match (self, key) { - (Self::UniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) - if deleted.is_empty() => - { - keys.get(key) - .copied() - .map_or(RuntimeRowIdSet::Empty, RuntimeRowIdSet::Single) - } - (Self::UniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => keys - .get(key) - .copied() - .map(|row_id| Self::visible_single(row_id, deleted)) - .unwrap_or(RuntimeRowIdSet::Empty), - (Self::NonUniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => keys - .get(key) - .map(|row_ids| Self::visible_encoded_row_ids(row_ids, deleted)) - .unwrap_or(RuntimeRowIdSet::Empty), - (Self::UniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) - if deleted.is_empty() => - { - keys.get(key) - .map_or(RuntimeRowIdSet::Empty, RuntimeRowIdSet::Single) - } - (Self::UniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => keys - .get(key) - .map(|row_id| Self::visible_single(row_id, deleted)) - .unwrap_or(RuntimeRowIdSet::Empty), - (Self::NonUniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => keys - .get(key) - .map(|row_ids| Self::visible_int64_row_ids(row_ids, deleted)) - .unwrap_or(RuntimeRowIdSet::Empty), - (Self::UniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) if deleted.is_empty() => { - keys.get(key) - .copied() - .map_or(RuntimeRowIdSet::Empty, RuntimeRowIdSet::Single) - } - (Self::UniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => keys - .get(key) - .copied() - .map(|row_id| Self::visible_single(row_id, deleted)) - .unwrap_or(RuntimeRowIdSet::Empty), - (Self::NonUniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) - if deleted.is_empty() => - { - keys.get(key) - .map(|row_ids| RuntimeRowIdSet::Many(row_ids.as_slice())) - .unwrap_or(RuntimeRowIdSet::Empty) - } - (Self::NonUniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => keys - .get(key) - .map(|row_ids| Self::visible_many(row_ids, deleted)) - .unwrap_or(RuntimeRowIdSet::Empty), - _ => RuntimeRowIdSet::Empty, - } - } - - pub(super) fn row_ids_for_key(&self, key: &RuntimeBtreeKey) -> Vec { - let row_ids = self.row_id_set_for_key(key); - let mut values = Vec::with_capacity(row_ids.len()); - row_ids.for_each(|row_id| values.push(row_id)); - values - } - - pub(super) fn row_ids_for_encoded_key_prefix(&self, prefix: &[Value]) -> Result> { - if prefix.is_empty() { - return Ok(Vec::new()); - } - self.row_ids_for_encoded_key_prefixes(std::slice::from_ref(&prefix)) - } - - pub(super) fn row_ids_for_encoded_key_prefixes( - &self, - prefixes: &[&[Value]], - ) -> Result> { - if prefixes.is_empty() || prefixes.iter().any(|prefix| prefix.is_empty()) { - return Ok(Vec::new()); - } - let mut row_ids = Vec::new(); - let mut collect_matching_row_ids = - |encoded_key: &[u8], entry_row_ids: &[i64]| -> Result<()> { - let mut matched = false; - for prefix in prefixes { - if Row::encoded_prefix_matches(encoded_key, prefix)? { - matched = true; - break; - } - } - if matched { - row_ids.extend(entry_row_ids.iter().copied()); - } - Ok(()) - }; - - match self { - Self::UniqueEncoded(keys, deleted) if deleted.is_empty() => { - for (encoded_key, row_id) in keys.iter() { - collect_matching_row_ids(encoded_key, std::slice::from_ref(row_id))?; - } - } - Self::UniqueEncoded(keys, deleted) => { - for (encoded_key, row_id) in keys.iter() { - if deleted.contains(row_id) { - continue; - } - collect_matching_row_ids(encoded_key, std::slice::from_ref(row_id))?; - } - } - Self::NonUniqueEncoded(keys, deleted) if deleted.is_empty() => { - for (encoded_key, entry_row_ids) in keys.iter() { - collect_matching_row_ids(encoded_key, entry_row_ids)?; - } - } - Self::NonUniqueEncoded(keys, deleted) => { - for (encoded_key, entry_row_ids) in keys.iter() { - let visible = entry_row_ids - .iter() - .copied() - .filter(|row_id| !deleted.contains(row_id)) - .collect::>(); - collect_matching_row_ids(encoded_key, &visible)?; - } - } - Self::UniqueInt64(_, _) - | Self::NonUniqueInt64(_, _) - | Self::UniqueUuid(_, _) - | Self::NonUniqueUuid(_, _) => {} - } - - Ok(row_ids) - } - - pub(super) fn row_ids_for_value_set(&self, value: &Value) -> Result> { - match self { - Self::UniqueEncoded(_, _) | Self::NonUniqueEncoded(_, _) => { - let key = RuntimeBtreeKey::Encoded(encode_runtime_index_key(value)?); - Ok(self.row_id_set_for_key(&key)) - } - Self::UniqueInt64(_, _) | Self::NonUniqueInt64(_, _) => match value { - Value::Int64(value) => Ok(self.row_id_set_for_key(&RuntimeBtreeKey::Int64(*value))), - _ => Ok(RuntimeRowIdSet::Empty), - }, - Self::UniqueUuid(_, _) | Self::NonUniqueUuid(_, _) => match value { - Value::Uuid(value) => Ok(self.row_id_set_for_key(&RuntimeBtreeKey::Uuid(*value))), - _ => Ok(RuntimeRowIdSet::Empty), - }, - } - } - - pub(super) fn row_ids_for_value(&self, value: &Value) -> Result> { - let row_ids = self.row_ids_for_value_set(value)?; - let mut values = Vec::with_capacity(row_ids.len()); - row_ids.for_each(|row_id| values.push(row_id)); - Ok(values) - } - - pub(super) fn row_ids_for_values(&self, values: &[&Value]) -> Result> { - if values.is_empty() { - return Ok(Vec::new()); - } - let mut row_ids = Vec::new(); - match self { - Self::UniqueEncoded(keys, deleted) => { - for value in values { - let key = encode_runtime_index_key(value)?; - if let Some(row_id) = keys.get(&key) { - if !deleted.contains(row_id) { - row_ids.push(*row_id); - } - } - } - } - Self::NonUniqueEncoded(keys, deleted) => { - for value in values { - let key = encode_runtime_index_key(value)?; - if let Some(entry_row_ids) = keys.get(&key) { - row_ids.extend( - entry_row_ids - .iter() - .copied() - .filter(|row_id| !deleted.contains(row_id)), - ); - } - } - } - Self::UniqueInt64(keys, deleted) => { - for value in values { - if let Value::Int64(value) = value { - if let Some(row_id) = keys.get(value) { - if !deleted.contains(&row_id) { - row_ids.push(row_id); - } - } - } - } - } - Self::NonUniqueInt64(keys, deleted) => { - for value in values { - if let Value::Int64(value) = value { - if let Some(entry_row_ids) = keys.get(value) { - row_ids.extend( - entry_row_ids - .iter() - .filter(|row_id| !deleted.contains(row_id)), - ); - } - } - } - } - Self::UniqueUuid(keys, deleted) => { - for value in values { - if let Value::Uuid(value) = value { - if let Some(row_id) = keys.get(value) { - if !deleted.contains(row_id) { - row_ids.push(*row_id); - } - } - } - } - } - Self::NonUniqueUuid(keys, deleted) => { - for value in values { - if let Value::Uuid(value) = value { - if let Some(entry_row_ids) = keys.get(value) { - row_ids.extend( - entry_row_ids - .iter() - .copied() - .filter(|row_id| !deleted.contains(row_id)), - ); - } - } - } - } - } - Ok(row_ids) - } - - fn distinct_key_counts(&self) -> Vec<(RuntimeBtreeKey, usize)> { - match self { - Self::UniqueEncoded(keys, deleted) => keys - .iter() - .filter(|(_, row_id)| !deleted.contains(row_id)) - .map(|(key, _)| (RuntimeBtreeKey::Encoded(key.clone()), 1)) - .collect(), - Self::NonUniqueEncoded(keys, deleted) => keys - .iter() - .map(|(key, row_ids)| { - ( - RuntimeBtreeKey::Encoded(key.clone()), - row_ids - .iter() - .filter(|row_id| !deleted.contains(row_id)) - .count(), - ) - }) - .filter(|(_, count)| *count > 0) - .collect(), - Self::UniqueInt64(keys, deleted) => keys - .iter() - .filter(|(_, row_id)| !deleted.contains(row_id)) - .map(|(key, _)| (RuntimeBtreeKey::Int64(key), 1)) - .collect(), - Self::NonUniqueInt64(keys, deleted) => keys - .iter() - .map(|(key, row_ids)| { - ( - RuntimeBtreeKey::Int64(key), - row_ids - .iter() - .filter(|row_id| !deleted.contains(row_id)) - .count(), - ) - }) - .filter(|(_, count)| *count > 0) - .collect(), - Self::UniqueUuid(keys, deleted) => keys - .iter() - .filter(|(_, row_id)| !deleted.contains(row_id)) - .map(|(key, _)| (RuntimeBtreeKey::Uuid(*key), 1)) - .collect(), - Self::NonUniqueUuid(keys, deleted) => keys - .iter() - .map(|(key, row_ids)| { - ( - RuntimeBtreeKey::Uuid(*key), - row_ids - .iter() - .filter(|row_id| !deleted.contains(row_id)) - .count(), - ) - }) - .filter(|(_, count)| *count > 0) - .collect(), - } - } - - #[cfg(test)] - pub(super) fn contains_any(&self, key: &RuntimeBtreeKey) -> bool { - match (self, key) { - (Self::UniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => keys - .get(key) - .is_some_and(|row_id| !deleted.contains(row_id)), - (Self::NonUniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => keys - .get(key) - .is_some_and(|row_ids| row_ids.iter().any(|row_id| !deleted.contains(row_id))), - (Self::UniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => keys - .get(key) - .is_some_and(|row_id| !deleted.contains(&row_id)), - (Self::NonUniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => keys - .get(key) - .is_some_and(|row_ids| row_ids.iter().any(|row_id| !deleted.contains(&row_id))), - (Self::UniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => keys - .get(key) - .is_some_and(|row_id| !deleted.contains(row_id)), - (Self::NonUniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => keys - .get(key) - .is_some_and(|row_ids| row_ids.iter().any(|row_id| !deleted.contains(row_id))), - _ => false, - } - } - - pub(super) fn insert_row_id(&mut self, key: RuntimeBtreeKey, row_id: i64) -> Result<()> { - match (self, key) { - (Self::UniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => { - if deleted.remove(&row_id) { - Arc::make_mut(keys).retain(|_, existing| *existing != row_id); - } - if let Some(existing) = keys.get(&key).copied() { - if deleted.remove(&existing) { - Arc::make_mut(keys).insert(key, row_id); - return Ok(()); - } - return Err(DbError::internal( - "unique runtime BTREE index received a duplicate key insert", - )); - } - Arc::make_mut(keys).insert(key, row_id); - } - (Self::NonUniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => { - let keys = Arc::make_mut(keys); - if !deleted.is_empty() && deleted.remove(&row_id) { - keys.remove_row_id_everywhere(row_id); - } - keys.insert_row_id(key, row_id); - } - (Self::UniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => { - let revived = deleted.remove(&row_id); - let keys = Arc::make_mut(keys); - if revived { - if keys.get(&key) == Some(row_id) { - return Ok(()); - } - keys.remove_row_id_mapping(row_id); - } - if let Some(existing) = keys.get(&key) { - if deleted.remove(&existing) { - keys.insert(key, row_id); - return Ok(()); - } - return Err(DbError::internal( - "unique runtime BTREE index received a duplicate key insert", - )); - } - keys.insert(key, row_id); - } - (Self::NonUniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => { - let keys = Arc::make_mut(keys); - if deleted.remove(&row_id) { - keys.remove_row_id_mapping(row_id); - } - keys.insert_row_id(key, row_id); - } - (Self::UniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => { - if deleted.remove(&row_id) { - Arc::make_mut(keys).retain(|_, existing| *existing != row_id); - } - if let Some(existing) = keys.get(&key).copied() { - if deleted.remove(&existing) { - Arc::make_mut(keys).insert(key, row_id); - return Ok(()); - } - return Err(DbError::internal( - "unique runtime BTREE index received a duplicate key insert", - )); - } - Arc::make_mut(keys).insert(key, row_id); - } - (Self::NonUniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => { - let keys = Arc::make_mut(keys); - if deleted.remove(&row_id) { - for row_ids in keys.values_mut() { - row_ids.retain(|existing| *existing != row_id); - } - keys.retain(|_, row_ids| !row_ids.is_empty()); - } - Self::push_non_unique_row_id(keys.entry(key).or_default(), row_id); - } - _ => { - return Err(DbError::internal( - "runtime BTREE key type did not match the runtime index representation", - )); - } - } - Ok(()) - } - - pub(super) fn move_row_id( - &mut self, - old_key: &RuntimeBtreeKey, - new_key: RuntimeBtreeKey, - row_id: i64, - ) -> Result { - match (self, old_key, new_key) { - ( - Self::NonUniqueEncoded(keys, deleted), - RuntimeBtreeKey::Encoded(old_key), - RuntimeBtreeKey::Encoded(new_key), - ) if !deleted.contains(&row_id) => { - let keys = Arc::make_mut(keys); - keys.remove_row_id_for_key(old_key.as_slice(), row_id); - keys.insert_row_id(new_key, row_id); - Ok(true) - } - ( - Self::NonUniqueInt64(keys, deleted), - RuntimeBtreeKey::Int64(old_key), - RuntimeBtreeKey::Int64(new_key), - ) if !deleted.contains(&row_id) => { - let keys = Arc::make_mut(keys); - if let Some(row_ids) = keys.get_mut(old_key) { - row_ids.retain(|existing| *existing != row_id); - } - keys.remove_empty_key(*old_key); - keys.insert_row_id(new_key, row_id); - Ok(true) - } - ( - Self::NonUniqueUuid(keys, deleted), - RuntimeBtreeKey::Uuid(old_key), - RuntimeBtreeKey::Uuid(new_key), - ) if !deleted.contains(&row_id) => { - let keys = Arc::make_mut(keys); - if let Some(row_ids) = keys.get_mut(old_key) { - row_ids.retain(|existing| *existing != row_id); - } - if keys.get(old_key).is_some_and(Vec::is_empty) { - keys.remove(old_key); - } - Self::push_non_unique_row_id(keys.entry(new_key).or_default(), row_id); - Ok(true) - } - _ => Ok(false), - } - } - - pub(super) fn remove_row_id(&mut self, key: &RuntimeBtreeKey, row_id: i64) -> Result<()> { - match (self, key) { - (Self::UniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => { - if let Some(existing) = keys.get(key).copied() { - if existing != row_id { - return Err(DbError::internal( - "unique runtime BTREE index row-id mismatch during delete", - )); - } - deleted.insert(row_id); - } - } - (Self::NonUniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => { - if keys - .get(key) - .is_some_and(|row_ids| row_ids.contains(&row_id)) - { - deleted.insert(row_id); - } - } - (Self::UniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => { - if let Some(existing) = keys.get(key) { - if existing != row_id { - return Err(DbError::internal( - "unique runtime BTREE index row-id mismatch during delete", - )); - } - deleted.insert(row_id); - } - } - (Self::NonUniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => { - if keys - .get(key) - .is_some_and(|row_ids| row_ids.contains(&row_id)) - { - deleted.insert(row_id); - } - } - (Self::UniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => { - if let Some(existing) = keys.get(key).copied() { - if existing != row_id { - return Err(DbError::internal( - "unique runtime BTREE index row-id mismatch during delete", - )); - } - deleted.insert(row_id); - } - } - (Self::NonUniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => { - if keys - .get(key) - .is_some_and(|row_ids| row_ids.contains(&row_id)) - { - deleted.insert(row_id); - } - } - _ => { - return Err(DbError::internal( - "runtime BTREE key type did not match the runtime index representation", - )); - } - } - Ok(()) - } - - pub(super) fn mark_row_ids_deleted(&mut self, row_ids: I) - where - I: IntoIterator, - { - match self { - Self::UniqueEncoded(_, deleted) - | Self::NonUniqueEncoded(_, deleted) - | Self::UniqueInt64(_, deleted) - | Self::NonUniqueInt64(_, deleted) - | Self::UniqueUuid(_, deleted) - | Self::NonUniqueUuid(_, deleted) => { - deleted.extend(row_ids); - } - } - } - - pub(crate) fn total_row_id_count(&self) -> usize { - match self { - Self::UniqueEncoded(keys, deleted) => keys.len().saturating_sub(deleted.len()), - Self::NonUniqueEncoded(keys, deleted) => keys - .values() - .map(|row_ids| { - row_ids - .iter() - .filter(|row_id| !deleted.contains(row_id)) - .count() - }) - .sum(), - Self::UniqueInt64(keys, deleted) => keys.len().saturating_sub(deleted.len()), - Self::NonUniqueInt64(keys, deleted) if deleted.is_empty() => { - keys.values().map(RuntimeInt64RowIds::len).sum() - } - Self::NonUniqueInt64(keys, deleted) => keys - .values() - .map(|row_ids| { - row_ids - .iter() - .filter(|row_id| !deleted.contains(row_id)) - .count() - }) - .sum(), - Self::UniqueUuid(keys, deleted) => keys.len().saturating_sub(deleted.len()), - Self::NonUniqueUuid(keys, deleted) => keys - .values() - .map(|row_ids| { - row_ids - .iter() - .filter(|row_id| !deleted.contains(row_id)) - .count() - }) - .sum(), - } - } - - pub(crate) fn distinct_key_count(&self) -> usize { - match self { - Self::UniqueEncoded(keys, deleted) => keys.len().saturating_sub(deleted.len()), - Self::NonUniqueEncoded(keys, deleted) => keys - .values() - .filter(|row_ids| row_ids.iter().any(|row_id| !deleted.contains(row_id))) - .count(), - Self::UniqueInt64(keys, deleted) => keys.len().saturating_sub(deleted.len()), - Self::NonUniqueInt64(keys, deleted) => keys - .values() - .filter(|row_ids| row_ids.iter().any(|row_id| !deleted.contains(&row_id))) - .count(), - Self::UniqueUuid(keys, deleted) => keys.len().saturating_sub(deleted.len()), - Self::NonUniqueUuid(keys, deleted) => keys - .values() - .filter(|row_ids| row_ids.iter().any(|row_id| !deleted.contains(row_id))) - .count(), - } - } - - #[cfg(test)] - fn is_empty(&self) -> bool { - match self { - Self::UniqueEncoded(keys, deleted) => keys.len() == deleted.len(), - Self::NonUniqueEncoded(keys, deleted) => keys - .values() - .all(|row_ids| row_ids.iter().all(|row_id| deleted.contains(row_id))), - Self::UniqueInt64(keys, deleted) => keys.len() == deleted.len(), - Self::NonUniqueInt64(keys, deleted) => keys - .values() - .all(|row_ids| row_ids.iter().all(|row_id| deleted.contains(&row_id))), - Self::UniqueUuid(keys, deleted) => keys.len() == deleted.len(), - Self::NonUniqueUuid(keys, deleted) => keys - .values() - .all(|row_ids| row_ids.iter().all(|row_id| deleted.contains(row_id))), - } - } -} - -#[derive(Clone, Debug)] -pub(crate) struct RuntimeCoveringPayloads { - columns: Vec, - rows: BTreeMap>, -} - -impl RuntimeCoveringPayloads { - fn new(columns: Vec) -> Self { - Self { - columns, - rows: BTreeMap::new(), - } - } - - fn column_position(&self, column_name: &str) -> Option { - self.columns - .iter() - .position(|candidate| identifiers_equal(candidate, column_name)) - } - - fn insert_row_values(&mut self, row_id: i64, values: Vec) { - self.rows.insert(row_id, values); - } - - fn remove_row_id(&mut self, row_id: i64) { - self.rows.remove(&row_id); - } - - fn shrink_to_fit(&mut self) -> usize { - let mut freed = 0usize; - let old_columns_capacity = self.columns.capacity(); - self.columns.shrink_to_fit(); - freed = freed.saturating_add( - old_columns_capacity - .saturating_sub(self.columns.capacity()) - .saturating_mul(std::mem::size_of::()), - ); - for values in self.rows.values_mut() { - let old_capacity = values.capacity(); - values.shrink_to_fit(); - freed = freed.saturating_add( - old_capacity - .saturating_sub(values.capacity()) - .saturating_mul(std::mem::size_of::()), - ); - } - freed - } - - fn project_row(&self, row_id: i64, offsets: &[usize]) -> Option { - let values = self.rows.get(&row_id)?; - let mut projected = Vec::with_capacity(offsets.len()); - for offset in offsets { - projected.push(values.get(*offset)?.clone()); - } - Some(QueryRow::new(projected)) - } -} - -#[derive(Clone, Debug)] -pub(crate) enum RuntimeIndex { - Btree { - keys: RuntimeBtreeKeys, - covering: Option, - }, - Trigram { - index: TrigramIndex, - }, - Spatial { - index: SpatialRuntimeIndex, - }, - FullText { - index: FullTextIndex, - }, -} - -impl RuntimeIndex { - fn shrink_to_fit_if_unique(&mut self) -> usize { - match self { - Self::Btree { keys, covering } => keys.shrink_to_fit_if_unique().saturating_add( - covering - .as_mut() - .map_or(0, RuntimeCoveringPayloads::shrink_to_fit), - ), - Self::Trigram { .. } | Self::Spatial { .. } | Self::FullText { .. } => 0, - } - } -} - -fn runtime_index_entry_count(index: &RuntimeIndex) -> usize { - match index { - RuntimeIndex::Btree { keys, .. } => keys.total_row_id_count(), - RuntimeIndex::Trigram { index } => index.entry_count(), - RuntimeIndex::Spatial { index } => index.len(), - RuntimeIndex::FullText { index } => index.entry_count(), - } -} - -#[derive(Debug)] -pub(super) enum PendingIndexInsert { - Btree { - name: String, - key: RuntimeBtreeKey, - row_id: i64, - covering_values: Option>, - }, - Trigram { - name: String, - row_id: u64, - text: String, - }, - Spatial { - name: String, - row_id: i64, - value: SpatialValue, - }, - FullText { - name: String, - row_id: u64, - fields: Vec>, - }, -} - -#[derive(Debug)] -pub(crate) struct EngineRuntime { - pub(crate) catalog: Arc, - pub(crate) tables: Arc>, - pub(crate) temp_tables: Arc>, - pub(crate) temp_table_data: Arc>>, - pub(crate) temp_views: Arc>, - pub(crate) temp_indexes: Arc>, - pub(crate) temp_schema_cookie: u32, - pub(crate) indexes: Arc>>, - pub(crate) persisted_tables: Arc>, - deferred_paged_row_locator_caches: Arc>>, - view_query_cache: Arc>, - resident_tombstone_locators: Arc>>>, - /// Tables whose row data has not yet been loaded from storage. - /// Populated during `decode_manifest_payload` and cleared by - /// `load_deferred_tables`. - pub(crate) deferred_tables: Arc>, - pub(crate) dirty_tables: Arc>, - pub(crate) paged_mutations: BTreeMap, - /// Per-session cache; capped at `cached_payloads_max_entries`. Eviction is LRU. - payload_cache: Arc>, - root_state: Option, - pub(crate) index_state_epoch: u64, - pub(crate) paged_row_storage: bool, - manifest_template: Option, - overflow_chain_caches: BTreeMap, - manifest_chain_cache: Option, - sync_capture_active: bool, - pub(crate) sync_mutations: Vec, - reactive_capture_active: bool, - pub(crate) reactive_mutations: Vec, - pub(crate) extension_trust_anchors: Arc>, - pub(crate) extension_unsigned_development_mode: bool, - pub(crate) audit_context: Arc>, - pub(crate) tracing: Option>, - fts_eval_context: Arc>, -} - -#[derive(Clone, Debug, Default)] -struct FtsEvalContext { - scores: BTreeMap<(String, i64), f64>, -} - -#[derive(Debug)] -pub(crate) struct PayloadCache { - entries: HashMap, - next_touch_gen: u64, - max_entries: usize, -} - -#[derive(Debug)] -struct PayloadCacheEntry { - payload: Arc>, - last_touch_gen: u64, -} - -impl PayloadCache { - fn new(max_entries: usize) -> Self { - Self { - entries: HashMap::new(), - next_touch_gen: 0, - max_entries, - } - } - - fn set_max_entries(&mut self, max_entries: usize) { - self.max_entries = max_entries; - self.evict_excess(); - } - - fn get(&mut self, table_name: &str) -> Option>> { - let payload = Arc::clone(&self.entries.get(table_name)?.payload); - self.touch(table_name); - Some(payload) - } - - fn take(&mut self, table_name: &str) -> Option>> { - self.entries.remove(table_name).map(|entry| entry.payload) - } - - fn insert(&mut self, table_name: String, payload: Arc>) { - if self.max_entries == 0 { - return; - } - let last_touch_gen = self.advance_touch_gen(); - self.entries.insert( - table_name, - PayloadCacheEntry { - payload, - last_touch_gen, - }, - ); - self.evict_excess(); - } - - fn remove(&mut self, table_name: &str) { - self.entries.remove(table_name); - } - - fn touch(&mut self, table_name: &str) { - let last_touch_gen = self.advance_touch_gen(); - if let Some(entry) = self.entries.get_mut(table_name) { - entry.last_touch_gen = last_touch_gen; - } - } - - fn evict_excess(&mut self) { - while self.entries.len() > self.max_entries { - if let Some(evicted) = self.oldest_key().cloned() { - self.entries.remove(&evicted); - } else { - break; - } - } - } - - fn advance_touch_gen(&mut self) -> u64 { - let touch_gen = self.next_touch_gen; - self.next_touch_gen = self.next_touch_gen.wrapping_add(1); - touch_gen - } - - fn oldest_key(&self) -> Option<&String> { - self.entries - .iter() - .min_by(|(_, left), (_, right)| { - if touch_gen_older(left.last_touch_gen, right.last_touch_gen) { - std::cmp::Ordering::Less - } else if touch_gen_older(right.last_touch_gen, left.last_touch_gen) { - std::cmp::Ordering::Greater - } else { - std::cmp::Ordering::Equal - } - }) - .map(|(key, _)| key) - } - - #[cfg(test)] - fn len(&self) -> usize { - self.entries.len() - } - - #[cfg(test)] - fn contains_key(&self, table_name: &str) -> bool { - self.entries.contains_key(table_name) - } - - #[cfg(test)] - fn last_touch_gen(&self, table_name: &str) -> Option { - self.entries - .get(table_name) - .map(|entry| entry.last_touch_gen) - } -} - -fn touch_gen_older(left: u64, right: u64) -> bool { - left != right && left.wrapping_sub(right) > (u64::MAX / 2) -} - -#[derive(Clone, Debug, PartialEq, Default)] -pub(crate) struct PagedMutationDelta { - pub(crate) append_count: usize, - pub(crate) updated_rows: BTreeMap>, - pub(crate) deleted_rows: BTreeSet, - pub(crate) original_rows: BTreeMap>, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct BulkLoadOptions { - pub batch_size: usize, - pub sync_interval: usize, - pub disable_indexes: bool, - pub checkpoint_on_complete: bool, -} - -impl Default for BulkLoadOptions { - fn default() -> Self { - Self { - batch_size: 1_000, - sync_interval: 1_000, - disable_indexes: false, - checkpoint_on_complete: true, - } - } -} - -impl Clone for EngineRuntime { - fn clone(&self) -> Self { - Self { - catalog: Arc::clone(&self.catalog), - tables: Arc::clone(&self.tables), - temp_tables: Arc::clone(&self.temp_tables), - temp_table_data: Arc::clone(&self.temp_table_data), - temp_views: Arc::clone(&self.temp_views), - temp_indexes: Arc::clone(&self.temp_indexes), - temp_schema_cookie: self.temp_schema_cookie, - indexes: Arc::clone(&self.indexes), - persisted_tables: Arc::clone(&self.persisted_tables), - deferred_paged_row_locator_caches: Arc::clone(&self.deferred_paged_row_locator_caches), - view_query_cache: Arc::clone(&self.view_query_cache), - resident_tombstone_locators: Arc::clone(&self.resident_tombstone_locators), - deferred_tables: Arc::clone(&self.deferred_tables), - // Preserve dirty state so that multi-statement transactions - // (clone-and-replace) do not lose modifications from earlier - // statements. `persist_to_db` clears dirty state after a - // successful persist, so autocommit paths are unaffected. - dirty_tables: Arc::clone(&self.dirty_tables), - // Escalate paged_mutations to full dirty on clone: the - // subsequent generic execution path may modify the same rows - // in ways that invalidate the splice assumption. - paged_mutations: BTreeMap::new(), - payload_cache: Arc::clone(&self.payload_cache), - root_state: self.root_state, - index_state_epoch: self.index_state_epoch, - paged_row_storage: self.paged_row_storage, - // These caches are keyed by persisted overflow pointers and remain - // valid across clone-and-replace write transactions until the - // corresponding table is rewritten. - manifest_template: None, - overflow_chain_caches: self.overflow_chain_caches.clone(), - manifest_chain_cache: None, - sync_capture_active: self.sync_capture_active, - sync_mutations: self.sync_mutations.clone(), - reactive_capture_active: self.reactive_capture_active, - reactive_mutations: self.reactive_mutations.clone(), - extension_trust_anchors: Arc::clone(&self.extension_trust_anchors), - extension_unsigned_development_mode: self.extension_unsigned_development_mode, - audit_context: Arc::clone(&self.audit_context), - tracing: self.tracing.as_ref().map(Arc::clone), - fts_eval_context: Arc::clone(&self.fts_eval_context), - } - } -} - -pub(crate) struct SimpleRowIdProjectionRequest<'a> { - pub(crate) table_name: &'a str, - pub(crate) projection_columns: &'a [&'a str], - pub(crate) filter_column: &'a str, - pub(crate) lookup_row_id: i64, - pub(crate) pager: &'a PagerHandle, - pub(crate) wal: &'a WalHandle, - pub(crate) snapshot_lsn: u64, - pub(crate) use_persistent_pk_index: bool, -} - -pub(crate) struct ResolvedSimpleRowIdProjectionRequest<'a> { - pub(crate) table_name: &'a str, - pub(crate) projection_indexes: &'a [usize], - pub(crate) column_names: Arc<[String]>, - pub(crate) lookup_row_id: i64, - pub(crate) pager: &'a PagerHandle, - pub(crate) wal: &'a WalHandle, - pub(crate) snapshot_lsn: u64, - pub(crate) use_persistent_pk_index: bool, -} - -pub(crate) struct ResolvedSimpleOrderedRowIdProjectionRequest<'a> { - pub(crate) table_name: &'a str, - pub(crate) order_column: &'a str, - pub(crate) projection_indexes: &'a [usize], - pub(crate) column_names: Arc<[String]>, - pub(crate) limit: Option, - pub(crate) offset: usize, - pub(crate) descending: bool, -} - -pub(crate) struct ResolvedSimpleRowIdRangeProjectionRequest<'a> { - pub(crate) table_name: &'a str, - pub(crate) projection_indexes: &'a [usize], - pub(crate) column_names: Arc<[String]>, - pub(crate) filter_column: &'a str, - pub(crate) lower_bound: Option, - pub(crate) upper_bound: Option, - pub(crate) limit: Option, - pub(crate) pager: &'a PagerHandle, - pub(crate) wal: &'a WalHandle, - pub(crate) snapshot_lsn: u64, - pub(crate) use_persistent_pk_index: bool, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum SimpleJoinProjectionSide { - Left, - Right, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) struct ResolvedSimpleJoinProjection { - pub(crate) side: SimpleJoinProjectionSide, - pub(crate) index: usize, -} - -pub(crate) struct ResolvedSimpleRowIdJoinProjectionRequest<'a> { - pub(crate) left_table_name: &'a str, - pub(crate) right_table_name: &'a str, - pub(crate) left_projection_indexes: &'a [usize], - pub(crate) right_projection_indexes: &'a [usize], - pub(crate) projections: &'a [ResolvedSimpleJoinProjection], - pub(crate) column_names: Arc<[String]>, - pub(crate) lookup_row_id: i64, - pub(crate) pager: &'a PagerHandle, - pub(crate) wal: &'a WalHandle, - pub(crate) snapshot_lsn: u64, - pub(crate) use_persistent_pk_index: bool, -} - -struct ValidatedSimpleRowIdProjectionRequest<'a> { - table_schema: &'a TableSchema, - projection_indexes: &'a [usize], - column_names: Arc<[String]>, - lookup_row_id: i64, - pager: &'a PagerHandle, - wal: &'a WalHandle, - snapshot_lsn: u64, - use_persistent_pk_index: bool, -} - -impl EngineRuntime { - #[must_use] - pub(crate) fn empty(schema_cookie: u32) -> Self { - let config = crate::config::DbConfig { - paged_row_storage: false, - ..crate::config::DbConfig::default() - }; - Self::from_config(schema_cookie, &config) - } - - #[must_use] - pub(crate) fn from_config(schema_cookie: u32, config: &crate::config::DbConfig) -> Self { - Self { - catalog: Arc::new(CatalogState::empty(schema_cookie)), - tables: Arc::new(BTreeMap::new()), - temp_tables: Arc::new(BTreeMap::new()), - temp_table_data: Arc::new(BTreeMap::new()), - temp_views: Arc::new(BTreeMap::new()), - temp_indexes: Arc::new(BTreeMap::new()), - temp_schema_cookie: 0, - indexes: Arc::new(BTreeMap::new()), - persisted_tables: Arc::new(BTreeMap::new()), - deferred_paged_row_locator_caches: Arc::new(BTreeMap::new()), - view_query_cache: Arc::new(Mutex::new(ViewQueryCache::default())), - resident_tombstone_locators: Arc::new(BTreeMap::new()), - deferred_tables: Arc::new(BTreeSet::new()), - dirty_tables: Arc::new(BTreeSet::new()), - paged_mutations: BTreeMap::new(), - payload_cache: Arc::new(Mutex::new(PayloadCache::new( - config.cached_payloads_max_entries, - ))), - root_state: None, - index_state_epoch: 0, - paged_row_storage: config.paged_row_storage, - manifest_template: None, - overflow_chain_caches: BTreeMap::new(), - manifest_chain_cache: None, - sync_capture_active: false, - sync_mutations: Vec::new(), - reactive_capture_active: false, - reactive_mutations: Vec::new(), - extension_trust_anchors: Arc::new(config.extension_trust_anchors.clone()), - extension_unsigned_development_mode: config.extension_unsigned_development_mode, - audit_context: Arc::new(Mutex::new(crate::security::AuditContext::default())), - tracing: None, - fts_eval_context: Arc::new(Mutex::new(FtsEvalContext::default())), - } - } - - pub(crate) fn set_audit_context_handle( - &mut self, - handle: Arc>, - ) { - self.audit_context = handle; - } - - pub(crate) fn set_sync_capture_active(&mut self, active: bool) { - self.sync_capture_active = active; - if !active { - self.sync_mutations.clear(); - } - } - - pub(crate) fn set_reactive_capture_active(&mut self, active: bool) { - self.reactive_capture_active = active; - if !active { - self.reactive_mutations.clear(); - } - } - - pub(crate) fn sync_capture_active(&self) -> bool { - self.sync_capture_active - } - - pub(crate) fn set_tracing(&mut self, tracing: Arc) { - self.tracing = Some(tracing); - } - - #[allow(dead_code)] - pub(crate) fn drain_index_usage(&self) { - let events = crate::tracing::index_usage::drain_local_index_usage(); - if events.is_empty() { - return; - } - if let Some(tracing) = self.tracing.as_ref() { - for (table_name, index_name, index_kind, kind) in events { - tracing.record_index_usage(&table_name, &index_name, &index_kind, kind); - } - } - } - - pub(crate) fn mutation_capture_active(&self) -> bool { - self.sync_capture_active || self.reactive_capture_active - } - - pub(crate) fn should_record_sync_mutation_for_table(&self, table: &TableSchema) -> bool { - (self.sync_capture_active || self.reactive_capture_active) - && !table.temporary - && !crate::sync::is_internal_table_name(&table.name) - } - - pub(crate) fn record_sync_mutation( - &mut self, - table_name: &str, - operation: crate::sync::SyncOperation, - primary_key: serde_json::Value, - after: Option, - schema_cookie: u32, - ) { - if self.sync_capture_active { - self.sync_mutations.push(crate::sync::SyncMutation { - table: table_name.to_string(), - operation, - primary_key: primary_key.clone(), - after: after.clone(), - schema_cookie, - }); - } - if self.reactive_capture_active { - self.reactive_mutations - .push(crate::reactive::RowChange::new( - table_name.to_string(), - crate::reactive::row_operation_from_sync(operation), - primary_key, - None, - after, - )); - } - } - - pub(crate) fn take_sync_mutations(&mut self) -> Vec { - std::mem::take(&mut self.sync_mutations) - } - - pub(crate) fn take_reactive_mutations(&mut self) -> Vec { - std::mem::take(&mut self.reactive_mutations) - } - - pub(super) fn bump_temp_schema_cookie(&mut self) { - self.temp_schema_cookie = self.temp_schema_cookie.wrapping_add(1); - if self.temp_schema_cookie == 0 { - self.temp_schema_cookie = 1; - } - } - - fn persistent_resolution_runtime(&self) -> Self { - let mut runtime = self.clone(); - runtime.temp_tables_mut().clear(); - runtime.temp_table_data_map_mut().clear(); - runtime.temp_views_mut().clear(); - runtime.temp_indexes_mut().clear(); - runtime.temp_schema_cookie = 0; - runtime - } - - fn cached_view_query(&self, view: &ViewSchema) -> Result> { - let key = ViewQueryCacheKey::new(view); - { - let cache = self - .view_query_cache - .lock() - .expect("view query cache lock should not be poisoned"); - if let Some(query) = cache.get(&key) { - return Ok(query); - } - } - - let view_statement = parse_sql_statement(&view.sql_text)?; - let Statement::Query(query) = view_statement else { - return Err(DbError::corruption(format!( - "view {} does not contain a SELECT statement", - view.name - ))); - }; - let query = Arc::new(query); - let mut cache = self - .view_query_cache - .lock() - .expect("view query cache lock should not be poisoned"); - Ok(cache.insert(key, query)) - } - - fn cache_view_query(&self, view: &ViewSchema, query: Query) { - self.view_query_cache - .lock() - .expect("view query cache lock should not be poisoned") - .insert(ViewQueryCacheKey::new(view), Arc::new(query)); - } - - fn catalog_mut(&mut self) -> &mut CatalogState { - Arc::make_mut(&mut self.catalog) - } - - fn tables_mut(&mut self) -> &mut BTreeMap { - Arc::make_mut(&mut self.tables) - } - - fn temp_table_data_map_mut(&mut self) -> &mut BTreeMap> { - Arc::make_mut(&mut self.temp_table_data) - } - - /// Per-entry COW: locates `name` in `tables` (case-insensitive) and - /// returns a `&mut TableData` for the targeted table, cloning only that - /// table's row vector when the per-entry `Arc` is shared. - /// This avoids deep-cloning every other table's rows during writes. - fn entry_table_data_mut(&mut self, name: &str) -> Option<&mut TableData> { - let canonical = map_key_ci(self.tables.as_ref(), name)?; - let map = self.tables_mut(); - let entry = map.get_mut(&canonical)?; - Some(entry.resident_data_mut()) - } - - fn entry_table_row_source_mut(&mut self, name: &str) -> Option<&mut TableRowSource> { - let canonical = map_key_ci(self.tables.as_ref(), name)?; - self.tables_mut().get_mut(&canonical) - } - - fn entry_temp_table_data_mut(&mut self, name: &str) -> Option<&mut TableData> { - let canonical = map_key_ci(self.temp_table_data.as_ref(), name)?; - let map = self.temp_table_data_map_mut(); - let entry = map.get_mut(&canonical)?; - Some(Arc::make_mut(entry)) - } - - fn temp_tables_mut(&mut self) -> &mut BTreeMap { - Arc::make_mut(&mut self.temp_tables) - } - - fn temp_views_mut(&mut self) -> &mut BTreeMap { - Arc::make_mut(&mut self.temp_views) - } - - fn temp_indexes_mut(&mut self) -> &mut BTreeMap { - Arc::make_mut(&mut self.temp_indexes) - } - - fn indexes_mut(&mut self) -> &mut BTreeMap> { - Arc::make_mut(&mut self.indexes) - } - - fn persisted_tables_mut(&mut self) -> &mut BTreeMap { - Arc::make_mut(&mut self.persisted_tables) - } - - fn deferred_paged_row_locator_caches_mut( - &mut self, - ) -> &mut BTreeMap> { - Arc::make_mut(&mut self.deferred_paged_row_locator_caches) - } - - fn resident_tombstone_locators_mut(&mut self) -> &mut BTreeMap>> { - Arc::make_mut(&mut self.resident_tombstone_locators) - } - - fn deferred_tables_mut(&mut self) -> &mut BTreeSet { - Arc::make_mut(&mut self.deferred_tables) - } - - fn dirty_tables_mut(&mut self) -> &mut BTreeSet { - Arc::make_mut(&mut self.dirty_tables) - } - - /// Read-only access to a runtime index by name (case-sensitive). - pub(crate) fn index(&self, name: &str) -> Option<&RuntimeIndex> { - self.indexes.get(name).map(|arc| arc.as_ref()) - } - - /// Targeted copy-on-write access to a single runtime index entry. - /// - /// Performs `Arc::make_mut` only on the targeted entry (and on the outer - /// map), so unrelated indexes are not cloned even when the runtime is - /// shared with concurrent readers. - pub(crate) fn index_mut(&mut self, name: &str) -> Option<&mut RuntimeIndex> { - let map = Arc::make_mut(&mut self.indexes); - map.get_mut(name).map(Arc::make_mut) - } - - pub(crate) fn compact_dirty_resident_storage_after_transaction_commit(&mut self) -> usize { - if self.dirty_tables.is_empty() { - return 0; - } - - let dirty_tables = self.dirty_tables.iter().cloned().collect::>(); - let mut freed = 0usize; - { - let tables = Arc::make_mut(&mut self.tables); - for table_name in &dirty_tables { - if let Some(row_source) = tables.get_mut(table_name) { - freed = freed.saturating_add(row_source.shrink_resident_to_fit_if_unique()); - } - } - } - - let dirty_index_names = self - .catalog - .indexes - .values() - .filter(|index| { - dirty_tables - .iter() - .any(|table_name| identifiers_equal(table_name, &index.table_name)) - }) - .map(|index| index.name.clone()) - .collect::>(); - if !dirty_index_names.is_empty() { - let indexes = Arc::make_mut(&mut self.indexes); - for index_name in dirty_index_names { - if let Some(index) = indexes.get_mut(&index_name).and_then(Arc::get_mut) { - freed = freed.saturating_add(index.shrink_to_fit_if_unique()); - } - } - } - - freed - } - - fn cached_payload(&mut self, table_name: &str) -> Option>> { - self.payload_cache - .lock() - .expect("payload cache lock should not be poisoned") - .get(table_name) - } - - fn cached_payload_take(&mut self, table_name: &str) -> Option>> { - self.payload_cache - .lock() - .expect("payload cache lock should not be poisoned") - .take(table_name) - } - - fn cache_payload_insert(&mut self, table_name: String, payload: Arc>) { - self.payload_cache - .lock() - .expect("payload cache lock should not be poisoned") - .insert(table_name, payload); - } - - fn cache_payload_remove(&mut self, table_name: &str) { - self.payload_cache - .lock() - .expect("payload cache lock should not be poisoned") - .remove(table_name); - } - - fn cache_deferred_paged_row_locators( - &mut self, - table_name: &str, - state: PersistedTableState, - chunks: &[TablePageManifestChunk], - ) -> Result<()> { - let cache = build_deferred_paged_row_locator_cache(state, chunks)?; - self.deferred_paged_row_locator_caches_mut() - .insert(table_name.to_string(), Arc::new(cache)); - Ok(()) - } - - #[cfg(test)] - pub(crate) fn has_deferred_paged_row_locator_cache_for_tests(&self, table_name: &str) -> bool { - self.deferred_paged_row_locator_caches - .contains_key(table_name) - } - - #[cfg(test)] - pub(crate) fn deferred_paged_row_locator_cache_is_dense_for_tests( - &self, - table_name: &str, - ) -> Option { - let canonical = map_key_ci(self.deferred_paged_row_locator_caches.as_ref(), table_name)?; - self.deferred_paged_row_locator_caches - .get(&canonical) - .map(|cache| cache.locators.is_dense()) - } - - #[cfg(test)] - pub(crate) fn deferred_paged_row_locator_cache_sparse_len_for_tests( - &self, - table_name: &str, - ) -> Option { - let canonical = map_key_ci(self.deferred_paged_row_locator_caches.as_ref(), table_name)?; - self.deferred_paged_row_locator_caches - .get(&canonical) - .map(|cache| cache.locators.sparse_len()) - } - - fn should_cache_deferred_paged_row_locators(&self, table_name: &str) -> bool { - let has_runtime_btree = self.catalog.indexes.values().any(|index| { - identifiers_equal(&index.table_name, table_name) - && index.fresh - && index.kind == IndexKind::Btree - && matches!(self.index(&index.name), Some(RuntimeIndex::Btree { .. })) - }); - has_runtime_btree - || self - .catalog - .table(table_name) - .and_then(row_id_alias_column_name) - .is_some() - } - - fn refresh_paged_lookup_cache_and_pk_index( - &mut self, - db: &crate::db::Db, - store: &DbTxnPageStore<'_>, - table_name: &str, - state: PersistedTableState, - ) -> Result> { - let needs_locator_cache = self.should_cache_deferred_paged_row_locators(table_name); - if !db.config().persistent_pk_index && !needs_locator_cache { - self.deferred_paged_row_locator_caches_mut() - .remove(table_name); - return Ok(None); - } - - let chunk_payloads = read_paged_table_chunk_payloads(store, state)?; - self.refresh_paged_lookup_cache_and_pk_index_from_chunks( - db, - table_name, - state, - &chunk_payloads, - ) - } - - fn refresh_paged_lookup_cache_and_pk_index_from_chunks( - &mut self, - db: &crate::db::Db, - table_name: &str, - state: PersistedTableState, - chunk_payloads: &[TablePageManifestChunk], - ) -> Result> { - let needs_locator_cache = self.should_cache_deferred_paged_row_locators(table_name); - if needs_locator_cache { - self.cache_deferred_paged_row_locators(table_name, state, chunk_payloads)?; - } else { - self.deferred_paged_row_locator_caches_mut() - .remove(table_name); - } - - if db.config().persistent_pk_index { - build_persistent_pk_index_root_from_chunk_payloads(db, chunk_payloads) - } else { - Ok(None) - } - } - - pub(crate) fn load_from_storage( - pager: &PagerHandle, - wal: &WalHandle, - schema_cookie: u32, - config: &crate::config::DbConfig, - ) -> Result<(Self, u64)> { - let reader = wal.begin_reader_with_pager(pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - let runtime = - Self::load_from_storage_at_snapshot(pager, wal, schema_cookie, config, snapshot_lsn)?; - drop(reader); - Ok((runtime, snapshot_lsn)) - } - - pub(crate) fn load_from_storage_at_snapshot( - pager: &PagerHandle, - wal: &WalHandle, - schema_cookie: u32, - config: &crate::config::DbConfig, - snapshot_lsn: u64, - ) -> Result { - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - let root_page = store.read_page(page::CATALOG_ROOT_PAGE_ID)?; - let root = decode_root_header(&root_page)?; - let mut runtime = if let Some(root) = root { - let payload = if root.pointer.logical_len == 0 || root.pointer.head_page_id == 0 { - Vec::new() - } else { - read_overflow(&store, root.pointer)? - }; - if crc32c_parts(&[payload.as_slice()]) != root.payload_checksum { - return Err(DbError::corruption("catalog state checksum mismatch")); - } - let mut runtime = if payload.is_empty() { - Self::from_config(root.schema_cookie, config) - } else if payload.starts_with(LEGACY_RUNTIME_PAYLOAD_MAGIC) { - let mut runtime = decode_runtime_payload(&payload)?; - runtime.mark_all_tables_dirty(); - runtime - } else if payload.starts_with(MANIFEST_PAYLOAD_MAGIC) { - decode_manifest_payload(&store, &payload)? - } else { - return Err(DbError::corruption("unknown catalog state payload magic")); - }; - let root_schema_cookie = root.schema_cookie; - runtime.root_state = Some(root); - runtime.catalog_mut().schema_cookie = root_schema_cookie; - runtime.paged_row_storage = config.paged_row_storage; - runtime.extension_trust_anchors = Arc::new(config.extension_trust_anchors.clone()); - runtime.extension_unsigned_development_mode = - config.extension_unsigned_development_mode; - runtime - } else { - Self::from_config(schema_cookie, config) - }; - runtime - .payload_cache - .lock() - .expect("payload cache lock should not be poisoned") - .set_max_entries(config.cached_payloads_max_entries); - if runtime.root_state.is_none() { - runtime.catalog_mut().schema_cookie = schema_cookie; - } - // Materialize any deferred tables under the same reader guard so - // that overflow pointers from the manifest are read against the - // same WAL snapshot. If deferred loading uses a later snapshot, - // a concurrent writer may have extended the overflow chain, causing - // a length mismatch. - // - // ADR 0143 Phase B (opt-in): when - // `DbConfig::defer_table_materialization` is true we intentionally - // skip the eager materialize+rebuild here so that `Db::open` does - // not allocate `Vec` for every persisted table. The - // per-statement/transaction lazy-load path in `db.rs` now pins a - // single reader snapshot across both the manifest refresh and the - // overflow payload read so first-use materialization does not mix - // snapshots under concurrent checkpoints. - if !runtime.deferred_tables.is_empty() && !config.defer_table_materialization { - runtime.materialize_deferred_tables_with_store(&store, pager.page_size(), None)?; - } - if !config.defer_table_materialization || runtime.deferred_tables.is_empty() { - runtime.rebuild_indexes(pager.page_size())?; - } - Ok(runtime) - } - - /// Returns `true` when one or more tables still have their row data - /// deferred (not yet loaded from storage). - #[must_use] - pub(crate) fn has_deferred_tables(&self) -> bool { - !self.deferred_tables.is_empty() - } - - /// Returns an iterator over deferred table names. - #[allow(clippy::double_must_use)] - #[must_use] - pub(crate) fn deferred_table_names(&self) -> impl Iterator { - self.deferred_tables.iter() - } - - /// Returns per-table residency stats `(table_name, row_count, heap_bytes)` - /// for every fully-loaded table in this runtime. Deferred (not-yet-loaded) - /// tables are omitted. Used by `Db::inspect_storage_state_json` per - /// ADR 0143 Phase A. Result is sorted by table name for deterministic - /// output. - #[must_use] - #[allow(dead_code)] // exposed for follow-up Phase A JSON breakdown wiring - pub(crate) fn table_memory_breakdown(&self) -> Vec<(String, usize, usize)> { - let mut out = Vec::with_capacity(self.tables.len()); - for (name, data) in self.tables.iter() { - out.push(( - name.clone(), - data.row_count(), - data.approximate_heap_bytes(), - )); - } - out - } - - /// Returns aggregate `(total_rows, total_heap_bytes, table_count, - /// deferred_table_count)` across all loaded tables. Deferred tables - /// contribute zero to the byte/row totals but are counted separately. - /// Per ADR 0143 Phase A. - #[must_use] - pub(crate) fn table_memory_totals(&self) -> (u64, u64, u32, u32) { - let mut rows: u64 = 0; - let mut bytes: u64 = 0; - for data in self.tables.values() { - rows = rows.saturating_add(data.row_count() as u64); - bytes = bytes.saturating_add(data.approximate_heap_bytes() as u64); - } - let table_count = u32::try_from(self.tables.len()).unwrap_or(u32::MAX); - let deferred_count = u32::try_from(self.deferred_tables.len()).unwrap_or(u32::MAX); - (rows, bytes, table_count, deferred_count) - } - - /// Materializes all deferred table data from storage, then rebuilds - /// indexes. After this call `deferred_tables` is empty and the runtime - /// is fully populated. - pub(crate) fn load_deferred_tables( - &mut self, - pager: &PagerHandle, - wal: &WalHandle, - page_size: u32, - ) -> Result<()> { - self.load_deferred_tables_with_snapshot(pager, wal, page_size, None, None) - } - - pub(crate) fn load_deferred_tables_at_snapshot( - &mut self, - pager: &PagerHandle, - wal: &WalHandle, - page_size: u32, - snapshot_lsn: u64, - ) -> Result<()> { - self.load_deferred_tables_with_snapshot(pager, wal, page_size, None, Some(snapshot_lsn)) - } - - /// Loads a subset of deferred tables, specified by name. - /// - /// This is used for per-table on-demand loading where only the tables - /// referenced by the current SQL statement are materialized. - #[allow(dead_code)] - pub(crate) fn load_deferred_tables_filtered( - &mut self, - pager: &PagerHandle, - wal: &WalHandle, - page_size: u32, - filter: &BTreeSet, - ) -> Result<()> { - self.load_deferred_tables_with_snapshot(pager, wal, page_size, Some(filter), None) - } - - pub(crate) fn load_deferred_tables_filtered_at_snapshot( - &mut self, - pager: &PagerHandle, - wal: &WalHandle, - page_size: u32, - filter: &BTreeSet, - snapshot_lsn: u64, - ) -> Result<()> { - self.load_deferred_tables_with_snapshot( - pager, - wal, - page_size, - Some(filter), - Some(snapshot_lsn), - ) - } - - pub(crate) fn load_deferred_table_row_sources_filtered( - &mut self, - pager: &PagerHandle, - wal: &WalHandle, - page_size: u32, - filter: &BTreeSet, - ) -> Result<()> { - self.load_deferred_table_row_sources_with_snapshot( - pager, - wal, - page_size, - Some(filter), - None, - ) - } - - pub(crate) fn load_deferred_table_row_sources_at_snapshot( - &mut self, - pager: &PagerHandle, - wal: &WalHandle, - page_size: u32, - snapshot_lsn: u64, - ) -> Result<()> { - self.load_deferred_table_row_sources_with_snapshot( - pager, - wal, - page_size, - None, - Some(snapshot_lsn), - ) - } - - pub(crate) fn load_deferred_table_row_sources( - &mut self, - pager: &PagerHandle, - wal: &WalHandle, - page_size: u32, - ) -> Result<()> { - self.load_deferred_table_row_sources_with_snapshot(pager, wal, page_size, None, None) - } - - fn load_deferred_table_row_sources_with_snapshot( - &mut self, - pager: &PagerHandle, - wal: &WalHandle, - page_size: u32, - filter: Option<&BTreeSet>, - snapshot_lsn: Option, - ) -> Result<()> { - if self.deferred_tables.is_empty() { - return Ok(()); - } - let Some(snapshot_lsn) = snapshot_lsn else { - let reader = wal.begin_reader_with_pager(pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - self.materialize_deferred_table_row_sources_with_store(&store, page_size, filter)?; - drop(reader); - return Ok(()); - }; - - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - self.materialize_deferred_table_row_sources_with_store(&store, page_size, filter)?; - Ok(()) - } - - pub(crate) fn load_deferred_table_row_sources_filtered_at_snapshot( - &mut self, - pager: &PagerHandle, - wal: &WalHandle, - page_size: u32, - filter: &BTreeSet, - snapshot_lsn: u64, - ) -> Result<()> { - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - self.materialize_deferred_table_row_sources_with_store(&store, page_size, Some(filter)) - } - - pub(crate) fn hydrate_deferred_runtime_index_at_snapshot( - &mut self, - pager: &PagerHandle, - wal: &WalHandle, - page_size: u32, - table_name: &str, - index_name: &str, - snapshot_lsn: u64, - ) -> Result<()> { - let Some(canonical_table_name) = self - .deferred_tables - .iter() - .find(|deferred| identifiers_equal(deferred, table_name)) - .cloned() - else { - return Ok(()); - }; - - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - let state = *self - .persisted_tables - .get(&canonical_table_name) - .ok_or_else(|| { - DbError::internal(format!( - "deferred table '{canonical_table_name}' has no persisted state" - )) - })?; - let table_schema = self - .catalog - .table(&canonical_table_name) - .ok_or_else(|| { - DbError::internal(format!( - "deferred table '{canonical_table_name}' has no schema" - )) - })? - .clone(); - let index_schema = self - .catalog - .index(index_name) - .ok_or_else(|| DbError::sql(format!("unknown index {index_name}")))? - .clone(); - let cache_key = DeferredRuntimeBtreeIndexCacheKey::new(&table_schema, &index_schema, state); - if let Some(entry) = cached_deferred_runtime_btree_index(&cache_key)? { - self.indexes_mut() - .insert(index_schema.name.clone(), Arc::clone(&entry.runtime_index)); - if let Some(locator_cache) = entry.paged_locator_cache.as_ref() { - self.deferred_paged_row_locator_caches_mut() - .insert(canonical_table_name, Arc::clone(locator_cache)); - } - return Ok(()); - } - - let row_source = if state.pointer.is_table_paged_manifest() { - TableRowSource::Paged(Arc::new(read_table_page_manifest_from_state( - &store, state, - )?)) - } else { - TableRowSource::Resident(Arc::new(decode_persisted_table_data(&store, state)?)) - }; - let row_count = row_source.row_count(); - if let Some(ps) = self.persisted_tables_mut().get_mut(&canonical_table_name) { - ps.row_count = row_count; - ps.tail = read_uncompressed_overflow_tail(&store, ps.pointer)?.unwrap_or_default(); - } - self.tables_mut() - .insert(canonical_table_name.clone(), row_source); - self.deferred_tables_mut().remove(&canonical_table_name); - if !self.indexes.contains_key(&index_schema.name) { - self.rebuild_index(&index_schema.name, page_size)?; - } - if state.pointer.is_table_paged_manifest() { - let Some(TableRowSource::Paged(manifest)) = self.tables.get(&canonical_table_name) - else { - return Err(DbError::internal(format!( - "paged row source for {canonical_table_name} is missing after index hydration" - ))); - }; - let chunks = Arc::clone(&manifest.chunks); - self.cache_deferred_paged_row_locators(&canonical_table_name, state, chunks.as_ref())?; - } - if let Some(runtime_index) = self.indexes.get(&index_schema.name).cloned() { - let paged_locator_cache = self - .deferred_paged_row_locator_caches - .get(&canonical_table_name) - .cloned(); - cache_deferred_runtime_btree_index( - cache_key, - DeferredRuntimeBtreeIndexCacheEntry { - runtime_index, - paged_locator_cache, - }, - )?; - } - let _ = self.redefer_persisted_tables(&[canonical_table_name.as_str()]); - Ok(()) - } - - fn load_deferred_tables_with_snapshot( - &mut self, - pager: &PagerHandle, - wal: &WalHandle, - page_size: u32, - filter: Option<&BTreeSet>, - snapshot_lsn: Option, - ) -> Result<()> { - if self.deferred_tables.is_empty() { - return Ok(()); - } - let Some(snapshot_lsn) = snapshot_lsn else { - let reader = wal.begin_reader_with_pager(pager)?; - let snapshot_lsn = reader.snapshot_lsn(); - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - self.materialize_deferred_tables_with_store(&store, page_size, filter)?; - drop(reader); - return Ok(()); - }; - - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - self.materialize_deferred_tables_with_store(&store, page_size, filter)?; - Ok(()) - } - - /// Loads deferred tables using an existing `SnapshotPageStore`. - /// - /// This ensures the overflow pointers recorded in `persisted_tables` - /// (from the manifest) are read against the same WAL snapshot that - /// produced those pointers, avoiding length mismatches when a - /// concurrent writer extends the overflow chain. - /// - /// When `filter` is `Some`, only the named tables are materialized. - /// When `None`, all deferred tables are materialized (legacy behavior). - fn materialize_deferred_tables_with_store( - &mut self, - store: &S, - page_size: u32, - filter: Option<&BTreeSet>, - ) -> Result<()> { - let table_names: Vec = if let Some(f) = filter { - self.deferred_tables - .iter() - .filter(|table_name| { - f.iter() - .any(|filter_name| filter_name.eq_ignore_ascii_case(table_name)) - }) - .cloned() - .collect() - } else { - self.deferred_tables.iter().cloned().collect() - }; - if table_names.is_empty() { - return Ok(()); - } - for table_name in &table_names { - let state = *self.persisted_tables.get(table_name).ok_or_else(|| { - DbError::internal(format!( - "deferred table '{table_name}' has no persisted state" - )) - })?; - let data = decode_persisted_table_data(store, state)?; - - if let Some(ps) = self.persisted_tables_mut().get_mut(table_name) { - ps.row_count = data.row_count(); - ps.tail = read_uncompressed_overflow_tail(store, ps.pointer)?.unwrap_or_default(); - } - self.tables_mut().insert(table_name.clone(), data.into()); - self.deferred_tables_mut().remove(table_name); - } - self.rebuild_stale_indexes(page_size)?; - Ok(()) - } - - fn materialize_deferred_table_row_sources_with_store( - &mut self, - store: &S, - page_size: u32, - filter: Option<&BTreeSet>, - ) -> Result<()> { - let table_names: Vec = if let Some(f) = filter { - self.deferred_tables - .iter() - .filter(|table_name| { - f.iter() - .any(|filter_name| filter_name.eq_ignore_ascii_case(table_name)) - }) - .cloned() - .collect() - } else { - self.deferred_tables.iter().cloned().collect() - }; - if table_names.is_empty() { - return Ok(()); - } - for table_name in &table_names { - let state = *self.persisted_tables.get(table_name).ok_or_else(|| { - DbError::internal(format!( - "deferred table '{table_name}' has no persisted state" - )) - })?; - let row_source = if state.pointer.is_table_paged_manifest() { - TableRowSource::Paged(Arc::new(read_table_page_manifest_from_state(store, state)?)) - } else { - TableRowSource::Resident(Arc::new(decode_persisted_table_data(store, state)?)) - }; - let row_count = row_source.row_count(); - if let Some(ps) = self.persisted_tables_mut().get_mut(table_name) { - ps.row_count = row_count; - ps.tail = read_uncompressed_overflow_tail(store, ps.pointer)?.unwrap_or_default(); - } - self.tables_mut().insert(table_name.clone(), row_source); - self.deferred_tables_mut().remove(table_name); - } - self.rebuild_stale_indexes(page_size)?; - Ok(()) - } - - pub(crate) fn persist_to_db(&mut self, db: &crate::db::Db) -> Result<()> { - let old_root = self.root_state; - let schema_cookie_changed = - old_root.is_none_or(|root| root.schema_cookie != self.catalog.schema_cookie); - let dirty_tables = if self.persisted_tables.is_empty() { - self.catalog.tables.keys().cloned().collect::>() - } else { - self.dirty_tables.iter().cloned().collect::>() - }; - let removed_tables = self - .persisted_tables - .keys() - .filter(|table_name| self.catalog.table(table_name).is_none()) - .cloned() - .collect::>(); - - { - let mut store = DbTxnPageStore { db }; - for table_name in dirty_tables { - let Some(table) = self.catalog.table(&table_name) else { - continue; - }; - let canonical_table_name = table.name.clone(); - let delta = self - .paged_mutations - .get(&canonical_table_name) - .cloned() - .unwrap_or_default(); - let previous_state = self - .persisted_tables - .get(&canonical_table_name) - .copied() - .unwrap_or_default(); - let previous_pointer = previous_state.pointer; - let resident_tombstone_locators = self - .resident_tombstone_locators - .get(&canonical_table_name) - .cloned(); - let row_source = - self.tables - .get(&canonical_table_name) - .cloned() - .ok_or_else(|| { - DbError::internal(format!("table data for {table_name} is missing")) - })?; - let mut use_paged_row_storage = - db.config().paged_row_storage || previous_pointer.is_table_paged_manifest(); - if db.config().paged_row_storage && !previous_pointer.is_table_paged_manifest() { - use_paged_row_storage = match &row_source { - TableRowSource::Resident(data) => resident_table_should_use_paged_storage( - data, - previous_state, - &delta, - db.config().page_size, - )?, - TableRowSource::Paged(manifest) => { - manifest.chunks.len() > 1 - || manifest.chunks.first().is_some_and(|chunk| { - chunk.payload.len() - > paged_table_target_chunk_bytes(db.config().page_size) - }) - } - }; - } - let resident_update_only = !delta.updated_rows.is_empty() - && delta.deleted_rows.is_empty() - && delta.append_count == 0; - let resident_delete_only = delta.updated_rows.is_empty() - && !delta.deleted_rows.is_empty() - && delta.append_count == 0; - let cached_payload = if (!use_paged_row_storage - || !previous_pointer.is_table_paged_manifest()) - && !resident_update_only - && !resident_delete_only - { - self.cached_payload(&canonical_table_name) - } else { - None - }; - if let Some(manifest) = row_source.paged_manifest() { - self.overflow_chain_caches.remove(&canonical_table_name); - if delta.append_count > 0 - && delta.updated_rows.is_empty() - && delta.deleted_rows.is_empty() - && manifest.tombstoned_row_ids.is_empty() - && !db.config().persistent_pk_index - { - if let Some((new_state, persisted_chunks)) = - try_append_only_paged_table_from_manifest( - &mut store, - previous_state, - manifest, - )? - { - self.persisted_tables_mut() - .insert(canonical_table_name.clone(), new_state); - let pk_index_root = self - .refresh_paged_lookup_cache_and_pk_index_from_chunks( - db, - &canonical_table_name, - new_state, - &persisted_chunks, - )?; - replace_table_pk_index_root( - self, - db, - &canonical_table_name, - pk_index_root, - )?; - let persisted_manifest = table_page_manifest_with_persisted_chunks( - manifest, - &persisted_chunks, - ); - self.replace_table_row_source( - &canonical_table_name, - TableRowSource::Paged(Arc::new(persisted_manifest)), - )?; - self.cache_payload_remove(&canonical_table_name); - continue; - } - } - let (new_state, persisted_chunks) = - rewrite_paged_table_from_manifest(&mut store, previous_state, manifest)?; - self.persisted_tables_mut() - .insert(canonical_table_name.clone(), new_state); - if new_state == previous_state { - if db.config().persistent_pk_index { - replace_table_pk_index_root( - self, - db, - &canonical_table_name, - previous_state.pk_index_root, - )?; - } - let persisted_manifest = - table_page_manifest_with_persisted_chunks(manifest, &persisted_chunks); - self.replace_table_row_source( - &canonical_table_name, - TableRowSource::Paged(Arc::new(persisted_manifest)), - )?; - self.cache_payload_remove(&canonical_table_name); - continue; - } - let pk_index_root = self.refresh_paged_lookup_cache_and_pk_index_from_chunks( - db, - &canonical_table_name, - new_state, - &persisted_chunks, - )?; - replace_table_pk_index_root(self, db, &canonical_table_name, pk_index_root)?; - let persisted_manifest = - table_page_manifest_with_persisted_chunks(manifest, &persisted_chunks); - self.replace_table_row_source( - &canonical_table_name, - TableRowSource::Paged(Arc::new(persisted_manifest)), - )?; - self.cache_payload_remove(&canonical_table_name); - continue; - } - if use_paged_row_storage && previous_pointer.is_table_paged_manifest() { - self.overflow_chain_caches.remove(&canonical_table_name); - if delta.append_count > 0 - && delta.updated_rows.is_empty() - && delta.deleted_rows.is_empty() - && !row_source.has_tombstoned_rows() - { - let data = row_source.resident_data(); - let existing_count = data.rows.len().saturating_sub(delta.append_count); - let appended_chunks = encode_paged_table_chunks_from_rows( - &data.rows[existing_count..], - db.config().page_size, - )?; - let new_state = if !appended_chunks.is_empty() { - append_paged_table_chunks( - &mut store, - previous_state, - &appended_chunks, - data.row_count(), - )? - } else { - previous_state - }; - self.persisted_tables_mut() - .insert(canonical_table_name.clone(), new_state); - let pk_index_root = self.refresh_paged_lookup_cache_and_pk_index( - db, - &store, - &canonical_table_name, - new_state, - )?; - replace_table_pk_index_root( - self, - db, - &canonical_table_name, - pk_index_root, - )?; - self.cache_payload_remove(&canonical_table_name); - continue; - } - } - let data = row_source.resident_data(); - if delta.append_count > 0 - && delta.updated_rows.is_empty() - && delta.deleted_rows.is_empty() - && previous_pointer.head_page_id != 0 - && !use_paged_row_storage - && !previous_pointer.is_compressed() - && !data.has_tombstoned_rows() - { - let existing_count = data.rows.len().saturating_sub(delta.append_count); - if existing_count <= data.rows.len() { - let appended_rows = encode_appended_table_rows(data, existing_count)?; - if !appended_rows.is_empty() { - if !db.config().persistent_pk_index { - let row_count = data.row_count(); - let row_count_bytes = u32::try_from(row_count) - .map_err(|_| { - DbError::constraint("table row count exceeds u32") - })? - .to_le_bytes(); - let (ptr, checksum, new_chain_cache, tail) = - append_uncompressed_with_first_page_patch( - &mut store, - previous_pointer, - TABLE_PAYLOAD_MAGIC.len(), - &row_count_bytes, - &appended_rows, - )?; - self.overflow_chain_caches - .insert(canonical_table_name.clone(), new_chain_cache); - let row_count = data.row_count(); - self.persisted_tables_mut().insert( - canonical_table_name.clone(), - PersistedTableState { - pointer: ptr, - checksum, - row_count, - tail, - pk_index_root: previous_state.pk_index_root, - }, - ); - replace_table_pk_index_root(self, db, &canonical_table_name, None)?; - self.cache_payload_remove(&canonical_table_name); - continue; - } - - let new_payload = if let Some(cached) = cached_payload { - let previous = Arc::try_unwrap(cached) - .unwrap_or_else(|arc| arc.as_slice().to_vec()); - append_encoded_rows_to_table_payload( - previous, - data.row_count(), - &appended_rows, - )? - } else { - let previous_payload = read_overflow(&store, previous_pointer)?; - append_encoded_rows_to_table_payload( - previous_payload, - data.row_count(), - &appended_rows, - )? - }; - let checksum = crc32c_parts(&[new_payload.as_slice()]); - let ptr = rewrite_overflow( - &mut store, - previous_pointer, - &new_payload, - CompressionMode::Never, - )?; - let new_chain_cache = - build_overflow_chain_cache(&store, ptr.head_page_id)?; - let tail = - read_uncompressed_overflow_tail(&store, ptr)?.unwrap_or_default(); - self.overflow_chain_caches - .insert(canonical_table_name.clone(), new_chain_cache); - let row_count = data.row_count(); - self.persisted_tables_mut().insert( - canonical_table_name.clone(), - PersistedTableState { - pointer: ptr, - checksum, - row_count, - tail, - pk_index_root: previous_state.pk_index_root, - }, - ); - if db.config().persistent_pk_index { - let pk_index_root = - build_persistent_pk_index_root(db, new_payload.as_slice())?; - replace_table_pk_index_root( - self, - db, - &canonical_table_name, - pk_index_root, - )?; - } else { - replace_table_pk_index_root(self, db, &canonical_table_name, None)?; - } - self.cache_payload_insert( - canonical_table_name.clone(), - Arc::new(new_payload), - ); - continue; - } - } - } - - if use_paged_row_storage { - self.overflow_chain_caches.remove(&canonical_table_name); - let new_state = if delta.append_count > 0 - && delta.updated_rows.is_empty() - && delta.deleted_rows.is_empty() - && !data.has_tombstoned_rows() - { - let existing_count = data.rows.len().saturating_sub(delta.append_count); - let appended_chunks = encode_paged_table_chunks_from_rows( - &data.rows[existing_count..], - db.config().page_size, - )?; - if !appended_chunks.is_empty() { - append_paged_table_chunks( - &mut store, - previous_state, - &appended_chunks, - data.row_count(), - )? - } else { - rewrite_paged_table_from_resident( - &mut store, - previous_state, - data, - db.config().page_size, - )? - } - } else if delta.updated_rows.is_empty() && !delta.deleted_rows.is_empty() { - // Delete-only delta: avoid decoding values for chunks that - // contain no deleted rows by scanning row ids only. - rewrite_paged_table_from_resident_delete_only( - &mut store, - previous_state, - data, - db.config().page_size, - &delta.deleted_rows, - )? - } else { - rewrite_paged_table_from_resident( - &mut store, - previous_state, - data, - db.config().page_size, - )? - }; - self.persisted_tables_mut() - .insert(canonical_table_name.clone(), new_state); - let pk_index_root = self.refresh_paged_lookup_cache_and_pk_index( - db, - &store, - &canonical_table_name, - new_state, - )?; - replace_table_pk_index_root(self, db, &canonical_table_name, pk_index_root)?; - self.cache_payload_remove(&canonical_table_name); - continue; - } - - // Choose the encoding path: - // 1. Row-update splice: only re-encode modified rows using cached payload - // 2. Row-delete splice: copy unchanged encoded rows from previous payload - // 3. Append-only: read old payload, append new rows - // 4. Full re-encode: encode every row from scratch - let mut resident_tombstone_locators_preserved = false; - let (payload, dirty_byte_ranges, pk_locator_preserved) = if !delta - .updated_rows - .is_empty() - && delta.deleted_rows.is_empty() - && delta.append_count == 0 - { - let mut dirty_indices = Vec::with_capacity(delta.updated_rows.len()); - for row_id in delta.updated_rows.keys() { - if let Some(idx) = data.row_index_by_id(*row_id) { - dirty_indices.push(idx); - } - } - dirty_indices.sort_unstable(); - - if let Some(cached) = self.cached_payload_take(&canonical_table_name) { - match Arc::try_unwrap(cached) { - Ok(mut payload) => { - if let Some(dirty_range) = splice_updated_rows_payload_in_place( - &mut payload, - data, - &dirty_indices, - )? { - ( - payload, - single_dirty_range( - dirty_range.first_dirty_byte - ..dirty_range.last_dirty_byte, - ), - true, - ) - } else { - let splice = splice_updated_rows_payload( - payload.as_slice(), - data, - &dirty_indices, - )?; - let first = splice.first_dirty_byte; - let last = splice.last_dirty_byte; - ( - splice.payload, - single_dirty_range(first..last), - splice.pk_locator_preserved, - ) - } - } - Err(cached) => { - let splice = splice_updated_rows_payload( - cached.as_slice(), - data, - &dirty_indices, - )?; - let first = splice.first_dirty_byte; - let last = splice.last_dirty_byte; - ( - splice.payload, - single_dirty_range(first..last), - splice.pk_locator_preserved, - ) - } - } - } else if previous_pointer.head_page_id != 0 { - let mut payload = read_overflow(&store, previous_pointer)?; - if let Some(dirty_range) = splice_updated_rows_payload_in_place( - &mut payload, - data, - &dirty_indices, - )? { - ( - payload, - single_dirty_range( - dirty_range.first_dirty_byte..dirty_range.last_dirty_byte, - ), - true, - ) - } else { - let splice = splice_updated_rows_payload( - payload.as_slice(), - data, - &dirty_indices, - )?; - let first = splice.first_dirty_byte; - let last = splice.last_dirty_byte; - ( - splice.payload, - single_dirty_range(first..last), - splice.pk_locator_preserved, - ) - } - } else { - let payload = encode_table_payload(data)?; - let last = payload.len(); - (payload, single_dirty_range(0..last), false) - } - } else if !delta.deleted_rows.is_empty() - && delta.updated_rows.is_empty() - && delta.append_count == 0 - { - if !db.config().paged_row_storage - && !db.config().persistent_pk_index - && previous_pointer.head_page_id != 0 - && !previous_pointer.is_compressed() - { - if let (Some(cached), Some(locators)) = ( - self.cached_payload_take(&canonical_table_name), - resident_tombstone_locators.as_deref(), - ) { - let mut payload = Arc::try_unwrap(cached) - .unwrap_or_else(|arc| arc.as_slice().to_vec()); - if let Some((dirty_ranges, checksum)) = - tombstone_deleted_rows_cached_payload_by_locator( - &mut payload, - &delta.deleted_rows, - locators, - previous_state.checksum, - )? - { - let chain_cache = - match self.overflow_chain_caches.get(&canonical_table_name) { - Some(cache) => cache.clone(), - None => build_overflow_chain_cache( - &store, - previous_pointer.head_page_id, - )?, - }; - let (pointer, new_chain_cache, tail) = - rewrite_overflow_cached_with_dirty_byte_ranges( - &mut store, - previous_pointer, - &payload, - &chain_cache.page_ids, - 0, - Some(dirty_ranges.as_slice()), - )?; - self.overflow_chain_caches - .insert(canonical_table_name.clone(), new_chain_cache); - self.persisted_tables_mut().insert( - canonical_table_name.clone(), - PersistedTableState { - pointer, - checksum, - row_count: data.row_count(), - tail, - pk_index_root: previous_state.pk_index_root, - }, - ); - replace_table_pk_index_root(self, db, &canonical_table_name, None)?; - self.cache_payload_insert( - canonical_table_name.clone(), - Arc::new(payload), - ); - continue; - } - self.cache_payload_insert( - canonical_table_name.clone(), - Arc::new(payload), - ); - } - } - - if !db.config().paged_row_storage - && !db.config().persistent_pk_index - && previous_pointer.head_page_id != 0 - && !previous_pointer.is_compressed() - { - if let Some(cached) = self.cached_payload_take(&canonical_table_name) { - let mut payload = Arc::try_unwrap(cached) - .unwrap_or_else(|arc| arc.as_slice().to_vec()); - if let Some(dirty_ranges) = truncate_tail_deleted_rows_payload( - &mut payload, - &delta.deleted_rows, - data.row_count(), - )? { - let checksum = crc32c_parts(&[payload.as_slice()]); - let chain_cache = - match self.overflow_chain_caches.get(&canonical_table_name) { - Some(cache) => cache.clone(), - None => build_overflow_chain_cache( - &store, - previous_pointer.head_page_id, - )?, - }; - let (pointer, new_chain_cache, tail) = - rewrite_overflow_cached_with_dirty_byte_ranges( - &mut store, - previous_pointer, - &payload, - &chain_cache.page_ids, - 0, - Some(dirty_ranges.as_slice()), - )?; - self.overflow_chain_caches - .insert(canonical_table_name.clone(), new_chain_cache); - self.persisted_tables_mut().insert( - canonical_table_name.clone(), - PersistedTableState { - pointer, - checksum, - row_count: data.row_count(), - tail, - pk_index_root: previous_state.pk_index_root, - }, - ); - replace_table_pk_index_root(self, db, &canonical_table_name, None)?; - self.resident_tombstone_locators_mut() - .remove(&canonical_table_name); - self.cache_payload_insert( - canonical_table_name.clone(), - Arc::new(payload), - ); - continue; - } - self.cache_payload_insert( - canonical_table_name.clone(), - Arc::new(payload), - ); - } - } - - if !db.config().paged_row_storage - && !db.config().persistent_pk_index - && previous_pointer.head_page_id != 0 - && !previous_pointer.is_compressed() - { - let chain_cache = - match self.overflow_chain_caches.get(&canonical_table_name) { - Some(cache) => Some(cache.clone()), - None => Some(build_overflow_chain_cache( - &store, - previous_pointer.head_page_id, - )?), - }; - if let (Some(locators), Some(chain_cache)) = - (resident_tombstone_locators.as_deref(), chain_cache.as_ref()) - { - let sparse_result = tombstone_deleted_rows_overflow_by_locator( - &mut store, - previous_state, - chain_cache, - &delta.deleted_rows, - locators, - data.row_count(), - )?; - if let Some((mut new_state, new_chain_cache)) = sparse_result { - new_state.pk_index_root = None; - self.persisted_tables_mut() - .insert(canonical_table_name.clone(), new_state); - self.overflow_chain_caches - .insert(canonical_table_name.clone(), new_chain_cache); - replace_table_pk_index_root(self, db, &canonical_table_name, None)?; - self.cache_payload_remove(&canonical_table_name); - continue; - } - } - } - - // ADR 0200: obtain the previous on-disk payload (cached - // or read back) so a delete can be applied as in-place - // tombstones instead of shifting every surviving byte. - let previous_payload: Option> = - if let Some(cached) = self.cached_payload_take(&canonical_table_name) { - match Arc::try_unwrap(cached) { - Ok(payload) => Some(payload), - Err(shared) => Some(shared.as_ref().clone()), - } - } else if previous_pointer.head_page_id != 0 { - Some(read_overflow(&store, previous_pointer)?) - } else { - None - }; - - match previous_payload { - Some(mut payload) => { - // Prefer in-place tombstones unless the table has - // become heavily fragmented, in which case a full - // re-encode of the live resident rows reclaims the - // accumulated dead slots. - let physical = - read_table_payload_row_count_from_bytes(&payload).unwrap_or(0); - let live = data.row_count(); - let dead_after = physical.saturating_sub(live); - // ADR 0200: in-place delete tombstones apply - // only to the resident single-payload storage - // form (`paged_row_storage = false`, e.g. the - // embedded_fast / tuned_durable profiles). When - // `paged_row_storage` is enabled, a resident - // payload can be promoted to a paged manifest by - // later writes/checkpoints, and mixing the two - // representations is unsafe, so those profiles - // keep the compacting splice path. - let tombstoned = - if !db.config().paged_row_storage && live > 0 && dead_after <= live - { - if let Some(locators) = resident_tombstone_locators.as_ref() { - match tombstone_deleted_rows_payload_by_locator( - &mut payload, - &delta.deleted_rows, - locators, - )? { - Some(dirty) => { - resident_tombstone_locators_preserved = true; - Some(dirty) - } - None => tombstone_deleted_rows_payload_in_place( - &mut payload, - &delta.deleted_rows, - )?, - } - } else { - tombstone_deleted_rows_payload_in_place( - &mut payload, - &delta.deleted_rows, - )? - } - } else { - None - }; - if let Some(dirty) = tombstoned { - (payload, dirty, false) - } else if dead_after == 0 { - // No pre-existing tombstones: the surviving - // rows still map 1:1 to the payload, so the - // byte-shifting splice is valid and compacts. - if let Some(dirty_range) = splice_deleted_rows_payload_in_place( - &mut payload, - data, - &delta.deleted_rows, - )? { - (payload, dirty_range, false) - } else { - let splice = splice_deleted_rows_payload( - payload.as_slice(), - data, - &delta.deleted_rows, - )?; - let first = splice.first_dirty_byte; - let last = splice.last_dirty_byte; - (splice.payload, single_dirty_range(first..last), false) - } - } else { - // Fragmented payload (pre-existing tombstones): - // re-encode the authoritative live rows. - let payload = encode_table_payload(data)?; - let last = payload.len(); - (payload, single_dirty_range(0..last), false) - } - } - None => { - let payload = encode_table_payload(data)?; - let last = payload.len(); - (payload, single_dirty_range(0..last), false) - } - } - } else if delta.append_count > 0 - && delta.updated_rows.is_empty() - && delta.deleted_rows.is_empty() - && previous_pointer.head_page_id != 0 - && !data.has_tombstoned_rows() - { - let previous_payload = read_overflow(&store, previous_pointer)?; - // ADR 0200: the append fast path assumes the previous - // payload's physical slot count equals the live row - // count before this append. If the payload carries - // delete tombstones (physical > live), that assumption - // is false, so re-encode the live rows (which also - // reclaims the tombstone slots). - let previous_physical = - read_table_payload_row_count_from_bytes(&previous_payload).unwrap_or(0); - let expected_prior_live = data.row_count().saturating_sub(delta.append_count); - if previous_physical == expected_prior_live { - let payload = append_table_payload(previous_payload, data)?; - let last = payload.len(); - (payload, single_dirty_range(0..last), false) - } else { - let payload = encode_table_payload(data)?; - let last = payload.len(); - (payload, single_dirty_range(0..last), false) - } - } else { - let payload = encode_table_payload(data)?; - let last = payload.len(); - (payload, single_dirty_range(0..last), false) - }; - - let checksum = crc32c_parts(&[payload.as_slice()]); - let (pointer, new_chain_cache, tail) = if let Some(chain_cache) = - self.overflow_chain_caches.get(&canonical_table_name) - { - let page_size = db.config().page_size as usize; - let chunk_cap = page_size.saturating_sub(OVERFLOW_HEADER_SIZE); - let skip = if chunk_cap == 0 { - 0 - } else { - dirty_byte_ranges - .iter() - .map(|range| range.start.checked_div(chunk_cap).unwrap_or(0)) - .min() - .unwrap_or(0) - }; - rewrite_overflow_cached_with_dirty_byte_ranges( - &mut store, - previous_pointer, - &payload, - &chain_cache.page_ids, - skip, - Some(dirty_byte_ranges.as_slice()), - )? - } else { - let ptr = rewrite_overflow( - &mut store, - previous_pointer, - &payload, - CompressionMode::Never, - )?; - let cache = build_overflow_chain_cache(&store, ptr.head_page_id)?; - let tail = read_uncompressed_overflow_tail(&store, ptr)?.unwrap_or_default(); - (ptr, cache, tail) - }; - self.overflow_chain_caches - .insert(canonical_table_name.clone(), new_chain_cache); - let row_count = data.row_count(); - self.persisted_tables_mut().insert( - canonical_table_name.clone(), - PersistedTableState { - pointer, - checksum, - row_count, - tail, - pk_index_root: previous_state.pk_index_root, - }, - ); - let pk_index_root = if db.config().persistent_pk_index && pk_locator_preserved { - previous_state.pk_index_root - } else if db.config().persistent_pk_index { - build_persistent_pk_index_root(db, payload.as_slice())? - } else { - None - }; - replace_table_pk_index_root(self, db, &canonical_table_name, pk_index_root)?; - if use_paged_row_storage || pointer.head_page_id == 0 { - self.resident_tombstone_locators_mut() - .remove(&canonical_table_name); - } else if resident_tombstone_locators_preserved { - // Offsets are unchanged by in-place tombstone patches. - } else if resident_delete_only { - self.resident_tombstone_locators_mut() - .remove(&canonical_table_name); - } else { - let locators = build_resident_tombstone_locators_from_payload(&payload)?; - self.resident_tombstone_locators_mut() - .insert(canonical_table_name.clone(), Arc::new(locators)); - } - self.cache_payload_insert(canonical_table_name, Arc::new(payload)); - } - for table_name in removed_tables { - let Some(state) = self.persisted_tables_mut().remove(&table_name) else { - continue; - }; - self.overflow_chain_caches.remove(&table_name); - self.deferred_paged_row_locator_caches_mut() - .remove(&table_name); - self.resident_tombstone_locators_mut().remove(&table_name); - self.cache_payload_remove(&table_name); - free_persisted_table_bytes(&mut store, state)?; - if state.pk_index_root.is_some() { - free_table_btree(&mut store, state.pk_index_root)?; - } - } - } - - let (checksum, pointer) = { - // Take the chain cache to avoid overlapping borrows with - // manifest_payload (which mutates self.manifest_template). - let chain_cache = self.manifest_chain_cache.take(); - let manifest = self.manifest_payload()?; - let checksum = crc32c_parts(&[manifest]); - let previous_manifest_pointer = old_root.map_or( - OverflowPointer { - head_page_id: 0, - logical_len: 0, - flags: 0, - }, - |root| root.pointer, - ); - let pointer = { - let mut store = DbTxnPageStore { db }; - if let Some(chain_cache) = chain_cache { - let (ptr, new_cache, _tail) = rewrite_overflow_cached( - &mut store, - previous_manifest_pointer, - manifest, - &chain_cache.page_ids, - 0, - )?; - self.manifest_chain_cache = Some(new_cache); - ptr - } else { - let ptr = rewrite_overflow( - &mut store, - previous_manifest_pointer, - manifest, - CompressionMode::Never, - )?; - let cache = build_overflow_chain_cache(&store, ptr.head_page_id)?; - self.manifest_chain_cache = Some(cache); - ptr - } - }; - (checksum, pointer) - }; - - let root_page = encode_root_header( - db.config().page_size, - RootHeader { - schema_cookie: self.catalog.schema_cookie, - payload_checksum: checksum, - pointer, - }, - ); - db.write_page_owned(page::CATALOG_ROOT_PAGE_ID, root_page)?; - self.dirty_tables_mut().clear(); - self.paged_mutations.clear(); - self.root_state = Some(RootHeader { - schema_cookie: self.catalog.schema_cookie, - payload_checksum: checksum, - pointer, - }); - if schema_cookie_changed { - db.set_schema_cookie(self.catalog.schema_cookie)?; - } - Ok(()) - } - - pub(crate) fn has_checkpoint_compaction_candidates( - &self, - store: &S, - _config: &crate::config::DbConfig, - ) -> Result { - for state in self.persisted_tables.values() { - if state.pointer.head_page_id == 0 { - continue; - } - if state.pointer.is_table_paged_manifest() { - if paged_table_state_needs_checkpoint_compaction(store, *state)? { - return Ok(true); - } - continue; - } - if state.pk_index_root.is_none() - && !state.pointer.is_compressed() - && usize::try_from(state.pointer.logical_len) - .ok() - .is_some_and(|len| len >= AUTO_MIN_PAYLOAD_BYTES) - { - return Ok(true); - } - } - Ok(self.root_state.is_some_and(|root| { - root.pointer.head_page_id != 0 - && !root.pointer.is_compressed() - && usize::try_from(root.pointer.logical_len) - .ok() - .is_some_and(|len| len >= AUTO_MIN_PAYLOAD_BYTES) - })) - } - - pub(crate) fn prepare_resident_payload_offset_caches( - &mut self, - store: &S, - config: &crate::config::DbConfig, - ) -> Result { - if !preserve_resident_payload_offsets_for_delete_tombstones(config) { - return Ok(false); - } - let table_names = self - .persisted_tables - .iter() - .filter_map(|(table_name, state)| { - if state.pointer.head_page_id != 0 - && !state.pointer.is_table_paged_manifest() - && !state.pointer.is_compressed() - && !self.overflow_chain_caches.contains_key(table_name) - { - Some((table_name.clone(), state.pointer.head_page_id)) - } else { - None - } - }) - .collect::>(); - if table_names.is_empty() { - return Ok(false); - } - for (table_name, head_page_id) in table_names { - let chain_cache = build_overflow_chain_cache(store, head_page_id)?; - self.overflow_chain_caches.insert(table_name, chain_cache); - } - Ok(true) - } - - pub(crate) fn backfill_missing_persistent_pk_index_for_table( - &mut self, - db: &crate::db::Db, - table_name: &str, - ) -> Result { - let Some(canonical_table_name) = self - .catalog - .tables - .keys() - .find(|candidate| identifiers_equal(candidate, table_name)) - .cloned() - else { - return Ok(false); - }; - let Some(table) = self.catalog.tables.get(&canonical_table_name) else { - return Ok(false); - }; - if table.pk_index_root.is_some() - || self - .persisted_tables - .get(&canonical_table_name) - .is_none_or(|state| state.pointer.head_page_id == 0) - { - return Ok(false); - } - self.backfill_missing_persistent_pk_index_for_canonical_table( - db, - canonical_table_name.as_str(), - ) - } - - fn backfill_missing_persistent_pk_index_for_canonical_table( - &mut self, - db: &crate::db::Db, - table_name: &str, - ) -> Result { - let Some(previous_state) = self.persisted_tables.get(table_name).copied() else { - return Ok(false); - }; - if self - .catalog - .tables - .get(table_name) - .is_some_and(|table| table.pk_index_root.is_some()) - || previous_state.pointer.head_page_id == 0 - { - return Ok(false); - } - - let mut store = DbTxnPageStore { db }; - if previous_state.pointer.is_table_paged_manifest() { - let chunk_payloads = read_paged_table_chunk_payloads(&store, previous_state)?; - let pk_index_root = - build_persistent_pk_index_root_from_chunk_payloads(db, &chunk_payloads)?; - replace_table_pk_index_root(self, db, table_name, pk_index_root)?; - return Ok(true); - } - - let payload = Arc::new(read_overflow(&store, previous_state.pointer)?); - let pointer = if previous_state.pointer.is_compressed() { - rewrite_overflow( - &mut store, - previous_state.pointer, - payload.as_slice(), - CompressionMode::Never, - )? - } else { - previous_state.pointer - }; - let tail = read_uncompressed_overflow_tail(&store, pointer)?.unwrap_or_default(); - let checksum = crc32c_parts(&[payload.as_slice()]); - self.persisted_tables_mut().insert( - table_name.to_string(), - PersistedTableState { - pointer, - checksum, - row_count: previous_state.row_count, - tail, - pk_index_root: previous_state.pk_index_root, - }, - ); - self.cache_payload_insert(table_name.to_string(), Arc::clone(&payload)); - self.overflow_chain_caches.remove(table_name); - let chain_cache = build_overflow_chain_cache(&store, pointer.head_page_id)?; - self.overflow_chain_caches - .insert(table_name.to_string(), chain_cache); - - let pk_index_root = build_persistent_pk_index_root(db, payload.as_slice())?; - replace_table_pk_index_root(self, db, table_name, pk_index_root)?; - Ok(true) - } - - pub(crate) fn backfill_paged_row_storage(&mut self, db: &crate::db::Db) -> Result { - let table_names = self - .catalog - .tables - .keys() - .filter_map(|table_name| { - self.persisted_tables - .get(table_name) - .filter(|state| { - state.pointer.head_page_id != 0 && !state.pointer.is_table_paged_manifest() - }) - .map(|_| table_name.clone()) - }) - .collect::>(); - if table_names.is_empty() { - return Ok(false); - } - - let mut changed = false; - let mut store = DbTxnPageStore { db }; - for table_name in table_names { - let Some(previous_state) = self.persisted_tables.get(&table_name).copied() else { - continue; - }; - let new_state = wrap_legacy_table_state_as_paged_manifest(&mut store, previous_state)?; - self.persisted_tables_mut() - .insert(table_name.clone(), new_state); - self.overflow_chain_caches.remove(&table_name); - if db.config().persistent_pk_index { - let chunk_payloads = read_paged_table_chunk_payloads(&store, new_state)?; - let pk_index_root = - build_persistent_pk_index_root_from_chunk_payloads(db, &chunk_payloads)?; - let manifest = TablePageManifest::from_chunks(chunk_payloads)?; - let payload = Arc::new(encode_legacy_table_payload_from_manifest(&manifest)?); - replace_table_pk_index_root(self, db, &table_name, pk_index_root)?; - self.cache_payload_insert(table_name.clone(), payload); - } - changed = true; - } - Ok(changed) - } - - pub(crate) fn compact_persisted_payloads_for_checkpoint( - &mut self, - db: &crate::db::Db, - ) -> Result { - let old_root = self.root_state; - let mut changed = false; - { - let mut store = DbTxnPageStore { db }; - let table_names = self.persisted_tables.keys().cloned().collect::>(); - for table_name in table_names { - let Some(previous_state) = self.persisted_tables.get(&table_name).copied() else { - continue; - }; - let previous_pointer = previous_state.pointer; - if previous_pointer.head_page_id == 0 { - continue; - } - if previous_pointer.is_table_paged_manifest() { - let (new_state, table_changed) = - compact_paged_table_state_for_checkpoint(&mut store, previous_state)?; - if table_changed { - self.persisted_tables_mut() - .insert(table_name.clone(), new_state); - if db.config().persistent_pk_index { - let chunk_payloads = - read_paged_table_chunk_payloads(&store, new_state)?; - let pk_index_root = build_persistent_pk_index_root_from_chunk_payloads( - db, - &chunk_payloads, - )?; - replace_table_pk_index_root(self, db, &table_name, pk_index_root)?; - } - changed = true; - } - continue; - } - if previous_state.pk_index_root.is_some() - || previous_pointer.is_compressed() - || usize::try_from(previous_pointer.logical_len) - .ok() - .is_none_or(|len| len < AUTO_MIN_PAYLOAD_BYTES) - { - continue; - } - let payload = if let Some(cached) = self.cached_payload(&table_name) { - cached - } else { - Arc::new(read_overflow(&store, previous_pointer)?) - }; - let pointer = rewrite_overflow( - &mut store, - previous_pointer, - payload.as_slice(), - CompressionMode::Auto, - )?; - if pointer != previous_pointer { - changed = true; - } - let tail = if pointer.is_compressed() { - OverflowTailInfo::default() - } else { - read_uncompressed_overflow_tail(&store, pointer)?.unwrap_or_default() - }; - self.persisted_tables_mut().insert( - table_name.clone(), - PersistedTableState { - pointer, - checksum: previous_state.checksum, - row_count: previous_state.row_count, - tail, - pk_index_root: previous_state.pk_index_root, - }, - ); - self.cache_payload_insert(table_name.clone(), payload); - self.overflow_chain_caches.remove(&table_name); - } - } - - let (checksum, pointer) = { - let manifest = self.manifest_payload()?; - let checksum = crc32c_parts(&[manifest]); - let previous_manifest_pointer = old_root.map_or( - OverflowPointer { - head_page_id: 0, - logical_len: 0, - flags: 0, - }, - |root| root.pointer, - ); - let pointer = { - let mut store = DbTxnPageStore { db }; - rewrite_overflow( - &mut store, - previous_manifest_pointer, - manifest, - CompressionMode::Auto, - )? - }; - (checksum, pointer) - }; - - let new_root = RootHeader { - schema_cookie: self.catalog.schema_cookie, - payload_checksum: checksum, - pointer, - }; - if old_root != Some(new_root) { - let root_page = encode_root_header(db.config().page_size, new_root); - db.write_page_owned(page::CATALOG_ROOT_PAGE_ID, root_page)?; - self.root_state = Some(new_root); - changed = true; - } - Ok(changed) - } - - fn manifest_payload(&mut self) -> Result<&[u8]> { - let use_template = - self.manifest_template.as_ref().is_some_and(|template| { - template.schema_cookie == self.catalog.schema_cookie - && template.table_next_row_id_offsets.len() == self.catalog.tables.len() - && template.table_state_offsets.len() == self.catalog.tables.len() - && template.table_pk_index_root_offsets.len() == self.catalog.tables.len() - && self.catalog.tables.keys().all(|table_name| { - template.table_next_row_id_offsets.contains_key(table_name) - }) - && self - .catalog - .tables - .keys() - .all(|table_name| template.table_state_offsets.contains_key(table_name)) - && self.catalog.tables.keys().all(|table_name| { - template - .table_pk_index_root_offsets - .contains_key(table_name) - }) - }); - - if !use_template { - let encoded = encode_manifest_payload_with_offsets(self, &self.persisted_tables)?; - self.manifest_template = Some(ManifestTemplate { - schema_cookie: self.catalog.schema_cookie, - table_next_row_id_offsets: encoded.table_next_row_id_offsets, - table_state_offsets: encoded.table_state_offsets, - table_pk_index_root_offsets: encoded.table_pk_index_root_offsets, - bytes: encoded.bytes, - }); - } - - let template = self - .manifest_template - .as_mut() - .ok_or_else(|| DbError::internal("manifest template was not initialized"))?; - for (table_name, offset) in &template.table_next_row_id_offsets { - let next_row_id = self - .catalog - .tables - .get(table_name) - .map(|table| table.next_row_id) - .ok_or_else(|| { - DbError::internal(format!( - "manifest next_row_id offset referenced unknown table {table_name}" - )) - })?; - patch_manifest_table_next_row_id(&mut template.bytes, *offset, next_row_id)?; - } - for (table_name, offset) in &template.table_state_offsets { - let state = self - .persisted_tables - .get(table_name) - .copied() - .unwrap_or_default(); - patch_manifest_table_state(&mut template.bytes, *offset, state)?; - } - for (table_name, offset) in &template.table_pk_index_root_offsets { - let pk_index_root = self - .catalog - .tables - .get(table_name) - .map(|table| table.pk_index_root) - .ok_or_else(|| { - DbError::internal(format!( - "manifest pk_index_root offset referenced unknown table {table_name}" - )) - })?; - patch_manifest_table_pk_index_root(&mut template.bytes, *offset, pk_index_root)?; - } - Ok(template.bytes.as_slice()) - } - - pub(super) fn planner_catalog(&self) -> CatalogState { - let mut catalog = self.catalog.as_ref().clone(); - for (name, table) in self.temp_tables.iter() { - catalog.views.remove(name); - catalog.tables.insert(name.clone(), table.clone()); - catalog - .indexes - .retain(|_, index| !identifiers_equal(&index.table_name, name)); - catalog - .triggers - .retain(|_, trigger| !identifiers_equal(&trigger.target_name, name)); - catalog.table_stats.remove(name); - } - for (name, view) in self.temp_views.iter() { - catalog.tables.remove(name); - catalog.views.insert(name.clone(), view.clone()); - catalog - .triggers - .retain(|_, trigger| !identifiers_equal(&trigger.target_name, name)); - } - catalog - } - - pub(super) fn temp_relation_exists(&self, name: &str) -> bool { - self.temp_table_schema(name).is_some() || self.temp_view(name).is_some() - } - - pub(super) fn temp_table_schema(&self, name: &str) -> Option<&TableSchema> { - match compat_schema_qualified_name(name) { - (Some(CompatSchemaQualifier::Main), _) => None, - (_, object) => map_get_ci(&self.temp_tables, object), - } - } - - pub(super) fn temp_table_schema_mut(&mut self, name: &str) -> Option<&mut TableSchema> { - let (qualifier, object) = compat_schema_qualified_name(name); - if qualifier == Some(CompatSchemaQualifier::Main) { - return None; - } - map_get_ci_mut(self.temp_tables_mut(), object) - } - - pub(super) fn temp_table_data(&self, name: &str) -> Option<&TableData> { - match compat_schema_qualified_name(name) { - (Some(CompatSchemaQualifier::Main), _) => None, - (_, object) => map_get_ci(&self.temp_table_data, object).map(|arc| arc.as_ref()), - } - } - - pub(super) fn temp_table_data_mut(&mut self, name: &str) -> Option<&mut TableData> { - let (qualifier, object) = compat_schema_qualified_name(name); - if qualifier == Some(CompatSchemaQualifier::Main) { - return None; - } - self.entry_temp_table_data_mut(object) - } - - pub(super) fn temp_view(&self, name: &str) -> Option<&ViewSchema> { - match compat_schema_qualified_name(name) { - (Some(CompatSchemaQualifier::Main), _) => None, - (_, object) => map_get_ci(&self.temp_views, object), - } - } - - pub(super) fn visible_view( - &self, - name: &str, - _scope: NameResolutionScope, - ) -> Option<&ViewSchema> { - let (qualifier, object) = compat_schema_qualified_name(name); - if qualifier == Some(CompatSchemaQualifier::Main) { - return self.catalog.view(object); - } - if let Some(view) = self.temp_view(name) { - return Some(view); - } - if self.temp_table_schema(name).is_some() { - return None; - } - if qualifier == Some(CompatSchemaQualifier::Temp) { - None - } else { - self.catalog.view(object) - } - } - - pub(super) fn visible_table_is_temporary(&self, name: &str) -> bool { - !self.temp_tables.is_empty() && self.temp_table_schema(name).is_some() - } - - pub(super) fn table_schema_in_scope( - &self, - name: &str, - _scope: NameResolutionScope, - ) -> Option<&TableSchema> { - let (qualifier, object) = compat_schema_qualified_name(name); - if qualifier == Some(CompatSchemaQualifier::Main) { - return self.catalog.table(object); - } - if self.temp_view(name).is_some() { - return None; - } - if let Some(table) = self.temp_table_schema(name) { - return Some(table); - } - if qualifier == Some(CompatSchemaQualifier::Temp) { - None - } else { - self.catalog.table(object) - } - } - - pub(super) fn table_schema(&self, name: &str) -> Option<&TableSchema> { - self.table_schema_in_scope(name, NameResolutionScope::Session) - } - - pub(super) fn catalog_table_mut(&mut self, name: &str) -> Option<&mut TableSchema> { - let (qualifier, object) = compat_schema_qualified_name(name); - if qualifier == Some(CompatSchemaQualifier::Temp) { - return None; - } - let catalog = self.catalog_mut(); - map_get_ci_mut(&mut catalog.tables, object) - } - - pub(super) fn canonical_catalog_table_name(&self, name: &str) -> Option { - let (qualifier, object) = compat_schema_qualified_name(name); - if qualifier == Some(CompatSchemaQualifier::Temp) { - return None; - } - self.catalog.table(object).map(|table| table.name.clone()) - } - - pub(super) fn table_data_in_scope( - &self, - name: &str, - _scope: NameResolutionScope, - ) -> Option<&TableData> { - let (qualifier, object) = compat_schema_qualified_name(name); - if qualifier == Some(CompatSchemaQualifier::Main) { - let table_name = self.catalog.table(object)?.name.clone(); - return self - .tables - .get(&table_name) - .map(TableRowSource::resident_data); - } - if self.temp_view(name).is_some() { - return None; - } - if let Some(table) = self.temp_table_schema(name) { - return self.temp_table_data(&table.name); - } - if qualifier == Some(CompatSchemaQualifier::Temp) { - None - } else { - let table_name = self.catalog.table(object)?.name.clone(); - self.tables - .get(&table_name) - .map(TableRowSource::resident_data) - } - } - - pub(super) fn table_row_source(&self, name: &str) -> Option<&TableRowSource> { - let (qualifier, object) = compat_schema_qualified_name(name); - if qualifier == Some(CompatSchemaQualifier::Temp) { - return None; - } - if self.temp_view(name).is_some() { - return None; - } - let table_name = self.catalog.table(object)?.name.clone(); - self.tables.get(&table_name) - } - - pub(super) fn prepared_insert_target_loaded(&self, table_name: &str) -> bool { - self.tables.contains_key(table_name) || self.temp_tables.contains_key(table_name) - } - - fn visible_table_row_source_in_scope( - &self, - name: &str, - _scope: NameResolutionScope, - ) -> Option> { - let (qualifier, object) = compat_schema_qualified_name(name); - if qualifier == Some(CompatSchemaQualifier::Main) { - let table_name = self.catalog.table(object)?.name.clone(); - return self - .tables - .get(&table_name) - .map(VisibleTableRowSource::Base); - } - if self.temp_view(name).is_some() { - return None; - } - if let Some(table) = self.temp_table_schema(name) { - return self - .temp_table_data(&table.name) - .map(VisibleTableRowSource::Temp); - } - if qualifier == Some(CompatSchemaQualifier::Temp) { - None - } else { - let table_name = self.catalog.table(object)?.name.clone(); - self.tables - .get(&table_name) - .map(VisibleTableRowSource::Base) - } - } - - fn visible_table_row_source(&self, name: &str) -> Option> { - self.visible_table_row_source_in_scope(name, NameResolutionScope::Session) - } - - pub(super) fn table_data(&self, name: &str) -> Option<&TableData> { - self.table_data_in_scope(name, NameResolutionScope::Session) - } - - pub(super) fn persisted_table_state(&self, name: &str) -> Option { - let table_name = self.catalog.table(name)?.name.clone(); - self.persisted_tables.get(&table_name).copied() - } - - pub(super) fn table_data_mut_in_scope( - &mut self, - name: &str, - _scope: NameResolutionScope, - ) -> Option<&mut TableData> { - let (qualifier, object) = compat_schema_qualified_name(name); - if qualifier == Some(CompatSchemaQualifier::Main) { - return self.entry_table_data_mut(object); - } - if self.temp_view(name).is_some() { - return None; - } - if self.temp_table_schema(name).is_some() { - return self.temp_table_data_mut(name); - } - if qualifier == Some(CompatSchemaQualifier::Temp) { - None - } else { - self.entry_table_data_mut(object) - } - } - - pub(super) fn table_data_mut(&mut self, name: &str) -> Option<&mut TableData> { - self.table_data_mut_in_scope(name, NameResolutionScope::Session) - } - - pub(super) fn replace_table_row_source( - &mut self, - name: &str, - row_source: TableRowSource, - ) -> Result<()> { - let Some(table_name) = self.canonical_catalog_table_name(name) else { - return Err(DbError::internal(format!( - "table row source for {name} is missing" - ))); - }; - self.tables_mut().insert(table_name, row_source); - Ok(()) - } - - pub(crate) fn redefer_persisted_tables(&mut self, names: &[&str]) -> usize { - let mut freed_bytes = 0usize; - for name in names { - let Some(table_name) = self.canonical_catalog_table_name(name) else { - continue; - }; - if self - .persisted_tables - .get(&table_name) - .is_some_and(|state| state.pointer.is_table_paged_manifest()) - { - if let Some(row_source) = self.tables_mut().remove(&table_name) { - freed_bytes = freed_bytes.saturating_add(row_source.approximate_heap_bytes()); - self.deferred_tables_mut().insert(table_name.clone()); - self.dirty_tables_mut().remove(&table_name); - self.paged_mutations.remove(&table_name); - } - } - } - freed_bytes - } - - pub(crate) fn has_redeferable_persisted_tables(&self, names: &[&str]) -> bool { - names.iter().any(|name| { - let Some(table_name) = self.canonical_catalog_table_name(name) else { - return false; - }; - self.persisted_tables - .get(&table_name) - .is_some_and(|state| state.pointer.is_table_paged_manifest()) - && self.tables.contains_key(&table_name) - }) - } - - pub(crate) fn redefer_all_persisted_paged_tables(&mut self) -> usize { - let paged_names: Vec = self - .persisted_tables - .iter() - .filter_map(|(name, state)| { - if state.pointer.is_table_paged_manifest() && self.tables.contains_key(name) { - Some(name.clone()) - } else { - None - } - }) - .collect(); - let name_refs: Vec<&str> = paged_names.iter().map(|s| s.as_str()).collect(); - self.redefer_persisted_tables(&name_refs) - } - - pub(crate) fn rebuild_indexes(&mut self, page_size: u32) -> Result<()> { - let indexes = self.catalog.indexes.values().cloned().collect::>(); - let mut rebuilt: BTreeMap> = BTreeMap::new(); - for index in indexes { - rebuilt.insert( - index.name.clone(), - Arc::new(build_runtime_index(&index, self, page_size)?), - ); - } - *self.indexes_mut() = rebuilt; - for index in self.catalog_mut().indexes.values_mut() { - index.fresh = true; - } - self.manifest_template = None; - self.index_state_epoch = self.index_state_epoch.wrapping_add(1); - Ok(()) - } - - pub(crate) fn rebuild_index(&mut self, name: &str, page_size: u32) -> Result<()> { - let index = self - .catalog - .index(name) - .cloned() - .ok_or_else(|| DbError::sql(format!("unknown index {name}")))?; - let rebuilt = build_runtime_index(&index, self, page_size)?; - self.indexes_mut() - .insert(name.to_string(), Arc::new(rebuilt)); - if let Some(index) = self.catalog_mut().indexes.get_mut(name) { - index.fresh = true; - } - self.manifest_template = None; - self.index_state_epoch = self.index_state_epoch.wrapping_add(1); - Ok(()) - } - - pub(crate) fn verify_index(&self, name: &str, page_size: u32) -> Result<()> { - self.catalog - .index(name) - .ok_or_else(|| DbError::sql(format!("unknown index {name}")))?; - let existing = self.index(name).map_or(0, runtime_index_entry_count); - let mut rebuilt = self.clone(); - rebuilt.rebuild_index(name, page_size)?; - let actual = rebuilt.index(name).map_or(0, runtime_index_entry_count); - if existing != actual { - return Err(DbError::corruption(format!( - "index {name} verification failed: expected {existing} entries, rebuilt {actual}" - ))); - } - Ok(()) - } - - pub(crate) fn rebuild_stale_indexes(&mut self, page_size: u32) -> Result<()> { - if self - .catalog - .indexes - .iter() - .all(|(name, index)| index.fresh && self.indexes.contains_key(name)) - { - return Ok(()); - } - - // ADR 0143 Phase B: under per-table deferred materialization, an - // index whose target table has not yet been loaded must not be - // rebuilt — its rows are not in memory. If the index was still - // resident from before the table was deferred, it remains valid; - // otherwise the index is rebuilt when the table itself is - // materialized and no longer appears in `deferred_tables`. - let names = self - .catalog - .indexes - .iter() - .filter(|(name, index)| !index.fresh || !self.indexes.contains_key(*name)) - .filter(|(_, index)| { - !self - .deferred_tables - .iter() - .any(|table_name| identifiers_equal(table_name, &index.table_name)) - }) - .map(|(name, _)| name.clone()) - .collect::>(); - for name in names { - self.rebuild_index(&name, page_size)?; - } - Ok(()) - } - - pub(super) fn mark_indexes_stale_for_table(&mut self, table_name: &str) { - if self.visible_table_is_temporary(table_name) { - return; - } - let Some(table_name) = self.canonical_catalog_table_name(table_name) else { - return; - }; - let index_names = self - .catalog - .indexes - .values() - .filter(|index| identifiers_equal(&index.table_name, &table_name)) - .map(|index| index.name.clone()) - .collect::>(); - self.mark_named_indexes_stale(&index_names); - } - - /// Mark only the named indexes (and their catalog entries) as stale, - /// discarding any in-memory runtime index for them. Used when a DML - /// successfully incrementally updates some indexes on a table but fails to - /// incrementally update others — the successful ones stay fresh and the - /// failed ones are rebuilt on next access. - pub(super) fn mark_named_indexes_stale(&mut self, index_names: &[String]) { - if index_names.is_empty() { - return; - } - let mut changed = false; - { - let catalog = self.catalog_mut(); - for name in index_names { - if let Some(index) = catalog.indexes.get_mut(name) { - if index.fresh { - index.fresh = false; - changed = true; - } - } - } - } - if changed { - self.manifest_template = None; - } - let indexes = self.indexes_mut(); - let mut any_removed = false; - for name in index_names { - if indexes.remove(name).is_some() { - any_removed = true; - } - } - if changed || any_removed { - self.index_state_epoch = self.index_state_epoch.wrapping_add(1); - } - } - - pub(super) fn mark_table_dirty(&mut self, table_name: &str) { - if self.visible_table_is_temporary(table_name) { - return; - } - let Some(table_name) = self.canonical_catalog_table_name(table_name) else { - return; - }; - self.catalog_mut().table_stats.remove(&table_name); - self.paged_mutations.remove(&table_name); - self.dirty_tables_mut().insert(table_name); - } - - pub(super) fn mark_table_row_dirty( - &mut self, - table_name: &str, - _row_index: usize, - row_id: i64, - values: &[Value], - ) { - if self.visible_table_is_temporary(table_name) { - return; - } - let Some(table_name) = self.canonical_catalog_table_name(table_name) else { - return; - }; - self.catalog_mut().table_stats.remove(&table_name); - if self.dirty_tables.contains(&table_name) - && !self.paged_mutations.contains_key(&table_name) - { - return; - } - self.dirty_tables_mut().insert(table_name.clone()); - self.paged_mutations - .entry(table_name) - .or_default() - .updated_rows - .insert(row_id, values.to_vec()); - } - - pub(super) fn mark_table_row_dirty_with_original_values( - &mut self, - table_name: &str, - _row_index: usize, - row_id: i64, - original_values: &[Value], - values: &[Value], - ) { - if self.visible_table_is_temporary(table_name) { - return; - } - let Some(table_name) = self.canonical_catalog_table_name(table_name) else { - return; - }; - self.catalog_mut().table_stats.remove(&table_name); - if self.dirty_tables.contains(&table_name) - && !self.paged_mutations.contains_key(&table_name) - { - return; - } - - let restores_original = self - .paged_mutations - .get(&table_name) - .and_then(|delta| delta.original_rows.get(&row_id)) - .is_some_and(|original| original == values); - - if restores_original || values == original_values { - if let Some(delta) = self.paged_mutations.get_mut(&table_name) { - delta.updated_rows.remove(&row_id); - delta.original_rows.remove(&row_id); - if delta.updated_rows.is_empty() - && delta.deleted_rows.is_empty() - && delta.append_count == 0 - { - self.paged_mutations.remove(&table_name); - self.dirty_tables_mut().remove(&table_name); - } - } - return; - } - - self.dirty_tables_mut().insert(table_name.clone()); - let delta = self.paged_mutations.entry(table_name.clone()).or_default(); - delta - .original_rows - .entry(row_id) - .or_insert_with(|| original_values.to_vec()); - delta.updated_rows.insert(row_id, values.to_vec()); - } - - pub(super) fn mark_table_row_deleted(&mut self, table_name: &str, row_id: i64) { - if self.visible_table_is_temporary(table_name) { - return; - } - let Some(table_name) = self.canonical_catalog_table_name(table_name) else { - return; - }; - self.catalog_mut().table_stats.remove(&table_name); - if self.dirty_tables.contains(&table_name) - && !self.paged_mutations.contains_key(&table_name) - { - return; - } - self.dirty_tables_mut().insert(table_name.clone()); - self.paged_mutations - .entry(table_name) - .or_default() - .deleted_rows - .insert(row_id); - } - - pub(super) fn mark_table_rows_deleted(&mut self, table_name: &str, row_ids: &BTreeSet) { - if row_ids.is_empty() || self.visible_table_is_temporary(table_name) { - return; - } - let Some(table_name) = self.canonical_catalog_table_name(table_name) else { - return; - }; - self.catalog_mut().table_stats.remove(&table_name); - if self.dirty_tables.contains(&table_name) - && !self.paged_mutations.contains_key(&table_name) - { - return; - } - self.dirty_tables_mut().insert(table_name.clone()); - let deleted_rows = &mut self - .paged_mutations - .entry(table_name) - .or_default() - .deleted_rows; - if deleted_rows.is_empty() { - *deleted_rows = row_ids.clone(); - } else { - deleted_rows.extend(row_ids.iter().copied()); - } - } - - pub(super) fn mark_table_row_appended(&mut self, table_name: &str) { - if self.visible_table_is_temporary(table_name) { - return; - } - if let Some(delta) = self.paged_mutations.get_mut(table_name) { - delta.append_count += 1; - return; - } - if self.dirty_tables.contains(table_name) { - return; - } - let Some(table_name) = self.canonical_catalog_table_name(table_name) else { - return; - }; - self.catalog_mut().table_stats.remove(&table_name); - if let Some(delta) = self.paged_mutations.get_mut(table_name.as_str()) { - delta.append_count += 1; - return; - } - if self.dirty_tables.contains(table_name.as_str()) { - return; - } - self.dirty_tables_mut().insert(table_name.clone()); - self.paged_mutations - .entry(table_name) - .or_default() - .append_count += 1; - } - - pub(super) fn mark_all_tables_dirty(&mut self) { - let table_names = self.catalog.tables.keys().cloned().collect::>(); - self.dirty_tables_mut().extend(table_names); - self.paged_mutations.clear(); - } - - pub(super) fn prepare_insert_index_updates( - &mut self, - table_name: &str, - row: &StoredRow, - page_size: u32, - ) -> Result> { - if self.visible_table_is_temporary(table_name) { - return Ok(Vec::new()); - } - let Some(canonical_table_name) = self.canonical_catalog_table_name(table_name) else { - return Ok(Vec::new()); - }; - let table = self - .table_schema(table_name) - .cloned() - .ok_or_else(|| DbError::sql(format!("unknown table {table_name}")))?; - let indexes = self - .catalog - .indexes - .values() - .filter(|index| { - identifiers_equal(&index.table_name, &canonical_table_name) && index.fresh - }) - .cloned() - .collect::>(); - let mut updates = Vec::new(); - - for index in indexes { - if !self.indexes.contains_key(&index.name) { - self.rebuild_index(&index.name, page_size)?; - } - - match index.kind { - IndexKind::Btree => { - let Some(key) = compute_index_key(self, &index, &table, &row.values)? else { - continue; - }; - if index.unique { - self.remove_tombstoned_unique_btree_entries_for_insert( - &canonical_table_name, - &index, - &key, - )?; - } - updates.push(PendingIndexInsert::Btree { - name: index.name.clone(), - key, - row_id: row.row_id, - covering_values: covering_payload_values_for_row( - &index, - &table, - &row.values, - ), - }); - } - IndexKind::Trigram => { - if !row_satisfies_index_predicate(self, &index, &table, &row.values)? { - continue; - } - let text = compute_index_values(self, &index, &table, &row.values)? - .into_iter() - .next() - .ok_or_else(|| { - DbError::constraint("trigram index requires a single text expression") - })?; - let Value::Text(text) = text else { - return Err(DbError::constraint( - "trigram index requires a single text expression", - )); - }; - updates.push(PendingIndexInsert::Trigram { - name: index.name.clone(), - row_id: row.row_id as u64, - text, - }); - } - IndexKind::Spatial => { - if let Some(value) = - spatial_index_value_for_row(self, &index, &table, &row.values)? - { - updates.push(PendingIndexInsert::Spatial { - name: index.name.clone(), - row_id: row.row_id, - value, - }); - } - } - IndexKind::FullText => { - if !row_satisfies_index_predicate(self, &index, &table, &row.values)? { - continue; - } - let fields = full_text_fields_for_row(self, &index, &table, &row.values)?; - updates.push(PendingIndexInsert::FullText { - name: index.name.clone(), - row_id: row.row_id as u64, - fields, - }); - } - } - } - - Ok(updates) - } - - fn remove_tombstoned_unique_btree_entries_for_insert( - &mut self, - table_name: &str, - index: &IndexSchema, - key: &RuntimeBtreeKey, - ) -> Result<()> { - let Some(row_source) = self.visible_table_row_source(table_name) else { - return Ok(()); - }; - if !row_source.has_tombstoned_rows() { - return Ok(()); - } - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { - return Ok(()); - }; - let mut stale_row_ids = Vec::new(); - for row_id in keys.row_ids_for_key(key) { - if row_source.row_by_id(row_id)?.is_none() { - stale_row_ids.push(row_id); - } - } - if stale_row_ids.is_empty() { - return Ok(()); - } - let Some(RuntimeIndex::Btree { keys, covering }) = self.index_mut(&index.name) else { - return Ok(()); - }; - for row_id in stale_row_ids { - keys.remove_row_id(key, row_id)?; - if let Some(covering) = covering.as_mut() { - covering.remove_row_id(row_id); - } - } - Ok(()) - } - - pub(super) fn apply_insert_index_updates( - &mut self, - updates: Vec, - ) -> Result<()> { - for update in updates { - match update { - PendingIndexInsert::Btree { - name, - key, - row_id, - covering_values, - } => match self.index_mut(&name) { - Some(RuntimeIndex::Btree { keys, covering }) => { - keys.insert_row_id(key, row_id)?; - if let (Some(covering), Some(values)) = (covering.as_mut(), covering_values) - { - covering.insert_row_values(row_id, values); - } - } - Some(_) => { - return Err(DbError::internal(format!( - "runtime index {name} is not a BTREE index" - ))) - } - None => { - return Err(DbError::internal(format!( - "runtime index {name} is missing" - ))) - } - }, - PendingIndexInsert::Trigram { name, row_id, text } => match self.index_mut(&name) { - Some(RuntimeIndex::Trigram { index }) => { - index.queue_insert(row_id, &text); - } - Some(_) => { - return Err(DbError::internal(format!( - "runtime index {name} is not a trigram index" - ))) - } - None => { - return Err(DbError::internal(format!( - "runtime index {name} is missing" - ))) - } - }, - PendingIndexInsert::Spatial { - name, - row_id, - value, - } => match self.index_mut(&name) { - Some(RuntimeIndex::Spatial { index }) => { - index.insert(row_id, value).map_err(spatial_error)?; - } - Some(_) => { - return Err(DbError::internal(format!( - "runtime index {name} is not a SPATIAL index" - ))) - } - None => { - return Err(DbError::internal(format!( - "runtime index {name} is missing" - ))) - } - }, - PendingIndexInsert::FullText { - name, - row_id, - fields, - } => match self.index_mut(&name) { - Some(RuntimeIndex::FullText { index }) => { - let refs = fields.iter().map(Option::as_deref).collect::>(); - index.insert_document(row_id, &refs); - } - Some(_) => { - return Err(DbError::internal(format!( - "runtime index {name} is not a fulltext index" - ))) - } - None => { - return Err(DbError::internal(format!( - "runtime index {name} is missing" - ))) - } - }, - } - } - Ok(()) - } - - pub(crate) fn execute_statement( - &mut self, - statement: &Statement, - params: &[Value], - page_size: u32, - ) -> Result { - match statement { - Statement::Query(_) | Statement::Explain(_) => { - self.execute_read_statement(statement, params, page_size) - } - Statement::Insert(statement) => { - let result = self.execute_insert(statement, params, page_size)?; - Ok(result) - } - Statement::Update(statement) => { - let result = self.execute_update(statement, params, page_size)?; - Ok(result) - } - Statement::Delete(statement) => { - let result = self.execute_delete(statement, params, page_size)?; - Ok(result) - } - Statement::Analyze { table_name } => { - self.execute_analyze(table_name.as_deref())?; - Ok(QueryResult::with_affected_rows(0)) - } - Statement::CreateTable(statement) => { - self.execute_create_table(statement)?; - if !statement.temporary { - self.rebuild_indexes(page_size)?; - } - Ok(QueryResult::with_affected_rows(0)) - } - Statement::CreateSchema { - name, - if_not_exists, - } => { - self.execute_create_schema(name, *if_not_exists)?; - Ok(QueryResult::with_affected_rows(0)) - } - Statement::CreateTableAs(statement) => { - let result = self.execute_create_table_as(statement, params, page_size)?; - if !statement.temporary { - self.rebuild_indexes(page_size)?; - } - Ok(result) - } - Statement::CreateIndex(statement) => { - if let Some(index_name) = self.execute_create_index(statement, page_size)? { - self.rebuild_index(&index_name, page_size)?; - } - Ok(QueryResult::with_affected_rows(0)) - } - Statement::AlterIndexRebuild { name } => { - self.rebuild_index(name, page_size)?; - Ok(QueryResult::with_affected_rows(0)) - } - Statement::AlterIndexVerify { name } => { - self.verify_index(name, page_size)?; - Ok(QueryResult::with_affected_rows(0)) - } - Statement::CreateView(statement) => { - self.execute_create_view(statement)?; - if !statement.temporary { - self.rebuild_indexes(page_size)?; - } - Ok(QueryResult::with_affected_rows(0)) - } - Statement::CreateTrigger(statement) => { - self.execute_create_trigger(statement)?; - self.rebuild_indexes(page_size)?; - Ok(QueryResult::with_affected_rows(0)) - } - Statement::DropTable { name, if_exists } => { - let temporary = self.temp_table_schema(name).is_some(); - self.execute_drop_table(name, *if_exists, page_size)?; - if !temporary { - self.rebuild_indexes(page_size)?; - } - Ok(QueryResult::with_affected_rows(0)) - } - Statement::DropIndex { name, if_exists } => { - self.execute_drop_index(name, *if_exists)?; - self.rebuild_indexes(page_size)?; - Ok(QueryResult::with_affected_rows(0)) - } - Statement::DropView { name, if_exists } => { - let temporary = self.temp_view(name).is_some(); - self.execute_drop_view(name, *if_exists)?; - if !temporary { - self.rebuild_indexes(page_size)?; - } - Ok(QueryResult::with_affected_rows(0)) - } - Statement::DropTrigger { - name, - table_name, - if_exists, - } => { - self.execute_drop_trigger(name, table_name, *if_exists)?; - self.rebuild_indexes(page_size)?; - Ok(QueryResult::with_affected_rows(0)) - } - Statement::AlterViewRename { - view_name, - new_name, - } => { - self.execute_alter_view_rename(view_name, new_name)?; - self.rebuild_indexes(page_size)?; - Ok(QueryResult::with_affected_rows(0)) - } - Statement::AlterTable { - table_name, - actions, - } => { - self.execute_alter_table(table_name, actions, params, page_size)?; - Ok(QueryResult::with_affected_rows(0)) - } - Statement::TruncateTable { - table_name, - identity, - cascade, - } => { - self.execute_truncate_table( - table_name, - *identity == TruncateIdentityMode::Restart, - *cascade, - page_size, - )?; - Ok(QueryResult::with_affected_rows(0)) - } - } - } - - fn execute_create_table_as( - &mut self, - statement: &CreateTableAsStatement, - params: &[Value], - page_size: u32, - ) -> Result { - let (qualifier, object_name) = compat_schema_qualified_name(&statement.table_name); - if statement.temporary && qualifier == Some(CompatSchemaQualifier::Main) { - return Err(DbError::sql( - "temporary tables cannot be created in the main schema", - )); - } - let temporary = statement.temporary || qualifier == Some(CompatSchemaQualifier::Temp); - let table_name = object_name.to_string(); - if temporary { - if self.temp_relation_exists(&table_name) { - if statement.if_not_exists && self.temp_table_schema(&table_name).is_some() { - return Ok(QueryResult::with_affected_rows(0)); - } - return Err(DbError::sql(format!( - "object {} already exists", - table_name - ))); - } - } else if self.catalog.contains_object(&table_name) { - if statement.if_not_exists && self.catalog.table(&table_name).is_some() { - return Ok(QueryResult::with_affected_rows(0)); - } - return Err(DbError::sql(format!( - "object {} already exists", - table_name - ))); - } - - let mut source = self.evaluate_query(&statement.query, params, &BTreeMap::new())?; - let target_columns = if statement.column_names.is_empty() { - source - .columns - .iter() - .enumerate() - .map(|(index, binding)| { - if binding.name.is_empty() { - format!("column{}", index + 1) - } else { - binding.name.clone() - } - }) - .collect::>() - } else { - if statement.column_names.len() != source.columns.len() { - return Err(DbError::sql(format!( - "CREATE TABLE AS expected {} column names but query produced {} columns", - statement.column_names.len(), - source.columns.len() - ))); - } - statement.column_names.clone() - }; - - let columns = target_columns - .iter() - .enumerate() - .map(|(index, name)| ColumnDefinition { - name: name.clone(), - column_type: infer_column_type_for_ctas(&source.rows, index), - spatial_type: None, - enum_type: None, - nullable: true, - default: None, - generated: None, - generated_stored: true, - primary_key: false, - unique: false, - checks: Vec::new(), - references: None, - }) - .collect::>(); - let create_statement = CreateTableStatement { - table_name: table_name.clone(), - temporary, - if_not_exists: false, - columns, - constraints: Vec::new(), - }; - self.execute_create_table(&create_statement)?; - if !statement.with_data { - return Ok(QueryResult::with_affected_rows(0)); - } - - let temporary = self.visible_table_is_temporary(&table_name); - let mut affected_rows = 0_u64; - for source_row in source.take_rows() { - let candidate = { - let mut staged_table = self - .table_schema(&table_name) - .cloned() - .ok_or_else(|| DbError::sql(format!("unknown table {}", table_name)))?; - let candidate = dml::build_insert_row_values( - self, - &mut staged_table, - &target_columns, - source_row, - params, - )?; - if temporary { - self.temp_table_schema_mut(&table_name) - .ok_or_else(|| DbError::sql(format!("unknown table {}", table_name)))? - .next_row_id = staged_table.next_row_id; - } else { - self.catalog_mut() - .tables - .get_mut(&table_name) - .ok_or_else(|| DbError::sql(format!("unknown table {}", table_name)))? - .next_row_id = staged_table.next_row_id; - } - candidate - }; - self.validate_row(&table_name, &candidate, None, params)?; - let row_id = { - let table = self - .table_schema(&table_name) - .ok_or_else(|| DbError::sql(format!("unknown table {}", table_name)))?; - dml::primary_row_id(table, &candidate) - .unwrap_or_else(|| dml::next_row_id(self, &table_name)) - }; - let stored_row = StoredRow { - row_id, - values: candidate, - }; - let index_updates = - self.prepare_insert_index_updates(&table_name, &stored_row, page_size)?; - self.table_data_mut(&table_name) - .ok_or_else(|| { - DbError::internal(format!("table data for {table_name} is missing")) - })? - .push_row(stored_row); - self.apply_insert_index_updates(index_updates)?; - if !temporary { - self.mark_table_dirty(&table_name); - } - affected_rows += 1; - } - - Ok(QueryResult::with_affected_rows(affected_rows)) - } - - pub(crate) fn execute_read_statement( - &self, - statement: &Statement, - params: &[Value], - _page_size: u32, - ) -> Result { - self.clear_fts_eval_context()?; - match statement { - Statement::Query(query) => { - if self.security_rules_active()? { - return self - .evaluate_query(query, params, &BTreeMap::new()) - .map(dataset_to_result); - } - if let Some(result) = Self::try_execute_simple_integer_series_query(query) { - return Ok(result); - } - if let Some(result) = self.try_execute_simple_count_query(query, params)? { - return Ok(result); - } - if let Some(result) = self.try_execute_simple_min_max_query(query)? { - return Ok(result); - } - if let Some(result) = - self.try_execute_simple_indexed_projection_query(query, params)? - { - return Ok(result); - } - if let Some(result) = - self.try_execute_simple_distinct_filtered_projection_query(query, params)? - { - return Ok(result); - } - if let Some(result) = - self.try_execute_simple_distinct_projection_query(query, params)? - { - return Ok(result); - } - if let Some(result) = - self.try_execute_simple_filtered_projection_query(query, params)? - { - return Ok(result); - } - if let Some(result) = - self.try_execute_simple_expression_projection_query(query, params)? - { - return Ok(result); - } - if let Some(result) = - self.try_execute_simple_table_projection_query(query, params)? - { - return Ok(result); - } - if let Some(result) = - self.try_execute_left_join_status_aggregate_query(query, params)? - { - return Ok(result); - } - if let Some(result) = self.try_execute_crm_revenue_raw_aggregate_query(query)? { - return Ok(result); - } - if let Some(result) = self.try_execute_left_join_aggregate_query(query, params)? { - return Ok(result); - } - if let Some(result) = self.try_execute_simple_grouped_count_query(query, params)? { - return Ok(result); - } - if let Some(result) = - self.try_execute_simple_grouped_numeric_aggregate_query(query, params)? - { - return Ok(result); - } - if let Some(result) = - self.try_execute_three_table_genre_popularity_query(query, params)? - { - return Ok(result); - } - if let Some(result) = self.try_execute_movie_tag_search_query(query, params)? { - return Ok(result); - } - if let Some(result) = self.try_execute_movie_watchlist_query(query, params)? { - return Ok(result); - } - if let Some(result) = - self.try_execute_movie_top_rated_by_year_query(query, params)? - { - return Ok(result); - } - if let Some(result) = self.try_execute_movie_busiest_people_query(query, params)? { - return Ok(result); - } - if let Some(result) = self.try_execute_showdown_window_query(query)? { - return Ok(result); - } - if let Some(result) = - self.try_execute_showdown_directors_cte_query(query, params)? - { - return Ok(result); - } - if let Some(result) = self.try_execute_general_grouped_query(query, params)? { - return Ok(result); - } - if let Some(result) = - self.try_execute_indexed_join_grouped_count_query(query, params)? - { - return Ok(result); - } - if let Some(result) = - self.try_execute_simple_view_projection_limit_query(query, params)? - { - return Ok(result); - } - if let Some(result) = - self.try_execute_indexed_join_limit_projection_query(query, params)? - { - return Ok(result); - } - if let Some(result) = self.try_execute_benchmark_history_query(query, params)? { - return Ok(result); - } - if let Some(result) = self.try_execute_benchmark_report_query(query, params)? { - return Ok(result); - } - if let Some(result) = - self.try_execute_simple_indexed_join_projection_query(query, params)? - { - return Ok(result); - } - if let Some(result) = - self.try_execute_three_table_indexed_join_projection_query(query, params)? - { - return Ok(result); - } - if let Some(result) = self.try_execute_base_table_join(query, params)? { - return Ok(result); - } - self.evaluate_query(query, params, &BTreeMap::new()) - .map(dataset_to_result) - } - Statement::Explain(explain) => { - let mut planner_catalog = self.planner_catalog(); - if self.security_rules_active()? { - for index in planner_catalog.indexes.values_mut() { - if index.kind == IndexKind::Btree { - index.fresh = false; - } - } - } - match explain.statement.as_ref() { - Statement::Update(update) => { - if explain.analyze { - return Err(DbError::sql( - "EXPLAIN ANALYZE is not supported for UPDATE".to_string(), - )); - } - if self - .visible_view(&update.table_name, NameResolutionScope::Session) - .is_some() - { - return Err(DbError::sql(format!( - "EXPLAIN UPDATE is not supported for view {}", - update.table_name - ))); - } - - let mut lines = vec![format!("Mutation: UPDATE {}", update.table_name)]; - for (index, assignment) in update.assignments.iter().enumerate() { - lines.push(format!( - "Assignment {}: {} = {}", - index + 1, - assignment.column_name, - assignment.expr.to_sql() - )); - } - match &update.filter { - Some(filter) => lines.push(format!("Filter: {}", filter.to_sql())), - None => lines.push("Filter: ".to_string()), - } - let table = self.table_schema(&update.table_name).ok_or_else(|| { - DbError::sql(format!("unknown table {}", update.table_name)) - })?; - let candidate_rows = dml::matching_row_ids( - self, - &update.table_name, - &update.table_name, - table, - update.filter.as_ref(), - params, - )? - .len(); - lines.push(format!("Candidate rows: {candidate_rows}")); - lines.push(format!( - "Returning: {}", - if update.returning.is_empty() { - "OFF" - } else { - "ON" - } - )); - - Ok(QueryResult::with_explain(lines)) - } - _ => { - let mut lines = planner::plan_statement( - &Statement::Explain(explain.clone()), - &planner_catalog, - )? - .render(); - if explain.analyze { - lines.insert(0, "ANALYZE true".to_string()); - let started = Instant::now(); - let actual_rows = match explain.statement.as_ref() { - Statement::Query(query) => self - .evaluate_query(query, params, &BTreeMap::new())? - .rows - .len(), - other => { - return Err(DbError::sql(format!( - "EXPLAIN ANALYZE is not supported for {other:?}" - ))) - } - }; - lines.push(format!("Actual Rows: {actual_rows}")); - lines.push(format!( - "Actual Time: {:.3} ms", - started.elapsed().as_secs_f64() * 1_000.0 - )); - } - Ok(QueryResult::with_explain(lines)) - } - } - } - other => Err(DbError::internal(format!( - "read-only execution received mutating statement {other:?}" - ))), - } - } - - fn clear_fts_eval_context(&self) -> Result<()> { - self.fts_eval_context - .lock() - .map_err(|_| DbError::internal("FTS eval context lock poisoned"))? - .scores - .clear(); - Ok(()) - } - - fn analyze_simple_count_query<'a>( - &'a self, - query: &'a Query, - ) -> Result>> { - if !query.ctes.is_empty() - || !query.order_by.is_empty() - || query.limit.is_some() - || query.offset.is_some() - { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if !select.group_by.is_empty() - || select.having.is_some() - || select.distinct - || !select.distinct_on.is_empty() - || select.from.len() != 1 - || select.projection.len() != 1 - { - return Ok(None); - } - let FromItem::Table { - name, - alias: table_alias, - } = &select.from[0] - else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - { - return Ok(None); - } - let Some(table) = self.table_schema(name) else { - return Ok(None); - }; - if !generated_columns_are_stored(table) { - return Ok(None); - } - - let SelectItem::Expr { expr, alias } = &select.projection[0] else { - return Ok(None); - }; - let Expr::Aggregate { - name: aggregate_name, - args, - distinct, - star, - order_by, - within_group, - } = expr - else { - return Ok(None); - }; - if !aggregate_name.eq_ignore_ascii_case("count") - || !args.is_empty() - || *distinct - || !*star - || !order_by.is_empty() - || *within_group - { - return Ok(None); - } - - Ok(Some(SimpleCountQueryPlan { - table_name: name, - table_ref: table_alias.as_deref().unwrap_or(name), - filter: select.filter.as_ref(), - column_name: alias.clone().unwrap_or_else(|| infer_expr_name(expr, 1)), - })) - } - - fn try_execute_simple_count_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_simple_count_query(query)? else { - return Ok(None); - }; - - let row_count = if let Some(filter) = plan.filter { - let table = self - .table_schema(plan.table_name) - .ok_or_else(|| DbError::sql(format!("unknown table {}", plan.table_name)))?; - dml::matching_row_ids( - self, - plan.table_name, - plan.table_ref, - table, - Some(filter), - params, - )? - .len() - } else { - self.visible_table_row_source(plan.table_name).map_or_else( - || { - self.table_data(plan.table_name) - .map_or(0, TableData::row_count) - }, - |source| source.row_count(), - ) - }; - let row_count = i64::try_from(row_count).map_err(|_| { - DbError::sql(format!( - "table {} exceeds COUNT(*) row-count limits", - plan.table_name - )) - })?; - Ok(Some(QueryResult::with_rows( - vec![plan.column_name], - vec![QueryRow::new(vec![Value::Int64(row_count)])], - ))) - } - - fn analyze_simple_min_max_query<'a>( - &'a self, - query: &'a Query, - ) -> Result>> { - if !query.ctes.is_empty() - || !query.order_by.is_empty() - || query.limit.is_some() - || query.offset.is_some() - { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.filter.is_some() - || !select.group_by.is_empty() - || select.having.is_some() - || select.distinct - || !select.distinct_on.is_empty() - || select.from.len() != 1 - || select.projection.len() != 1 - { - return Ok(None); - } - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - { - return Ok(None); - } - let Some(table) = self.table_schema(name) else { - return Ok(None); - }; - if !generated_columns_are_stored(table) { - return Ok(None); - } - - let SelectItem::Expr { - expr, - alias: output_alias, - } = &select.projection[0] - else { - return Ok(None); - }; - let Expr::Aggregate { - name: aggregate_name, - args, - distinct, - star, - order_by, - within_group, - } = expr - else { - return Ok(None); - }; - if *distinct || *star || !order_by.is_empty() || *within_group || args.len() != 1 { - return Ok(None); - } - let is_max = if aggregate_name.eq_ignore_ascii_case("max") { - true - } else if aggregate_name.eq_ignore_ascii_case("min") { - false - } else { - return Ok(None); - }; - - let binding_name = alias.as_deref().unwrap_or(name); - let Expr::Column { - table: aggregate_table, - column: aggregate_column, - } = &args[0] - else { - return Ok(None); - }; - if let Some(aggregate_table) = aggregate_table.as_deref() { - if !identifiers_equal(aggregate_table, name) - && !identifiers_equal(aggregate_table, binding_name) - { - return Ok(None); - } - } - let Some(column_index) = table - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, aggregate_column)) - else { - return Ok(None); - }; - - Ok(Some(SimpleMinMaxQueryPlan { - table_name: name, - column_index, - is_max, - column_name: output_alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, 1)), - })) - } - - fn try_execute_simple_min_max_query(&self, query: &Query) -> Result> { - let Some(plan) = self.analyze_simple_min_max_query(query)? else { - return Ok(None); - }; - let Some(row_source) = self.visible_table_row_source(plan.table_name) else { - return Ok(None); - }; - Ok(Some( - self.simple_min_max_result_from_source(row_source, &plan)?, - )) - } - - fn simple_min_max_result_from_source( - &self, - row_source: VisibleTableRowSource<'_>, - plan: &SimpleMinMaxQueryPlan<'_>, - ) -> Result { - let mut best = Value::Null; - for stored_row in row_source.rows() { - let stored_row = stored_row?; - update_simple_min_max_value( - &mut best, - stored_row.values()[plan.column_index].clone(), - plan.is_max, - )?; - } - Ok(QueryResult::with_rows( - vec![plan.column_name.clone()], - vec![QueryRow::new(vec![best])], - )) - } - - fn simple_min_max_result_from_persisted_state( - &self, - store: &S, - state: PersistedTableState, - plan: &SimpleMinMaxQueryPlan<'_>, - ) -> Result { - let mut best = Value::Null; - visit_persisted_table_rows(store, state, |_, values| { - update_simple_min_max_value(&mut best, values[plan.column_index].clone(), plan.is_max) - })?; - Ok(QueryResult::with_rows( - vec![plan.column_name.clone()], - vec![QueryRow::new(vec![best])], - )) - } - - pub(crate) fn try_execute_simple_deferred_count_query( - &self, - query: &Query, - pager: &PagerHandle, - wal: &WalHandle, - snapshot_lsn: u64, - ) -> Result> { - let Some(plan) = self.analyze_simple_count_query(query)? else { - return Ok(None); - }; - if plan.filter.is_some() { - return Ok(None); - } - if self.visible_table_is_temporary(plan.table_name) - || self.visible_table_row_source(plan.table_name).is_some() - || !self.has_deferred_tables() - { - return Ok(None); - } - if !self - .deferred_table_names() - .any(|candidate| identifiers_equal(candidate, plan.table_name)) - { - return Ok(None); - } - let Some(state) = self.persisted_table_state(plan.table_name) else { - return Ok(None); - }; - let row_count = if state.pointer.is_table_paged_manifest() - && state.pointer.head_page_id != 0 - && state.pointer.logical_len != 0 - { - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - read_persisted_table_row_count(&store, state) - .ok() - .and_then(|count| i64::try_from(count).ok()) - .unwrap_or(0) - } else { - self.catalog - .table_stats - .iter() - .find(|(name, _)| identifiers_equal(name, plan.table_name)) - .map(|(_, stats)| stats.row_count) - .or_else(|| { - if state.row_count == 0 - && state.pointer.head_page_id != 0 - && state.pointer.logical_len != 0 - { - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - read_persisted_table_row_count(&store, state) - .ok() - .and_then(|count| i64::try_from(count).ok()) - } else { - i64::try_from(state.row_count).ok() - } - }) - .unwrap_or(0) - }; - Ok(Some(QueryResult::with_rows( - vec![plan.column_name], - vec![QueryRow::new(vec![Value::Int64(row_count)])], - ))) - } - - pub(crate) fn try_execute_simple_deferred_min_max_query( - &self, - query: &Query, - pager: &PagerHandle, - wal: &WalHandle, - snapshot_lsn: u64, - ) -> Result> { - let Some(plan) = self.analyze_simple_min_max_query(query)? else { - return Ok(None); - }; - if self.visible_table_is_temporary(plan.table_name) - || self.visible_table_row_source(plan.table_name).is_some() - || !self.has_deferred_tables() - { - return Ok(None); - } - if !self - .deferred_table_names() - .any(|candidate| identifiers_equal(candidate, plan.table_name)) - { - return Ok(None); - } - let Some(state) = self.persisted_table_state(plan.table_name) else { - return Ok(None); - }; - if !state.pointer.is_table_paged_manifest() { - return Ok(None); - } - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - Ok(Some(self.simple_min_max_result_from_persisted_state( - &store, state, &plan, - )?)) - } - - fn try_execute_simple_grouped_count_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_simple_grouped_count_query(query, params)? else { - return Ok(None); - }; - let Some(source) = self.visible_table_row_source(plan.table_name) else { - return Ok(None); - }; - Ok(Some(self.simple_grouped_count_result_from_source( - source, &plan, params, - )?)) - } - - fn analyze_simple_grouped_count_query<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result>> { - if !query.ctes.is_empty() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || select.from.len() != 1 - || select.group_by.is_empty() - || select.projection.len() != select.group_by.len() + 1 - { - return Ok(None); - } - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - { - return Ok(None); - } - - let table_schema = match self.table_schema(name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - let binding_name = alias.as_deref().unwrap_or(name); - let table_binding = TableBindingRef { name, alias }; - for group_expr in &select.group_by { - if expr_contains_recursive_unsupported_feature(group_expr) - || !expr_references_only_binding(group_expr, table_binding) - { - return Ok(None); - } - } - let group_eval_bindings = table_schema - .columns - .iter() - .map(|column| { - ColumnBinding::visible(Some(binding_name.to_string()), column.name.clone()) - }) - .collect::>(); - - let filter_expr = match select.filter.as_ref() { - Some(filter) - if !expr_contains_recursive_unsupported_feature(filter) - && expr_references_only_binding(filter, table_binding) => - { - Some(filter.clone()) - } - Some(_) => return Ok(None), - None => None, - }; - - let mut column_names = Vec::with_capacity(select.projection.len()); - let mut group_projection_needs_rewrite = false; - for (projection_item, group_expr) in select - .projection - .iter() - .take(select.group_by.len()) - .zip(&select.group_by) - { - let SelectItem::Expr { - expr: projection_group_expr, - alias: projection_group_alias, - } = projection_item - else { - return Ok(None); - }; - if !grouped_projection_expr_matches_group_expr( - projection_group_expr, - group_expr, - table_binding, - ) { - group_projection_needs_rewrite = true; - } - column_names.push( - projection_group_alias.clone().unwrap_or_else(|| { - infer_expr_name(projection_group_expr, column_names.len() + 1) - }), - ); - } - - let SelectItem::Expr { - expr: count_expr, - alias: count_alias, - } = &select.projection[select.group_by.len()] - else { - return Ok(None); - }; - let count_projection_index = select.group_by.len(); - let count_binding = SimpleGroupedNumericAggregateBinding { - kind: SimpleGroupedNumericAggregateKind::CountRows, - projection_index: count_projection_index, - source_column_name: None, - source_column_index: None, - source_expr: None, - }; - let direct_count = matches!( - count_expr, - Expr::Aggregate { - name, - args, - distinct, - star, - order_by, - within_group, - } if name.eq_ignore_ascii_case("count") - && args.is_empty() - && !*distinct - && *star - && order_by.is_empty() - && !*within_group - ); - - column_names.push( - count_alias - .clone() - .unwrap_or_else(|| infer_expr_name(count_expr, select.group_by.len() + 1)), - ); - let needs_projection_rewrite = group_projection_needs_rewrite || !direct_count; - let (projection_exprs, raw_projection_bindings, having, having_bindings, order_by) = - if needs_projection_rewrite { - let raw_projection_bindings = - simple_grouped_projection_bindings(select.group_by.len(), 1); - let raw_names = raw_projection_bindings - .iter() - .map(|binding| binding.name.clone()) - .collect::>(); - let projection_exprs = select - .projection - .iter() - .map(|item| { - let SelectItem::Expr { expr, .. } = item else { - return None; - }; - rewrite_simple_grouped_output_expr( - expr, - select.group_by.as_slice(), - name, - binding_name, - table_binding, - &raw_names, - std::slice::from_ref(&count_binding), - ) - }) - .collect::>>(); - let Some(projection_exprs) = projection_exprs else { - return Ok(None); - }; - let having = match select.having.as_ref() { - Some(having) => rewrite_simple_grouped_output_expr( - having, - select.group_by.as_slice(), - name, - binding_name, - table_binding, - &raw_names, - std::slice::from_ref(&count_binding), - ), - None => None, - }; - if select.having.is_some() && having.is_none() { - return Ok(None); - } - let order_by = projection_order_by_plan(&query.order_by, &select.projection); - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - ( - Some(projection_exprs), - Some(raw_projection_bindings.clone()), - having, - raw_projection_bindings, - order_by, - ) - } else { - let having_bindings = simple_grouped_having_bindings(column_names.len()); - let having_names = having_bindings - .iter() - .map(|binding| binding.name.clone()) - .collect::>(); - let having = match select.having.as_ref() { - Some(having) => self.rewrite_simple_grouped_having_expr( - having, - select, - name, - binding_name, - &column_names, - &having_names, - count_projection_index, - None, - )?, - None => None, - }; - if select.having.is_some() && having.is_none() { - return Ok(None); - } - let order_by = self.simple_grouped_order_by_plan( - query, - select, - name, - binding_name, - &column_names, - )?; - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - (None, None, having, having_bindings, order_by) - }; - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - Ok(Some(SimpleGroupedCountPlan { - table_name: name, - group_exprs: &select.group_by, - group_eval_bindings, - filter_expr, - column_names, - projection_exprs, - raw_projection_bindings, - having, - having_bindings, - order_by, - limit, - offset, - })) - } - - fn simple_grouped_count_result_from_source( - &self, - row_source: VisibleTableRowSource<'_>, - plan: &SimpleGroupedCountPlan<'_>, - params: &[Value], - ) -> Result { - if let Some(result) = - self.try_simple_grouped_count_result_from_runtime_index(Some(row_source), plan, params)? - { - return Ok(result); - } - - let mut groups = Vec::::new(); - let mut group_positions = BTreeMap::, usize>::new(); - let group_dataset = Dataset::with_rows(plan.group_eval_bindings.clone(), Vec::new()); - for stored_row in row_source.rows() { - let stored_row = stored_row?; - if let Some(filter_expr) = plan.filter_expr.as_ref() { - if !matches!( - self.eval_expr( - filter_expr, - &group_dataset, - stored_row.values(), - params, - &BTreeMap::new(), - None, - )?, - Value::Bool(true) - ) { - continue; - } - } - - let group_values = evaluate_simple_grouped_values( - self, - plan.group_exprs, - &group_dataset, - stored_row.values(), - params, - )?; - let group_key = row_identity(&group_values)?; - let group_index = if let Some(group_index) = group_positions.get(&group_key).copied() { - group_index - } else { - groups.push(SimpleGroupedCountAggregate::new(group_values)); - let group_index = groups.len() - 1; - group_positions.insert(group_key, group_index); - group_index - }; - groups[group_index].count += 1; - } - - render_simple_grouped_count_groups(self, groups, plan, params) - } - - fn try_simple_grouped_count_result_from_runtime_index( - &self, - row_source: Option>, - plan: &SimpleGroupedCountPlan<'_>, - params: &[Value], - ) -> Result> { - if plan.group_exprs.len() != 1 - || plan.filter_expr.is_some() - || plan.projection_exprs.is_some() - || plan.having.is_some() - { - return Ok(None); - } - let Expr::Column { - table: group_table, - column: group_column, - } = &plan.group_exprs[0] - else { - return Ok(None); - }; - if group_table - .as_deref() - .is_some_and(|table| !identifiers_equal(table, plan.table_name)) - { - return Ok(None); - } - let Some(table_schema) = self.table_schema(plan.table_name) else { - return Ok(None); - }; - let Some(group_column_index) = schema_column_index(table_schema, group_column) else { - return Ok(None); - }; - let Some(index) = self.single_column_btree_index(plan.table_name, group_column) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { - return Ok(None); - }; - - let mut groups = Vec::new(); - match keys { - RuntimeBtreeKeys::UniqueInt64(entries, deleted) => { - groups.reserve(entries.len()); - for (key, row_id) in entries.iter() { - if deleted.contains(&row_id) { - continue; - } - groups.push(SimpleGroupedCountAggregate { - group_values: vec![Value::Int64(key)], - count: 1, - }); - } - } - RuntimeBtreeKeys::NonUniqueInt64(entries, deleted) => { - groups.reserve(entries.len()); - for (key, row_ids) in entries.iter() { - let count = row_ids - .iter() - .filter(|row_id| !deleted.contains(row_id)) - .count(); - if count == 0 { - continue; - } - groups.push(SimpleGroupedCountAggregate { - group_values: vec![Value::Int64(key)], - count: i64::try_from(count).map_err(|_| { - DbError::constraint( - "grouped COUNT index bucket exceeds INT64 row-count limits", - ) - })?, - }); - } - } - RuntimeBtreeKeys::UniqueUuid(entries, deleted) => { - groups.reserve(entries.len()); - for (key, row_id) in entries.iter() { - if deleted.contains(row_id) { - continue; - } - groups.push(SimpleGroupedCountAggregate { - group_values: vec![Value::Uuid(*key)], - count: 1, - }); - } - } - RuntimeBtreeKeys::NonUniqueUuid(entries, deleted) => { - groups.reserve(entries.len()); - for (key, row_ids) in entries.iter() { - let count = row_ids - .iter() - .filter(|row_id| !deleted.contains(row_id)) - .count(); - if count == 0 { - continue; - } - groups.push(SimpleGroupedCountAggregate { - group_values: vec![Value::Uuid(*key)], - count: i64::try_from(count).map_err(|_| { - DbError::constraint( - "grouped COUNT index bucket exceeds INT64 row-count limits", - ) - })?, - }); - } - } - RuntimeBtreeKeys::UniqueEncoded(entries, deleted) => { - groups.reserve(entries.len()); - for (key, row_id) in entries.iter() { - if deleted.contains(row_id) { - continue; - } - let Some(values) = Self::runtime_index_key_values_to_group_values( - key, - Some(*row_id), - row_source, - &[group_column_index], - )? - else { - continue; - }; - groups.push(SimpleGroupedCountAggregate { - group_values: values, - count: 1, - }); - } - } - RuntimeBtreeKeys::NonUniqueEncoded(entries, deleted) => { - groups.reserve(entries.len()); - for (key, row_ids) in entries.iter() { - let count = row_ids - .iter() - .filter(|row_id| !deleted.contains(row_id)) - .count(); - if count == 0 { - continue; - } - let Some(row_id) = row_ids - .iter() - .copied() - .find(|row_id| !deleted.contains(row_id)) - else { - continue; - }; - let Some(values) = Self::runtime_index_key_values_to_group_values( - key, - Some(row_id), - row_source, - &[group_column_index], - )? - else { - continue; - }; - groups.push(SimpleGroupedCountAggregate { - group_values: values, - count: i64::try_from(count).map_err(|_| { - DbError::constraint( - "grouped COUNT index bucket exceeds INT64 row-count limits", - ) - })?, - }); - } - } - } - - Ok(Some(render_simple_grouped_count_groups( - self, groups, plan, params, - )?)) - } - - pub(crate) fn try_execute_simple_grouped_count_sql_from_runtime_index( - &self, - table_name: &str, - group_column: &str, - ) -> Result> { - if self.security_rules_active()? - || self - .visible_view(table_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(table_name) - { - return Ok(None); - } - let Some(table_schema) = self.table_schema(table_name) else { - return Ok(None); - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - let Some(group_column_index) = schema_column_index(table_schema, group_column) else { - return Ok(None); - }; - let Some(index) = self.single_column_btree_index(table_name, group_column) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { - return Ok(None); - }; - - let mut groups = Vec::new(); - match keys { - RuntimeBtreeKeys::UniqueInt64(entries, deleted) => { - groups.reserve(entries.len()); - for (key, row_id) in entries.iter() { - if deleted.contains(&row_id) { - continue; - } - groups.push(SimpleGroupedCountAggregate { - group_values: vec![Value::Int64(key)], - count: 1, - }); - } - } - RuntimeBtreeKeys::NonUniqueInt64(entries, deleted) => { - groups.reserve(entries.len()); - for (key, row_ids) in entries.iter() { - let count = row_ids - .iter() - .filter(|row_id| !deleted.contains(row_id)) - .count(); - if count == 0 { - continue; - } - groups.push(SimpleGroupedCountAggregate { - group_values: vec![Value::Int64(key)], - count: i64::try_from(count).map_err(|_| { - DbError::constraint( - "grouped COUNT index bucket exceeds INT64 row-count limits", - ) - })?, - }); - } - } - RuntimeBtreeKeys::UniqueUuid(entries, deleted) => { - groups.reserve(entries.len()); - for (key, row_id) in entries.iter() { - if deleted.contains(row_id) { - continue; - } - groups.push(SimpleGroupedCountAggregate { - group_values: vec![Value::Uuid(*key)], - count: 1, - }); - } - } - RuntimeBtreeKeys::NonUniqueUuid(entries, deleted) => { - groups.reserve(entries.len()); - for (key, row_ids) in entries.iter() { - let count = row_ids - .iter() - .filter(|row_id| !deleted.contains(row_id)) - .count(); - if count == 0 { - continue; - } - groups.push(SimpleGroupedCountAggregate { - group_values: vec![Value::Uuid(*key)], - count: i64::try_from(count).map_err(|_| { - DbError::constraint( - "grouped COUNT index bucket exceeds INT64 row-count limits", - ) - })?, - }); - } - } - RuntimeBtreeKeys::UniqueEncoded(entries, deleted) => { - groups.reserve(entries.len()); - for (key, row_id) in entries.iter() { - if deleted.contains(row_id) { - continue; - } - let Some(value) = Self::decode_runtime_index_group_key(key) else { - return Ok(None); - }; - groups.push(SimpleGroupedCountAggregate { - group_values: vec![value], - count: 1, - }); - } - } - RuntimeBtreeKeys::NonUniqueEncoded(entries, deleted) => { - groups.reserve(entries.len()); - for (key, row_ids) in entries.iter() { - let count = row_ids - .iter() - .filter(|row_id| !deleted.contains(row_id)) - .count(); - if count == 0 { - continue; - } - let Some(value) = Self::decode_runtime_index_group_key(key) else { - return Ok(None); - }; - groups.push(SimpleGroupedCountAggregate { - group_values: vec![value], - count: i64::try_from(count).map_err(|_| { - DbError::constraint( - "grouped COUNT index bucket exceeds INT64 row-count limits", - ) - })?, - }); - } - } - } - - let mut rows = groups - .into_iter() - .map(SimpleGroupedCountAggregate::into_row) - .collect::>(); - sort_query_rows_by_projection_order( - Some(self), - &mut rows, - &[SimpleOrderByPlan { - projection_index: 0, - descending: false, - collation: None, - }], - )?; - Ok(Some(QueryResult::with_rows( - vec![ - table_schema.columns[group_column_index].name.clone(), - "col2".to_string(), - ], - rows, - ))) - } - - fn runtime_index_key_values_to_group_values<'a>( - key: &'a [u8], - row_id: Option, - row_source: Option>, - projection_indexes: &[usize], - ) -> Result>> { - if let Some(value) = Self::decode_runtime_index_group_key(key) { - return Ok(Some(vec![value])); - } - - let (row_source, row_id) = match (row_source, row_id) { - (Some(row_source), Some(row_id)) => (row_source, row_id), - _ => return Ok(None), - }; - row_source.projected_values_by_id(row_id, projection_indexes) - } - - fn decode_runtime_index_group_key(key: &[u8]) -> Option { - let (tag, payload) = key.split_first()?; - match *tag { - 0 if payload.is_empty() => Some(Value::Null), - 1 if payload.len() == 1 => match payload[0] { - 0 => Some(Value::Bool(false)), - 1 => Some(Value::Bool(true)), - _ => None, - }, - 2 if payload.len() == 8 => { - let mut bytes = [0_u8; 8]; - bytes.copy_from_slice(payload); - let bits = u64::from_be_bytes(bytes) ^ 0x8000_0000_0000_0000; - Some(Value::Int64(i64::from_be_bytes(bits.to_be_bytes()))) - } - 3 if payload.len() == 8 => { - let mut bytes = [0_u8; 8]; - bytes.copy_from_slice(payload); - let sortable = u64::from_be_bytes(bytes); - let bits = if sortable & (1_u64 << 63) != 0 { - sortable ^ (1_u64 << 63) - } else { - !sortable - }; - Some(Value::Float64(f64::from_bits(bits))) - } - 6 if payload.len() == 16 => { - let mut bytes = [0_u8; 16]; - bytes.copy_from_slice(payload); - Some(Value::Uuid(bytes)) - } - 7 => { - let text = String::from_utf8(payload.to_vec()).ok()?; - Some(Value::Text(text)) - } - 8 => Some(Value::Blob(payload.to_vec())), - 9 if payload.len() == 16 => { - let mut enum_type_id = [0_u8; 8]; - enum_type_id.copy_from_slice(&payload[..8]); - let mut label_id = [0_u8; 8]; - label_id.copy_from_slice(&payload[8..16]); - Some(Value::Enum { - enum_type_id: u64::from_be_bytes(enum_type_id), - label_id: u64::from_be_bytes(label_id), - }) - } - 10 if payload.len() == 17 => { - let family = *payload.last()?; - match family { - 4 => { - let mut addr = [0_u8; 16]; - addr[..4].copy_from_slice(&payload[12..16]); - Some(Value::IpAddr { family, addr }) - } - 6 => { - let mut addr = [0_u8; 16]; - addr.copy_from_slice(&payload[..16]); - Some(Value::IpAddr { family, addr }) - } - _ => None, - } - } - 11 if matches!(payload.len(), 6 | 18) => { - let family = payload[0]; - let prefix_len = payload[1]; - if family != 4 && family != 6 { - return None; - } - let mut network = [0_u8; 16]; - if family == 4 { - if payload.len() != 6 { - return None; - } - network[..4].copy_from_slice(&payload[2..6]); - } else { - network.copy_from_slice(&payload[2..18]); - } - Some(Value::Cidr { - family, - prefix_len, - network, - }) - } - 12 if payload.len() == 4 => { - let mut bytes = [0_u8; 4]; - bytes.copy_from_slice(payload); - let raw = u32::from_be_bytes(bytes) ^ 0x8000_0000; - Some(Value::DateDays(i32::from_be_bytes(raw.to_be_bytes()))) - } - 13 if payload.len() == 8 => { - let mut bytes = [0_u8; 8]; - bytes.copy_from_slice(payload); - let bits = u64::from_be_bytes(bytes) ^ 0x8000_0000_0000_0000; - Some(Value::TimeMicros(i64::from_be_bytes(bits.to_be_bytes()))) - } - 14 if payload.len() == 8 => { - let mut bytes = [0_u8; 8]; - bytes.copy_from_slice(payload); - let bits = u64::from_be_bytes(bytes) ^ 0x8000_0000_0000_0000; - Some(Value::TimestampTzMicros(i64::from_be_bytes( - bits.to_be_bytes(), - ))) - } - 15 if payload.len() == 16 => { - let mut months = [0_u8; 4]; - months.copy_from_slice(&payload[..4]); - let mut days = [0_u8; 4]; - days.copy_from_slice(&payload[4..8]); - let mut micros = [0_u8; 8]; - micros.copy_from_slice(&payload[8..16]); - Some(Value::Interval { - months: { - let raw = u32::from_be_bytes(months) ^ 0x8000_0000; - i32::from_be_bytes(raw.to_be_bytes()) - }, - days: { - let raw = u32::from_be_bytes(days) ^ 0x8000_0000; - i32::from_be_bytes(raw.to_be_bytes()) - }, - micros: { - let bits = u64::from_be_bytes(micros) ^ 0x8000_0000_0000_0000; - i64::from_be_bytes(bits.to_be_bytes()) - }, - }) - } - 16 if !payload.is_empty() => { - let len = *payload.last()?; - if len > 8 { - return None; - } - let len_usize = usize::from(len); - if payload.len() != len_usize + 1 { - return None; - } - let mut bytes = [0_u8; 8]; - bytes[..len_usize].copy_from_slice(&payload[..len_usize]); - Some(Value::MacAddr { len, bytes }) - } - 5 => None, - _ => None, - } - } - - fn simple_grouped_count_result_from_persisted_state( - &self, - store: &S, - state: PersistedTableState, - plan: &SimpleGroupedCountPlan<'_>, - params: &[Value], - ) -> Result { - let mut groups = Vec::::new(); - let mut group_positions = BTreeMap::, usize>::new(); - let group_dataset = Dataset::with_rows(plan.group_eval_bindings.clone(), Vec::new()); - visit_persisted_table_rows(store, state, |_, values| { - if let Some(filter_expr) = plan.filter_expr.as_ref() { - if !matches!( - self.eval_expr( - filter_expr, - &group_dataset, - values, - params, - &BTreeMap::new(), - None, - )?, - Value::Bool(true) - ) { - return Ok(()); - } - } - - let group_values = evaluate_simple_grouped_values( - self, - plan.group_exprs, - &group_dataset, - values, - params, - )?; - let group_key = row_identity(&group_values)?; - let group_index = if let Some(group_index) = group_positions.get(&group_key).copied() { - group_index - } else { - groups.push(SimpleGroupedCountAggregate::new(group_values)); - let group_index = groups.len() - 1; - group_positions.insert(group_key, group_index); - group_index - }; - groups[group_index].count += 1; - Ok(()) - })?; - - render_simple_grouped_count_groups(self, groups, plan, params) - } - - pub(crate) fn try_execute_simple_grouped_numeric_aggregate_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_simple_grouped_numeric_aggregate_query(query, params)? else { - return Ok(None); - }; - let Some(source) = self.visible_table_row_source(plan.table_name) else { - return Ok(None); - }; - Ok(Some( - self.simple_grouped_numeric_aggregate_result_from_source(source, &plan, params)?, - )) - } - - fn analyze_simple_grouped_numeric_aggregate_query<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result>> { - if !query.ctes.is_empty() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || select.from.len() != 1 - || select.projection.len() <= select.group_by.len() - { - return Ok(None); - } - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - { - return Ok(None); - } - - let table_schema = match self.table_schema(name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - let binding_name = alias.as_deref().unwrap_or(name); - let table_binding = TableBindingRef { name, alias }; - for group_expr in &select.group_by { - if expr_contains_recursive_unsupported_feature(group_expr) - || !expr_references_only_binding(group_expr, table_binding) - { - return Ok(None); - } - } - let group_eval_bindings = table_schema - .columns - .iter() - .map(|column| { - ColumnBinding::visible(Some(binding_name.to_string()), column.name.clone()) - }) - .collect::>(); - - let filter_expr = match select.filter.as_ref() { - Some(filter) - if !expr_contains_recursive_unsupported_feature(filter) - && expr_references_only_binding(filter, table_binding) => - { - Some(filter.clone()) - } - Some(_) => return Ok(None), - None => None, - }; - - let mut column_names = Vec::with_capacity(select.projection.len()); - let mut group_projection_needs_rewrite = false; - for (projection_item, group_expr) in select - .projection - .iter() - .take(select.group_by.len()) - .zip(&select.group_by) - { - let SelectItem::Expr { - expr: projection_group_expr, - alias: projection_group_alias, - } = projection_item - else { - return Ok(None); - }; - if !grouped_projection_expr_matches_group_expr( - projection_group_expr, - group_expr, - table_binding, - ) { - group_projection_needs_rewrite = true; - } - column_names.push( - projection_group_alias.clone().unwrap_or_else(|| { - infer_expr_name(projection_group_expr, column_names.len() + 1) - }), - ); - } - - let mut aggregate_bindings = Vec::new(); - let mut saw_supported_aggregate = false; - let mut projection_exprs: Option> = None; - for (projection_index, projection_item) in select - .projection - .iter() - .enumerate() - .skip(select.group_by.len()) - { - let SelectItem::Expr { - expr: projection_expr, - alias, - } = projection_item - else { - return Ok(None); - }; - let binding = if let Some(binding) = analyze_simple_grouped_numeric_aggregate_binding( - projection_expr, - projection_index, - name, - binding_name, - table_binding, - table_schema, - ) { - if !matches!(binding.kind, SimpleGroupedNumericAggregateKind::CountRows) { - saw_supported_aggregate = true; - } - Some(binding) - } else { - if collect_simple_grouped_numeric_projection_aggregates( - projection_expr, - name, - binding_name, - table_binding, - table_schema, - &mut aggregate_bindings, - &mut saw_supported_aggregate, - ) - .is_none() - { - return Ok(None); - } - projection_exprs.get_or_insert_with(Vec::new); - None - }; - column_names.push( - alias - .clone() - .unwrap_or_else(|| infer_expr_name(projection_expr, projection_index + 1)), - ); - if let Some(binding) = binding { - aggregate_bindings.push(binding); - } - } - let mut having_requires_projection_rewrite = false; - if let Some(having) = select.having.as_ref() { - let aggregate_count_before = aggregate_bindings.len(); - if collect_simple_grouped_numeric_having_aggregates( - having, - name, - binding_name, - table_binding, - table_schema, - &mut aggregate_bindings, - &mut saw_supported_aggregate, - ) - .is_none() - { - return Ok(None); - } - having_requires_projection_rewrite = aggregate_bindings.len() != aggregate_count_before; - } - if aggregate_bindings.is_empty() { - return Ok(None); - } - let (projection_exprs, raw_projection_bindings, having, having_bindings, order_by) = - if group_projection_needs_rewrite - || projection_exprs.is_some() - || having_requires_projection_rewrite - { - let raw_projection_bindings = simple_grouped_projection_bindings( - select.group_by.len(), - aggregate_bindings.len(), - ); - let raw_names = raw_projection_bindings - .iter() - .map(|binding| binding.name.clone()) - .collect::>(); - let mut rewritten_projection_exprs = Vec::with_capacity(select.projection.len()); - for projection_item in &select.projection { - let SelectItem::Expr { expr, .. } = projection_item else { - return Ok(None); - }; - let Some(rewritten) = rewrite_simple_grouped_output_expr( - expr, - select.group_by.as_slice(), - name, - binding_name, - table_binding, - &raw_names, - &aggregate_bindings, - ) else { - return Ok(None); - }; - rewritten_projection_exprs.push(rewritten); - } - let having = match select.having.as_ref() { - Some(having) => rewrite_simple_grouped_output_expr( - having, - select.group_by.as_slice(), - name, - binding_name, - table_binding, - &raw_names, - &aggregate_bindings, - ), - None => None, - }; - if select.having.is_some() && having.is_none() { - return Ok(None); - } - let order_by = projection_order_by_plan(&query.order_by, &select.projection); - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - ( - Some(rewritten_projection_exprs), - Some(raw_projection_bindings.clone()), - having, - raw_projection_bindings, - order_by, - ) - } else { - let having_bindings = simple_grouped_having_bindings(column_names.len()); - let having_names = having_bindings - .iter() - .map(|binding| binding.name.clone()) - .collect::>(); - let having = match select.having.as_ref() { - Some(having) => self.rewrite_simple_grouped_having_expr_with_bindings( - having, - select, - name, - binding_name, - &column_names, - &having_names, - &aggregate_bindings, - )?, - None => None, - }; - if select.having.is_some() && having.is_none() { - return Ok(None); - } - let order_by = self.simple_grouped_order_by_plan( - query, - select, - name, - binding_name, - &column_names, - )?; - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - (None, None, having, having_bindings, order_by) - }; - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - Ok(Some(SimpleGroupedNumericAggregatePlan { - table_name: name, - group_exprs: &select.group_by, - group_eval_bindings, - filter_expr, - column_names, - aggregate_bindings, - projection_exprs, - raw_projection_bindings, - having, - having_bindings, - order_by, - limit, - offset, - })) - } - - fn simple_grouped_numeric_aggregate_result_from_source( - &self, - row_source: VisibleTableRowSource<'_>, - plan: &SimpleGroupedNumericAggregatePlan<'_>, - params: &[Value], - ) -> Result { - if let Some(result) = - self.try_simple_scalar_int64_numeric_aggregate_from_source(row_source, plan)? - { - return Ok(result); - } - if let Some(result) = - self.try_simple_scalar_filtered_numeric_aggregate_from_source(row_source, plan, params)? - { - return Ok(result); - } - - let mut groups = Vec::::new(); - let mut group_positions = BTreeMap::, usize>::new(); - let group_dataset = Dataset::with_rows(plan.group_eval_bindings.clone(), Vec::new()); - if plan.group_exprs.is_empty() { - groups.push(SimpleGroupedNumericAggregate::new( - Vec::new(), - plan.aggregate_bindings.len(), - )); - group_positions.insert(row_identity(&[])?, 0); - } - for stored_row in row_source.rows() { - let stored_row = stored_row?; - if let Some(filter_expr) = plan.filter_expr.as_ref() { - if !matches!( - self.eval_expr( - filter_expr, - &group_dataset, - stored_row.values(), - params, - &BTreeMap::new(), - None, - )?, - Value::Bool(true) - ) { - continue; - } - } - - let group_values = evaluate_simple_grouped_values( - self, - plan.group_exprs, - &group_dataset, - stored_row.values(), - params, - )?; - let group_key = row_identity(&group_values)?; - let group_index = if let Some(group_index) = group_positions.get(&group_key).copied() { - group_index - } else { - groups.push(SimpleGroupedNumericAggregate::new( - group_values, - plan.aggregate_bindings.len(), - )); - let group_index = groups.len() - 1; - group_positions.insert(group_key, group_index); - group_index - }; - groups[group_index].count += 1; - for (aggregate_index, aggregate) in plan.aggregate_bindings.iter().enumerate() { - match aggregate.kind { - SimpleGroupedNumericAggregateKind::CountNonNull => { - if let Some(source_column_index) = aggregate.source_column_index { - groups[group_index].count_non_null( - aggregate_index, - &stored_row.values()[source_column_index], - ); - } else if let Some(source_expr) = aggregate.source_expr.as_ref() { - let value = self.eval_expr( - source_expr, - &group_dataset, - stored_row.values(), - params, - &BTreeMap::new(), - None, - )?; - groups[group_index].count_non_null(aggregate_index, &value); - } - } - SimpleGroupedNumericAggregateKind::CountDistinct => { - if let Some(source_column_index) = aggregate.source_column_index { - groups[group_index].count_distinct( - aggregate_index, - &stored_row.values()[source_column_index], - )?; - } else if let Some(source_expr) = aggregate.source_expr.as_ref() { - let value = self.eval_expr( - source_expr, - &group_dataset, - stored_row.values(), - params, - &BTreeMap::new(), - None, - )?; - groups[group_index].count_distinct(aggregate_index, &value)?; - } - } - SimpleGroupedNumericAggregateKind::Sum - | SimpleGroupedNumericAggregateKind::SumDistinct - | SimpleGroupedNumericAggregateKind::Avg - | SimpleGroupedNumericAggregateKind::AvgDistinct - | SimpleGroupedNumericAggregateKind::Total - | SimpleGroupedNumericAggregateKind::TotalDistinct => { - if let Some(source_column_index) = aggregate.source_column_index { - if matches!( - aggregate.kind, - SimpleGroupedNumericAggregateKind::SumDistinct - | SimpleGroupedNumericAggregateKind::AvgDistinct - | SimpleGroupedNumericAggregateKind::TotalDistinct - ) { - groups[group_index].add_numeric_distinct( - aggregate_index, - &stored_row.values()[source_column_index], - )?; - } else { - groups[group_index].add_numeric( - aggregate_index, - &stored_row.values()[source_column_index], - )?; - } - } else if let Some(source_expr) = aggregate.source_expr.as_ref() { - let value = self.eval_expr( - source_expr, - &group_dataset, - stored_row.values(), - params, - &BTreeMap::new(), - None, - )?; - if matches!( - aggregate.kind, - SimpleGroupedNumericAggregateKind::SumDistinct - | SimpleGroupedNumericAggregateKind::AvgDistinct - | SimpleGroupedNumericAggregateKind::TotalDistinct - ) { - groups[group_index] - .add_numeric_distinct(aggregate_index, &value)?; - } else { - groups[group_index].add_numeric(aggregate_index, &value)?; - } - } - } - SimpleGroupedNumericAggregateKind::StddevSamp - | SimpleGroupedNumericAggregateKind::StddevSampDistinct - | SimpleGroupedNumericAggregateKind::StddevPop - | SimpleGroupedNumericAggregateKind::StddevPopDistinct - | SimpleGroupedNumericAggregateKind::VarSamp - | SimpleGroupedNumericAggregateKind::VarSampDistinct - | SimpleGroupedNumericAggregateKind::VarPop - | SimpleGroupedNumericAggregateKind::VarPopDistinct => { - if let Some(source_column_index) = aggregate.source_column_index { - if aggregate.kind.uses_distinct() { - groups[group_index].add_variance_distinct( - aggregate_index, - &stored_row.values()[source_column_index], - )?; - } else { - groups[group_index].add_variance( - aggregate_index, - &stored_row.values()[source_column_index], - )?; - } - } else if let Some(source_expr) = aggregate.source_expr.as_ref() { - let value = self.eval_expr( - source_expr, - &group_dataset, - stored_row.values(), - params, - &BTreeMap::new(), - None, - )?; - if aggregate.kind.uses_distinct() { - groups[group_index] - .add_variance_distinct(aggregate_index, &value)?; - } else { - groups[group_index].add_variance(aggregate_index, &value)?; - } - } - } - SimpleGroupedNumericAggregateKind::BoolAnd - | SimpleGroupedNumericAggregateKind::BoolAndDistinct - | SimpleGroupedNumericAggregateKind::BoolOr - | SimpleGroupedNumericAggregateKind::BoolOrDistinct => { - if let Some(source_column_index) = aggregate.source_column_index { - if aggregate.kind.uses_distinct() { - groups[group_index].add_bool_distinct( - aggregate_index, - &stored_row.values()[source_column_index], - )?; - } else { - groups[group_index].add_bool( - aggregate_index, - &stored_row.values()[source_column_index], - )?; - } - } else if let Some(source_expr) = aggregate.source_expr.as_ref() { - let value = self.eval_expr( - source_expr, - &group_dataset, - stored_row.values(), - params, - &BTreeMap::new(), - None, - )?; - if aggregate.kind.uses_distinct() { - groups[group_index].add_bool_distinct(aggregate_index, &value)?; - } else { - groups[group_index].add_bool(aggregate_index, &value)?; - } - } - } - SimpleGroupedNumericAggregateKind::Min - | SimpleGroupedNumericAggregateKind::Max => { - let Some(source_expr) = aggregate.source_expr.as_ref() else { - continue; - }; - let value = self.eval_expr( - source_expr, - &group_dataset, - stored_row.values(), - params, - &BTreeMap::new(), - None, - )?; - update_simple_min_max_value( - &mut groups[group_index].extreme_values[aggregate_index], - value, - matches!(aggregate.kind, SimpleGroupedNumericAggregateKind::Max), - )?; - } - SimpleGroupedNumericAggregateKind::CountRows => {} - } - } - } - - render_simple_grouped_numeric_aggregate_groups(self, groups, plan, params) - } - - fn try_simple_scalar_int64_numeric_aggregate_from_source( - &self, - row_source: VisibleTableRowSource<'_>, - plan: &SimpleGroupedNumericAggregatePlan<'_>, - ) -> Result> { - let Some(column_index) = self.simple_scalar_int64_aggregate_column(plan) else { - return Ok(None); - }; - let mut stats = SimpleScalarInt64AggregateStats::default(); - row_source.visit_int64_column_values(column_index, |_, value| { - stats.add(value); - Ok(()) - })?; - Ok(Some(QueryResult::with_rows( - plan.column_names.clone(), - vec![QueryRow::new(stats.into_values(&plan.aggregate_bindings))], - ))) - } - - fn simple_scalar_int64_aggregate_column( - &self, - plan: &SimpleGroupedNumericAggregatePlan<'_>, - ) -> Option { - if !plan.group_exprs.is_empty() - || plan.filter_expr.is_some() - || plan.projection_exprs.is_some() - || plan.having.is_some() - || plan.order_by.is_some() - || plan.limit.is_some() - || plan.offset != 0 - { - return None; - } - let table_schema = self.table_schema(plan.table_name)?; - let mut scalar_column_index = None; - for aggregate in &plan.aggregate_bindings { - let column_index = match aggregate.kind { - SimpleGroupedNumericAggregateKind::CountRows => continue, - SimpleGroupedNumericAggregateKind::CountNonNull - | SimpleGroupedNumericAggregateKind::Sum - | SimpleGroupedNumericAggregateKind::Avg - | SimpleGroupedNumericAggregateKind::Min - | SimpleGroupedNumericAggregateKind::Max => { - simple_aggregate_source_column_index(aggregate, table_schema)? - } - _ => return None, - }; - if table_schema.columns.get(column_index)?.column_type != ColumnType::Int64 { - return None; - } - if scalar_column_index - .replace(column_index) - .is_some_and(|existing| existing != column_index) - { - return None; - } - } - scalar_column_index - } - - fn try_simple_scalar_filtered_numeric_aggregate_from_source( - &self, - row_source: VisibleTableRowSource<'_>, - plan: &SimpleGroupedNumericAggregatePlan<'_>, - params: &[Value], - ) -> Result> { - if !plan.group_exprs.is_empty() - || plan.projection_exprs.is_some() - || plan.having.is_some() - || plan.order_by.is_some() - || plan.limit.is_some() - || plan.offset != 0 - { - return Ok(None); - } - let Some(filter_expr) = plan.filter_expr.as_ref() else { - return Ok(None); - }; - let Some((_filter_table, filter_column, filter_value_expr)) = - simple_btree_lookup(filter_expr) - else { - return Ok(None); - }; - let Some(table_schema) = self.table_schema(plan.table_name) else { - return Ok(None); - }; - let Some(filter_column_index) = schema_column_index(table_schema, filter_column) else { - return Ok(None); - }; - if plan.aggregate_bindings.iter().any(|aggregate| { - !matches!( - aggregate.kind, - SimpleGroupedNumericAggregateKind::CountRows - | SimpleGroupedNumericAggregateKind::Sum - ) || (matches!(aggregate.kind, SimpleGroupedNumericAggregateKind::Sum) - && aggregate.source_column_index.is_none()) - }) { - return Ok(None); - } - - let filter_value = self.eval_expr( - filter_value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?; - let mut row_count = 0_i64; - let mut numeric_states = vec![ - SimpleGroupedNumericState { - numeric_count: 0, - total_int: 0, - total_float: 0.0, - saw_float: false, - saw_value: false, - }; - plan.aggregate_bindings.len() - ]; - - for stored_row in row_source.rows() { - let stored_row = stored_row?; - if compare_values(&stored_row.values()[filter_column_index], &filter_value)? - != std::cmp::Ordering::Equal - { - continue; - } - row_count += 1; - for (aggregate_index, aggregate) in plan.aggregate_bindings.iter().enumerate() { - if matches!(aggregate.kind, SimpleGroupedNumericAggregateKind::Sum) { - let source_column_index = aggregate.source_column_index.ok_or_else(|| { - DbError::internal("simple scalar SUM missing source column") - })?; - numeric_states[aggregate_index] - .add(&stored_row.values()[source_column_index])?; - } - } - } - - let values = plan - .aggregate_bindings - .iter() - .enumerate() - .map(|(aggregate_index, aggregate)| match aggregate.kind { - SimpleGroupedNumericAggregateKind::CountRows => Value::Int64(row_count), - SimpleGroupedNumericAggregateKind::Sum => { - numeric_states[aggregate_index].value(aggregate.kind) - } - _ => Value::Null, - }) - .collect::>(); - Ok(Some(QueryResult::with_rows( - plan.column_names.clone(), - vec![QueryRow::new(values)], - ))) - } - - fn try_simple_scalar_filtered_numeric_aggregate_from_persisted_state( - &self, - store: &S, - state: PersistedTableState, - plan: &SimpleGroupedNumericAggregatePlan<'_>, - params: &[Value], - ) -> Result> { - if !plan.group_exprs.is_empty() - || plan.projection_exprs.is_some() - || plan.having.is_some() - || plan.order_by.is_some() - || plan.limit.is_some() - || plan.offset != 0 - { - return Ok(None); - } - let Some(filter_expr) = plan.filter_expr.as_ref() else { - return Ok(None); - }; - let Some((_filter_table, filter_column, filter_value_expr)) = - simple_btree_lookup(filter_expr) - else { - return Ok(None); - }; - let Some(table_schema) = self.table_schema(plan.table_name) else { - return Ok(None); - }; - let Some(filter_column_index) = schema_column_index(table_schema, filter_column) else { - return Ok(None); - }; - if plan.aggregate_bindings.iter().any(|aggregate| { - !matches!( - aggregate.kind, - SimpleGroupedNumericAggregateKind::CountRows - | SimpleGroupedNumericAggregateKind::Sum - ) || (matches!(aggregate.kind, SimpleGroupedNumericAggregateKind::Sum) - && aggregate.source_column_index.is_none()) - }) { - return Ok(None); - } - - let filter_value = self.eval_expr( - filter_value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?; - let mut row_count = 0_i64; - let mut numeric_states = vec![ - SimpleGroupedNumericState { - numeric_count: 0, - total_int: 0, - total_float: 0.0, - saw_float: false, - saw_value: false, - }; - plan.aggregate_bindings.len() - ]; - - let mut projection_indexes = Vec::with_capacity(plan.aggregate_bindings.len() + 1); - projection_indexes.push(filter_column_index); - let filter_projection_index = 0; - let mut aggregate_projection_indexes = Vec::with_capacity(plan.aggregate_bindings.len()); - for aggregate in &plan.aggregate_bindings { - let Some(source_column_index) = aggregate.source_column_index else { - aggregate_projection_indexes.push(None); - continue; - }; - let projection_index = - push_projection_index(&mut projection_indexes, source_column_index); - aggregate_projection_indexes.push(Some(projection_index)); - } - - visit_persisted_table_projected_values(store, state, &projection_indexes, |_, values| { - if compare_values(&values[filter_projection_index], &filter_value)? - != std::cmp::Ordering::Equal - { - return Ok(()); - } - row_count += 1; - for (aggregate_index, aggregate) in plan.aggregate_bindings.iter().enumerate() { - if matches!(aggregate.kind, SimpleGroupedNumericAggregateKind::Sum) { - let source_projection_index = aggregate_projection_indexes[aggregate_index] - .ok_or_else(|| { - DbError::internal("simple scalar SUM missing source column") - })?; - numeric_states[aggregate_index].add(&values[source_projection_index])?; - } - } - Ok(()) - })?; - - let values = plan - .aggregate_bindings - .iter() - .enumerate() - .map(|(aggregate_index, aggregate)| match aggregate.kind { - SimpleGroupedNumericAggregateKind::CountRows => Value::Int64(row_count), - SimpleGroupedNumericAggregateKind::Sum => { - numeric_states[aggregate_index].value(aggregate.kind) - } - _ => Value::Null, - }) - .collect::>(); - Ok(Some(QueryResult::with_rows( - plan.column_names.clone(), - vec![QueryRow::new(values)], - ))) - } - - fn simple_grouped_numeric_aggregate_result_from_persisted_state( - &self, - store: &S, - state: PersistedTableState, - plan: &SimpleGroupedNumericAggregatePlan<'_>, - params: &[Value], - ) -> Result { - if let Some(result) = - self.try_simple_scalar_int64_numeric_aggregate_from_persisted_state(store, state, plan)? - { - return Ok(result); - } - if let Some(result) = self - .try_simple_scalar_filtered_numeric_aggregate_from_persisted_state( - store, state, plan, params, - )? - { - return Ok(result); - } - - let mut groups = Vec::::new(); - let mut group_positions = BTreeMap::, usize>::new(); - let group_dataset = Dataset::with_rows(plan.group_eval_bindings.clone(), Vec::new()); - if plan.group_exprs.is_empty() { - groups.push(SimpleGroupedNumericAggregate::new( - Vec::new(), - plan.aggregate_bindings.len(), - )); - group_positions.insert(row_identity(&[])?, 0); - } - visit_persisted_table_rows(store, state, |_, values| { - if let Some(filter_expr) = plan.filter_expr.as_ref() { - if !matches!( - self.eval_expr( - filter_expr, - &group_dataset, - values, - params, - &BTreeMap::new(), - None, - )?, - Value::Bool(true) - ) { - return Ok(()); - } - } - - let group_values = evaluate_simple_grouped_values( - self, - plan.group_exprs, - &group_dataset, - values, - params, - )?; - let group_key = row_identity(&group_values)?; - let group_index = if let Some(group_index) = group_positions.get(&group_key).copied() { - group_index - } else { - groups.push(SimpleGroupedNumericAggregate::new( - group_values, - plan.aggregate_bindings.len(), - )); - let group_index = groups.len() - 1; - group_positions.insert(group_key, group_index); - group_index - }; - groups[group_index].count += 1; - for (aggregate_index, aggregate) in plan.aggregate_bindings.iter().enumerate() { - match aggregate.kind { - SimpleGroupedNumericAggregateKind::CountNonNull => { - if let Some(source_column_index) = aggregate.source_column_index { - groups[group_index] - .count_non_null(aggregate_index, &values[source_column_index]); - } else if let Some(source_expr) = aggregate.source_expr.as_ref() { - let value = self.eval_expr( - source_expr, - &group_dataset, - values, - params, - &BTreeMap::new(), - None, - )?; - groups[group_index].count_non_null(aggregate_index, &value); - } - } - SimpleGroupedNumericAggregateKind::CountDistinct => { - if let Some(source_column_index) = aggregate.source_column_index { - groups[group_index] - .count_distinct(aggregate_index, &values[source_column_index])?; - } else if let Some(source_expr) = aggregate.source_expr.as_ref() { - let value = self.eval_expr( - source_expr, - &group_dataset, - values, - params, - &BTreeMap::new(), - None, - )?; - groups[group_index].count_distinct(aggregate_index, &value)?; - } - } - SimpleGroupedNumericAggregateKind::Sum - | SimpleGroupedNumericAggregateKind::SumDistinct - | SimpleGroupedNumericAggregateKind::Avg - | SimpleGroupedNumericAggregateKind::AvgDistinct - | SimpleGroupedNumericAggregateKind::Total - | SimpleGroupedNumericAggregateKind::TotalDistinct => { - if let Some(source_column_index) = aggregate.source_column_index { - if matches!( - aggregate.kind, - SimpleGroupedNumericAggregateKind::SumDistinct - | SimpleGroupedNumericAggregateKind::AvgDistinct - | SimpleGroupedNumericAggregateKind::TotalDistinct - ) { - groups[group_index].add_numeric_distinct( - aggregate_index, - &values[source_column_index], - )?; - } else { - groups[group_index] - .add_numeric(aggregate_index, &values[source_column_index])?; - } - } else if let Some(source_expr) = aggregate.source_expr.as_ref() { - let value = self.eval_expr( - source_expr, - &group_dataset, - values, - params, - &BTreeMap::new(), - None, - )?; - if matches!( - aggregate.kind, - SimpleGroupedNumericAggregateKind::SumDistinct - | SimpleGroupedNumericAggregateKind::AvgDistinct - | SimpleGroupedNumericAggregateKind::TotalDistinct - ) { - groups[group_index] - .add_numeric_distinct(aggregate_index, &value)?; - } else { - groups[group_index].add_numeric(aggregate_index, &value)?; - } - } - } - SimpleGroupedNumericAggregateKind::StddevSamp - | SimpleGroupedNumericAggregateKind::StddevSampDistinct - | SimpleGroupedNumericAggregateKind::StddevPop - | SimpleGroupedNumericAggregateKind::StddevPopDistinct - | SimpleGroupedNumericAggregateKind::VarSamp - | SimpleGroupedNumericAggregateKind::VarSampDistinct - | SimpleGroupedNumericAggregateKind::VarPop - | SimpleGroupedNumericAggregateKind::VarPopDistinct => { - if let Some(source_column_index) = aggregate.source_column_index { - if aggregate.kind.uses_distinct() { - groups[group_index].add_variance_distinct( - aggregate_index, - &values[source_column_index], - )?; - } else { - groups[group_index] - .add_variance(aggregate_index, &values[source_column_index])?; - } - } else if let Some(source_expr) = aggregate.source_expr.as_ref() { - let value = self.eval_expr( - source_expr, - &group_dataset, - values, - params, - &BTreeMap::new(), - None, - )?; - if aggregate.kind.uses_distinct() { - groups[group_index] - .add_variance_distinct(aggregate_index, &value)?; - } else { - groups[group_index].add_variance(aggregate_index, &value)?; - } - } - } - SimpleGroupedNumericAggregateKind::BoolAnd - | SimpleGroupedNumericAggregateKind::BoolAndDistinct - | SimpleGroupedNumericAggregateKind::BoolOr - | SimpleGroupedNumericAggregateKind::BoolOrDistinct => { - if let Some(source_column_index) = aggregate.source_column_index { - if aggregate.kind.uses_distinct() { - groups[group_index].add_bool_distinct( - aggregate_index, - &values[source_column_index], - )?; - } else { - groups[group_index] - .add_bool(aggregate_index, &values[source_column_index])?; - } - } else if let Some(source_expr) = aggregate.source_expr.as_ref() { - let value = self.eval_expr( - source_expr, - &group_dataset, - values, - params, - &BTreeMap::new(), - None, - )?; - if aggregate.kind.uses_distinct() { - groups[group_index].add_bool_distinct(aggregate_index, &value)?; - } else { - groups[group_index].add_bool(aggregate_index, &value)?; - } - } - } - SimpleGroupedNumericAggregateKind::Min - | SimpleGroupedNumericAggregateKind::Max => { - let Some(source_expr) = aggregate.source_expr.as_ref() else { - continue; - }; - let value = self.eval_expr( - source_expr, - &group_dataset, - values, - params, - &BTreeMap::new(), - None, - )?; - update_simple_min_max_value( - &mut groups[group_index].extreme_values[aggregate_index], - value, - matches!(aggregate.kind, SimpleGroupedNumericAggregateKind::Max), - )?; - } - SimpleGroupedNumericAggregateKind::CountRows => {} - } - } - Ok(()) - })?; - - render_simple_grouped_numeric_aggregate_groups(self, groups, plan, params) - } - - fn try_simple_scalar_int64_numeric_aggregate_from_persisted_state( - &self, - store: &S, - state: PersistedTableState, - plan: &SimpleGroupedNumericAggregatePlan<'_>, - ) -> Result> { - let Some(column_index) = self.simple_scalar_int64_aggregate_column(plan) else { - return Ok(None); - }; - let mut stats = SimpleScalarInt64AggregateStats::default(); - visit_persisted_table_int64_column(store, state, column_index, |_, value| { - stats.add(value); - Ok(()) - })?; - Ok(Some(QueryResult::with_rows( - plan.column_names.clone(), - vec![QueryRow::new(stats.into_values(&plan.aggregate_bindings))], - ))) - } - - fn try_execute_simple_deferred_paged_grouped_count_query( - &self, - query: &Query, - params: &[Value], - pager: &PagerHandle, - wal: &WalHandle, - snapshot_lsn: u64, - ) -> Result> { - let Some(plan) = self.analyze_simple_grouped_count_query(query, params)? else { - return Ok(None); - }; - if self.visible_table_is_temporary(plan.table_name) - || self.visible_table_row_source(plan.table_name).is_some() - { - return Ok(None); - } - if let Some(result) = - self.try_simple_grouped_count_result_from_runtime_index(None, &plan, params)? - { - return Ok(Some(result)); - } - let Some(state) = self.persisted_table_state(plan.table_name) else { - return Ok(None); - }; - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - Ok(Some( - self.simple_grouped_count_result_from_persisted_state(&store, state, &plan, params)?, - )) - } - - pub(crate) fn try_execute_simple_deferred_paged_grouped_numeric_aggregate_query( - &self, - query: &Query, - params: &[Value], - pager: &PagerHandle, - wal: &WalHandle, - snapshot_lsn: u64, - ) -> Result> { - let Some(plan) = self.analyze_simple_grouped_numeric_aggregate_query(query, params)? else { - return Ok(None); - }; - if self.visible_table_is_temporary(plan.table_name) - || self.visible_table_row_source(plan.table_name).is_some() - { - return Ok(None); - } - let Some(state) = self.persisted_table_state(plan.table_name) else { - return Ok(None); - }; - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - Ok(Some( - self.simple_grouped_numeric_aggregate_result_from_persisted_state( - &store, state, &plan, params, - )?, - )) - } - - fn try_execute_general_grouped_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_general_grouped_single_table_query(query) else { - return Ok(None); - }; - let Some(source) = self.visible_table_row_source(plan.table_name) else { - return Ok(None); - }; - Ok(Some(self.execute_general_grouped_from_source( - source, &plan, params, - )?)) - } - - fn analyze_general_grouped_single_table_query<'a>( - &self, - query: &'a Query, - ) -> Option> { - if !query.ctes.is_empty() || query.recursive { - return None; - } - let QueryBody::Select(select) = &query.body else { - return None; - }; - if select.group_by.is_empty() && !projection_has_aggregate_items(&select.projection) { - return None; - } - if select.from.len() != 1 { - return None; - } - let FromItem::Table { name, alias } = &select.from[0] else { - return None; - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - { - return None; - } - self.table_schema(name)?; - if select.projection.iter().any(|item| { - matches!( - item, - SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) - ) - }) { - return None; - } - for item in &select.projection { - if let SelectItem::Expr { expr, .. } = item { - if expr_contains_window(expr) { - return None; - } - } - } - if select.having.as_ref().is_some_and(expr_contains_window) { - return None; - } - for order in &query.order_by { - if expr_contains_window(&order.expr) { - return None; - } - } - if select.filter.as_ref().is_some_and(expr_contains_window) { - return None; - } - for gb in &select.group_by { - if expr_contains_window(gb) { - return None; - } - } - Some(GeneralGroupedSingleTablePlan { - table_name: name, - table_alias: alias.as_deref(), - group_by: &select.group_by, - filter: select.filter.as_ref(), - projection: &select.projection, - having: select.having.as_ref(), - order_by: &query.order_by, - distinct: select.distinct, - limit: query.limit.as_ref(), - offset: query.offset.as_ref(), - }) - } - - fn execute_general_grouped_from_source( - &self, - source: VisibleTableRowSource<'_>, - plan: &GeneralGroupedSingleTablePlan<'_>, - params: &[Value], - ) -> Result { - let table = self.table_schema(plan.table_name).ok_or_else(|| { - DbError::internal(format!( - "table {} not found for general grouped query", - plan.table_name - )) - })?; - - let binding_name = plan.table_alias.unwrap_or(plan.table_name); - let columns: Vec = table - .columns - .iter() - .map(|c| ColumnBinding::visible(Some(binding_name.to_string()), c.name.clone())) - .collect(); - - let empty_dataset = Dataset::with_rows(columns.clone(), Vec::new()); - let ctes = BTreeMap::new(); - let needs_virtual_generated = !generated_columns_are_stored(table); - - // Materialize filtered rows into a single flat buffer and store only - // row indices per group. Previously this path stored full row clones - // per group and then cloned them again into a per-group `Dataset`; - // that doubled the copy cost and regressed aggregate-over-full-table - // queries by ~2x versus the prior `evaluate_grouped_select` path. - // Indexing into one shared Dataset matches that older path's - // efficiency while keeping the streaming scan from `source.rows()`. - let mut all_rows: Vec> = Vec::new(); - let mut groups: BTreeMap, Vec> = BTreeMap::new(); - - for row_result in source.rows() { - let row_ref = row_result?; - let mut values = row_ref.values().to_vec(); - if needs_virtual_generated { - self.apply_virtual_generated_columns(table, &mut values)?; - } - - if let Some(filter) = plan.filter { - let val = self.eval_expr(filter, &empty_dataset, &values, params, &ctes, None)?; - if !matches!(val, Value::Bool(true)) { - continue; - } - } - - let key_values: Vec = plan - .group_by - .iter() - .map(|expr| self.eval_expr(expr, &empty_dataset, &values, params, &ctes, None)) - .collect::>>()?; - let key = row_identity(&key_values)?; - - let row_index = all_rows.len(); - all_rows.push(values); - groups.entry(key).or_default().push(row_index); - } - - if groups.is_empty() && plan.group_by.is_empty() { - groups.insert(Vec::new(), Vec::new()); - } - - let result_columns: Vec = plan - .projection - .iter() - .enumerate() - .map(|(index, item)| match item { - SelectItem::Expr { expr, alias } => ColumnBinding::visible( - None, - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - ), - _ => ColumnBinding::visible(None, format!("col{}", index + 1)), - }) - .collect(); - - let has_order_by = !plan.order_by.is_empty(); - let projection_order_plan = projection_order_by_plan(plan.order_by, plan.projection); - let mut output_with_order: Vec<(Vec, Vec)> = Vec::new(); - - // Build a single shared dataset that every group indexes into. This - // replaces the per-group `Dataset::with_rows(columns.clone(), - // group_rows.clone())` that previously cloned every matching row - // twice. - let group_dataset = Dataset::with_rows(columns.clone(), all_rows); - - for group_row_indexes in groups.values() { - if let Some(having) = plan.having { - let val = - self.eval_group_expr(having, &group_dataset, group_row_indexes, params, &ctes)?; - if !matches!(val, Value::Bool(true)) { - continue; - } - } - - let mut output = Vec::with_capacity(plan.projection.len()); - for item in plan.projection { - let SelectItem::Expr { expr, .. } = item else { - return Err(DbError::sql( - "wildcards not supported in grouped SELECT output", - )); - }; - output.push(self.eval_group_expr( - expr, - &group_dataset, - group_row_indexes, - params, - &ctes, - )?); - } - - let order_values = if let Some(order_plan) = &projection_order_plan { - order_plan - .iter() - .map(|plan| output[plan.projection_index].clone()) - .collect() - } else if has_order_by { - plan.order_by - .iter() - .map(|order| { - self.eval_group_expr( - &order.expr, - &group_dataset, - group_row_indexes, - params, - &ctes, - ) - }) - .collect::>>()? - } else { - Vec::new() - }; - - output_with_order.push((output, order_values)); - } - - if has_order_by { - let mut sort_error = None; - output_with_order.sort_by(|(_, left_order), (_, right_order)| { - if let Some(order_plan) = &projection_order_plan { - let mut ord = std::cmp::Ordering::Equal; - for (i, plan) in order_plan.iter().enumerate() { - match compare_values_with_runtime_collation( - Some(self), - &left_order[i], - &right_order[i], - plan.collation.clone(), - ) { - Ok(std::cmp::Ordering::Equal) => continue, - Ok(o) => { - ord = if plan.descending { o.reverse() } else { o }; - break; - } - Err(error) => { - if sort_error.is_none() { - sort_error = Some(error); - } - break; - } - } - } - ord - } else { - match compare_query_row_order_values( - Some(self), - left_order, - right_order, - plan.order_by, - ) { - Ok(ordering) => ordering, - Err(error) => { - if sort_error.is_none() { - sort_error = Some(error); - } - std::cmp::Ordering::Equal - } - } - } - }); - if let Some(error) = sort_error { - return Err(error); - } - } - - let mut rows: Vec = if plan.distinct { - let mut seen = BTreeSet::new(); - let mut distinct_rows = Vec::new(); - for (output, _) in output_with_order { - if seen.insert(row_identity(&output)?) { - distinct_rows.push(QueryRow::new(output)); - } - } - distinct_rows - } else { - output_with_order - .into_iter() - .map(|(output, _)| QueryRow::new(output)) - .collect() - }; - - let offset_val = plan - .offset - .map(|expr| self.eval_constant_i64(expr, params, &ctes)) - .transpose()? - .unwrap_or(0); - let limit_val = plan - .limit - .map(|expr| self.eval_constant_i64(expr, params, &ctes)) - .transpose()?; - - let start = usize::try_from(offset_val.max(0)).unwrap_or(usize::MAX); - if start > 0 || limit_val.is_some() { - let take = limit_val - .map(|l| usize::try_from(l.max(0)).unwrap_or(0)) - .unwrap_or(usize::MAX); - rows = rows.into_iter().skip(start).take(take).collect(); - } - - let column_names: Vec = result_columns.into_iter().map(|c| c.name).collect(); - Ok(QueryResult::with_rows(column_names, rows)) - } - - pub(crate) fn try_execute_left_join_status_aggregate_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_left_join_status_aggregate_query(query, params)? else { - return Ok(None); - }; - let Some(parent_source) = self.visible_table_row_source(plan.parent_table_name) else { - return Ok(None); - }; - let Some(child_source) = self.visible_table_row_source(plan.child_table_name) else { - return Ok(None); - }; - - let bounded_order = plan - .order_by - .as_deref() - .zip(plan.limit) - .filter(|(_, _)| plan.offset == 0); - let child_index_keys = - plan.child_index_name - .as_deref() - .and_then(|index_name| match self.index(index_name) { - Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), - _ => None, - }); - - if let Some(keys) = child_index_keys { - let mut rows = Vec::new(); - for parent_row in parent_source.rows() { - let parent_row = parent_row?; - let parent_values = parent_row.values(); - let Some(join_value) = parent_values.get(plan.parent_join_index) else { - return Err(DbError::internal("parent join row is shorter than schema")); - }; - - let mut counts = LeftJoinStatusCounts::default(); - if !matches!(join_value, Value::Null) { - let child_row_ids = keys.row_ids_for_value_set(join_value)?; - match child_row_ids { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(child_row_id) => { - let Some(child_row) = child_source.row_by_id(child_row_id)? else { - return Err(DbError::internal( - "child index referenced missing row id", - )); - }; - add_status_aggregate_child_row(&mut counts, child_row.values(), &plan)?; - } - RuntimeRowIdSet::Contiguous { start, len } => { - for child_row_id in contiguous_row_ids(start, len) { - let Some(child_row) = child_source.row_by_id(child_row_id)? else { - return Err(DbError::internal( - "child index referenced missing row id", - )); - }; - add_status_aggregate_child_row( - &mut counts, - child_row.values(), - &plan, - )?; - } - } - RuntimeRowIdSet::Many(row_ids) => { - for child_row_id in row_ids { - let Some(child_row) = child_source.row_by_id(*child_row_id)? else { - return Err(DbError::internal( - "child index referenced missing row id", - )); - }; - add_status_aggregate_child_row( - &mut counts, - child_row.values(), - &plan, - )?; - } - } - RuntimeRowIdSet::Owned(row_ids) => { - for child_row_id in row_ids { - let Some(child_row) = child_source.row_by_id(child_row_id)? else { - return Err(DbError::internal( - "child index referenced missing row id", - )); - }; - add_status_aggregate_child_row( - &mut counts, - child_row.values(), - &plan, - )?; - } - } - } - } - - let mut output = Vec::with_capacity(plan.group_column_indexes.len() + 5); - for index in &plan.group_column_indexes { - output.push(parent_values[*index].clone()); - } - output.push(Value::Int64(counts.open_count)); - output.push(Value::Int64(counts.in_progress_count)); - output.push(Value::Int64(counts.resolved_count)); - output.push(Value::Int64(counts.closed_count)); - output.push(Value::Int64(counts.total_count)); - let row = QueryRow::new(output); - - if let Some((order_by, limit)) = bounded_order { - push_bounded_projection_ordered_query_row( - Some(self), - &mut rows, - row, - order_by, - limit, - )?; - } else { - rows.push(row); - } - } - - if let Some((order_by, _)) = bounded_order { - sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; - return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); - } - - return Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - plan.column_names, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - )?)); - } - - let mut counts_by_join_key = HashMap::, LeftJoinStatusCounts>::new(); - for child_row in child_source.rows() { - let child_row = child_row?; - let child_values = child_row.values(); - let Some(child_join_value) = child_values.get(plan.child_join_index) else { - return Err(DbError::internal("child join row is shorter than schema")); - }; - if matches!(child_join_value, Value::Null) { - continue; - } - let Some(child_status) = child_values.get(plan.child_status_index) else { - return Err(DbError::internal("child join row is shorter than schema")); - }; - let Some(child_id) = child_values.get(plan.child_id_index) else { - return Err(DbError::internal("child join row is shorter than schema")); - }; - counts_by_join_key - .entry(row_identity(std::slice::from_ref(child_join_value))?) - .or_default() - .add_child(child_status, child_id)?; - } - - let mut rows = Vec::new(); - for parent_row in parent_source.rows() { - let parent_row = parent_row?; - let parent_values = parent_row.values(); - let Some(join_value) = parent_values.get(plan.parent_join_index) else { - return Err(DbError::internal("parent join row is shorter than schema")); - }; - - let counts = if matches!(join_value, Value::Null) { - LeftJoinStatusCounts::default() - } else { - counts_by_join_key - .get(&row_identity(std::slice::from_ref(join_value))?) - .copied() - .unwrap_or_default() - }; - - let mut output = Vec::with_capacity(plan.group_column_indexes.len() + 5); - for index in &plan.group_column_indexes { - output.push(parent_values[*index].clone()); - } - output.push(Value::Int64(counts.open_count)); - output.push(Value::Int64(counts.in_progress_count)); - output.push(Value::Int64(counts.resolved_count)); - output.push(Value::Int64(counts.closed_count)); - output.push(Value::Int64(counts.total_count)); - let row = QueryRow::new(output); - - if let Some((order_by, limit)) = bounded_order { - push_bounded_projection_ordered_query_row( - Some(self), - &mut rows, - row, - order_by, - limit, - )?; - } else { - rows.push(row); - } - } - - if let Some((order_by, _)) = bounded_order { - sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; - return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); - } - - Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - plan.column_names, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - )?)) - } - - pub(crate) fn try_execute_left_join_aggregate_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_left_join_aggregate_query(query, params)? else { - return Ok(None); - }; - let Some(parent_source) = self.visible_table_row_source(plan.parent_table_name) else { - return Ok(None); - }; - let Some(child_source) = self.visible_table_row_source(plan.child_table_name) else { - return Ok(None); - }; - let child_index_keys = - plan.child_index_name - .as_deref() - .and_then(|index_name| match self.index(index_name) { - Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), - _ => None, - }); - - if child_index_keys.is_none() { - return Ok(None); - } - let keys = child_index_keys.unwrap(); - - let bounded_order = plan - .order_by - .as_deref() - .zip(plan.limit) - .filter(|(_, _)| plan.offset == 0); - let mut rows = Vec::new(); - - for parent_row in parent_source.rows() { - let parent_row = parent_row?; - let parent_values = parent_row.values(); - let Some(join_value) = parent_values.get(plan.parent_join_index) else { - return Err(DbError::internal("parent join row is shorter than schema")); - }; - - let mut state = IndexedJoinAggregateState::new(&plan.aggregate_kinds); - let mut matched_child = false; - - if !matches!(join_value, Value::Null) { - let child_row_ids = keys.row_ids_for_value_set(join_value)?; - match child_row_ids { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(child_row_id) => { - let Some(child_row) = child_source.row_by_id(child_row_id)? else { - return Err(DbError::internal("child index referenced missing row id")); - }; - matched_child = true; - state.accumulate(child_row.values())?; - } - RuntimeRowIdSet::Contiguous { start, len } => { - for child_row_id in contiguous_row_ids(start, len) { - let Some(child_row) = child_source.row_by_id(child_row_id)? else { - return Err(DbError::internal( - "child index referenced missing row id", - )); - }; - matched_child = true; - state.accumulate(child_row.values())?; - } - } - RuntimeRowIdSet::Many(row_ids) => { - for child_row_id in row_ids { - let Some(child_row) = child_source.row_by_id(*child_row_id)? else { - return Err(DbError::internal( - "child index referenced missing row id", - )); - }; - matched_child = true; - state.accumulate(child_row.values())?; - } - } - RuntimeRowIdSet::Owned(row_ids) => { - for child_row_id in row_ids { - let Some(child_row) = child_source.row_by_id(child_row_id)? else { - return Err(DbError::internal( - "child index referenced missing row id", - )); - }; - matched_child = true; - state.accumulate(child_row.values())?; - } - } - } - } - - if !matched_child && !plan.include_empty_parent { - continue; - } - - let mut output = - Vec::with_capacity(plan.group_column_indexes.len() + plan.aggregate_kinds.len()); - for index in &plan.group_column_indexes { - output.push(parent_values[*index].clone()); - } - state.finalize_into(&mut output); - let row = QueryRow::new(output); - - if let Some((order_by, limit)) = bounded_order { - push_bounded_projection_ordered_query_row( - Some(self), - &mut rows, - row, - order_by, - limit, - )?; - } else { - rows.push(row); - } - } - - if let Some((order_by, _)) = bounded_order { - sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; - return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); - } - - Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - plan.column_names, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - )?)) - } - - pub(crate) fn try_execute_three_table_genre_popularity_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_three_table_genre_popularity_query(query, params)? else { - return Ok(None); - }; - let Some(genre_source) = self.visible_table_row_source(plan.genre_table_name) else { - return Ok(None); - }; - let Some(bridge_source) = self.visible_table_row_source(plan.bridge_table_name) else { - return Ok(None); - }; - let Some(movie_source) = self.visible_table_row_source(plan.movie_table_name) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { - keys: bridge_keys, .. - }) = self.index(&plan.bridge_genre_index_name) - else { - return Ok(None); - }; - let movie_index_keys = - plan.movie_index_name - .as_deref() - .and_then(|index_name| match self.index(index_name) { - Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), - _ => None, - }); - if !plan.movie_id_is_rowid_alias && movie_index_keys.is_none() { - return Ok(None); - } - - let bounded_order = plan - .order_by - .as_deref() - .zip(plan.limit) - .filter(|(_, _)| plan.offset == 0); - let mut rows = Vec::new(); - - for genre_row in genre_source.rows() { - let genre_row = genre_row?; - let genre_values = genre_row.values(); - let Some(genre_id) = genre_values.get(plan.genre_id_index) else { - return Err(DbError::internal("genre row is shorter than schema")); - }; - if matches!(genre_id, Value::Null) { - continue; - } - - let mut movie_count = 0_i64; - let mut rating_sum = 0.0_f64; - let mut rating_count = 0_i64; - - let bridge_row_ids = bridge_keys.row_ids_for_value_set(genre_id)?; - match bridge_row_ids { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(row_id) => { - let Some(bridge_row) = bridge_source.row_by_id(row_id)? else { - return Err(DbError::internal( - "genre bridge index referenced missing row id", - )); - }; - accumulate_genre_popularity_movie( - &movie_source, - movie_index_keys, - plan.movie_id_is_rowid_alias, - bridge_row.values().get(plan.bridge_movie_id_index), - plan.movie_rating_index, - &mut movie_count, - &mut rating_sum, - &mut rating_count, - )?; - } - RuntimeRowIdSet::Contiguous { start, len } => { - for row_id in contiguous_row_ids(start, len) { - let Some(bridge_row) = bridge_source.row_by_id(row_id)? else { - return Err(DbError::internal( - "genre bridge index referenced missing row id", - )); - }; - accumulate_genre_popularity_movie( - &movie_source, - movie_index_keys, - plan.movie_id_is_rowid_alias, - bridge_row.values().get(plan.bridge_movie_id_index), - plan.movie_rating_index, - &mut movie_count, - &mut rating_sum, - &mut rating_count, - )?; - } - } - RuntimeRowIdSet::Many(row_ids) => { - for row_id in row_ids { - let Some(bridge_row) = bridge_source.row_by_id(*row_id)? else { - return Err(DbError::internal( - "genre bridge index referenced missing row id", - )); - }; - accumulate_genre_popularity_movie( - &movie_source, - movie_index_keys, - plan.movie_id_is_rowid_alias, - bridge_row.values().get(plan.bridge_movie_id_index), - plan.movie_rating_index, - &mut movie_count, - &mut rating_sum, - &mut rating_count, - )?; - } - } - RuntimeRowIdSet::Owned(row_ids) => { - for row_id in row_ids { - let Some(bridge_row) = bridge_source.row_by_id(row_id)? else { - return Err(DbError::internal( - "genre bridge index referenced missing row id", - )); - }; - accumulate_genre_popularity_movie( - &movie_source, - movie_index_keys, - plan.movie_id_is_rowid_alias, - bridge_row.values().get(plan.bridge_movie_id_index), - plan.movie_rating_index, - &mut movie_count, - &mut rating_sum, - &mut rating_count, - )?; - } - } - } - - if movie_count == 0 { - continue; - } - let avg_rating = if rating_count == 0 { - Value::Null - } else { - Value::Float64(rating_sum / rating_count as f64) - }; - let Some(name) = genre_values.get(plan.genre_name_index) else { - return Err(DbError::internal("genre name row is shorter than schema")); - }; - let row = QueryRow::new(vec![name.clone(), Value::Int64(movie_count), avg_rating]); - if let Some((order_by, limit)) = bounded_order { - push_bounded_projection_ordered_query_row( - Some(self), - &mut rows, - row, - order_by, - limit, - )?; - } else { - rows.push(row); - } - } - - if let Some((order_by, _)) = bounded_order { - sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; - return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); - } - - Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - plan.column_names, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - )?)) - } - - pub(crate) fn try_execute_movie_tag_search_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_movie_tag_search_query(query, params)? else { - return Ok(None); - }; - let Some(tag_source) = self.visible_table_row_source(plan.tag_table_name) else { - return Ok(None); - }; - let Some(bridge_source) = self.visible_table_row_source(plan.bridge_table_name) else { - return Ok(None); - }; - let Some(movie_source) = self.visible_table_row_source(plan.movie_table_name) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { - keys: tag_name_keys, - .. - }) = self.index(&plan.tag_name_index_name) - else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { - keys: bridge_tag_keys, - .. - }) = self.index(&plan.bridge_tag_index_name) - else { - return Ok(None); - }; - let movie_index_keys = - plan.movie_index_name - .as_deref() - .and_then(|index_name| match self.index(index_name) { - Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), - _ => None, - }); - if !plan.movie_id_is_rowid_alias && movie_index_keys.is_none() { - return Ok(None); - } - - if plan.limit == Some(0) { - return Ok(Some(QueryResult::with_rows(plan.column_names, Vec::new()))); - } - let bounded_order = plan - .order_by - .as_deref() - .zip(plan.limit) - .filter(|(_, _)| plan.offset == 0); - let mut rows = plan.limit.map_or_else(Vec::new, Vec::with_capacity); - - let mut visit_tag_row = |tag_row: TableRowRef<'_>| -> Result<()> { - let Some(tag_id) = tag_row.values().get(plan.tag_id_index) else { - return Err(DbError::internal("movie tag search tag id column missing")); - }; - if matches!(tag_id, Value::Null) { - return Ok(()); - } - - match bridge_tag_keys.row_ids_for_value_set(tag_id)? { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(row_id) => { - let Some(bridge_row) = bridge_source.row_by_id(row_id)? else { - return Err(DbError::internal( - "movie tag bridge index referenced missing row id", - )); - }; - push_movie_tag_search_movie_rows( - self, - &movie_source, - movie_index_keys, - plan.movie_id_is_rowid_alias, - bridge_row.values().get(plan.bridge_movie_id_index), - &plan.projection_indexes, - bounded_order, - &mut rows, - )?; - } - RuntimeRowIdSet::Contiguous { start, len } => { - for row_id in contiguous_row_ids(start, len) { - let Some(bridge_row) = bridge_source.row_by_id(row_id)? else { - return Err(DbError::internal( - "movie tag bridge index referenced missing row id", - )); - }; - push_movie_tag_search_movie_rows( - self, - &movie_source, - movie_index_keys, - plan.movie_id_is_rowid_alias, - bridge_row.values().get(plan.bridge_movie_id_index), - &plan.projection_indexes, - bounded_order, - &mut rows, - )?; - } - } - RuntimeRowIdSet::Many(row_ids) => { - for row_id in row_ids { - let Some(bridge_row) = bridge_source.row_by_id(*row_id)? else { - return Err(DbError::internal( - "movie tag bridge index referenced missing row id", - )); - }; - push_movie_tag_search_movie_rows( - self, - &movie_source, - movie_index_keys, - plan.movie_id_is_rowid_alias, - bridge_row.values().get(plan.bridge_movie_id_index), - &plan.projection_indexes, - bounded_order, - &mut rows, - )?; - } - } - RuntimeRowIdSet::Owned(row_ids) => { - for row_id in row_ids { - let Some(bridge_row) = bridge_source.row_by_id(row_id)? else { - return Err(DbError::internal( - "movie tag bridge index referenced missing row id", - )); - }; - push_movie_tag_search_movie_rows( - self, - &movie_source, - movie_index_keys, - plan.movie_id_is_rowid_alias, - bridge_row.values().get(plan.bridge_movie_id_index), - &plan.projection_indexes, - bounded_order, - &mut rows, - )?; - } - } - } - Ok(()) - }; - - match tag_name_keys.row_ids_for_value_set(&plan.tag_name_value)? { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(row_id) => { - if let Some(tag_row) = tag_source.row_by_id(row_id)? { - visit_tag_row(tag_row)?; - } - } - RuntimeRowIdSet::Contiguous { start, len } => { - for row_id in contiguous_row_ids(start, len) { - if let Some(tag_row) = tag_source.row_by_id(row_id)? { - visit_tag_row(tag_row)?; - } - } - } - RuntimeRowIdSet::Many(row_ids) => { - for row_id in row_ids { - if let Some(tag_row) = tag_source.row_by_id(*row_id)? { - visit_tag_row(tag_row)?; - } - } - } - RuntimeRowIdSet::Owned(row_ids) => { - for row_id in row_ids { - if let Some(tag_row) = tag_source.row_by_id(row_id)? { - visit_tag_row(tag_row)?; - } - } - } - } - - if let Some((order_by, _)) = bounded_order { - sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; - return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); - } - - Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - plan.column_names, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - )?)) - } - - fn analyze_movie_tag_search_query<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result>> { - if !query.ctes.is_empty() || query.recursive { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || select.having.is_some() - || !select.group_by.is_empty() - || select.from.len() != 1 - { - return Ok(None); - } - - let mut tables = Vec::new(); - let mut constraints = Vec::new(); - if !flatten_inner_join_chain(&select.from[0], &mut tables, &mut constraints) - || tables.len() != 3 - { - return Ok(None); - } - let tag_binding = tables - .iter() - .copied() - .find(|binding| identifiers_equal(binding.name, "tags")); - let bridge_binding = tables - .iter() - .copied() - .find(|binding| identifiers_equal(binding.name, "movietags")); - let movie_binding = tables - .iter() - .copied() - .find(|binding| identifiers_equal(binding.name, "movies")); - let (Some(tag_binding), Some(bridge_binding), Some(movie_binding)) = - (tag_binding, bridge_binding, movie_binding) - else { - return Ok(None); - }; - - if [tag_binding.name, bridge_binding.name, movie_binding.name] - .iter() - .any(|table| { - self.visible_view(table, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(table) - }) - { - return Ok(None); - } - let Some(tag_schema) = self.table_schema(tag_binding.name) else { - return Ok(None); - }; - let Some(bridge_schema) = self.table_schema(bridge_binding.name) else { - return Ok(None); - }; - let Some(movie_schema) = self.table_schema(movie_binding.name) else { - return Ok(None); - }; - if !generated_columns_are_stored(tag_schema) - || !generated_columns_are_stored(bridge_schema) - || !generated_columns_are_stored(movie_schema) - { - return Ok(None); - } - - let Some(filter) = select.filter.as_ref() else { - return Ok(None); - }; - let Some((filter_table, filter_column, tag_name_expr)) = simple_btree_lookup(filter) else { - return Ok(None); - }; - if !matches_table_binding(tag_binding, filter_table) - || !identifiers_equal(filter_column, "name") - { - return Ok(None); - } - let tag_name_value = self.eval_expr( - tag_name_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?; - - if !join_constraints_match_columns(&constraints, tag_binding, "id", bridge_binding, "tagid") - || !join_constraints_match_columns( - &constraints, - movie_binding, - "id", - bridge_binding, - "movieid", - ) - { - return Ok(None); - } - - let tag_id_index = schema_column_index(tag_schema, "id") - .ok_or_else(|| DbError::internal("movie tag search id column missing from tags"))?; - let bridge_movie_id_index = - schema_column_index(bridge_schema, "movieid").ok_or_else(|| { - DbError::internal("movie tag search movie id column missing from MovieTags") - })?; - - let Some(tag_name_index_name) = self - .single_column_btree_index(tag_binding.name, "name") - .map(|index| index.name.clone()) - else { - return Ok(None); - }; - let Some(bridge_tag_index_name) = self - .single_column_btree_index(bridge_binding.name, "tagid") - .map(|index| index.name.clone()) - else { - return Ok(None); - }; - let movie_index_name = self - .single_column_btree_index(movie_binding.name, "id") - .map(|index| index.name.clone()); - let movie_id_is_rowid_alias = row_id_alias_column_name(movie_schema) - .is_some_and(|column| identifiers_equal(column, "id")); - if !movie_id_is_rowid_alias && movie_index_name.is_none() { - return Ok(None); - } - - let Some((projection_indexes, column_names)) = self.simple_projection_plan( - select, - movie_binding.name, - movie_binding.alias, - movie_schema, - ) else { - return Ok(None); - }; - let order_by = self.simple_projection_order_by_plan( - query, - movie_schema, - movie_binding.name, - movie_binding.binding_name(), - &projection_indexes, - )?; - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - - Ok(Some(MovieTagSearchPlan { - tag_table_name: tag_binding.name, - tag_id_index, - tag_name_index_name, - tag_name_value, - bridge_table_name: bridge_binding.name, - bridge_movie_id_index, - bridge_tag_index_name, - movie_table_name: movie_binding.name, - movie_index_name, - movie_id_is_rowid_alias, - projection_indexes, - column_names, - order_by, - limit, - offset, - })) - } - - pub(crate) fn try_execute_movie_watchlist_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_movie_watchlist_query(query, params)? else { - return Ok(None); - }; - let Some(watchlist_source) = self.visible_table_row_source(plan.watchlist_table_name) - else { - return Ok(None); - }; - let Some(movie_source) = self.visible_table_row_source(plan.movie_table_name) else { - return Ok(None); - }; - let Some(review_source) = self.visible_table_row_source(plan.review_table_name) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { - keys: watchlist_user_keys, - .. - }) = self.index(&plan.watchlist_user_index_name) - else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { - keys: review_movie_keys, - .. - }) = self.index(&plan.review_movie_index_name) - else { - return Ok(None); - }; - let movie_index_keys = - plan.movie_index_name - .as_deref() - .and_then(|index_name| match self.index(index_name) { - Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), - _ => None, - }); - if !plan.movie_id_is_rowid_alias && movie_index_keys.is_none() { - return Ok(None); - } - - if plan.limit == Some(0) { - return Ok(Some(QueryResult::with_rows(plan.column_names, Vec::new()))); - } - let mut groups = BTreeMap::, QueryRow>::new(); - - let mut visit_watchlist_row = |watchlist_row: TableRowRef<'_>| -> Result<()> { - let watchlist_values = watchlist_row.values(); - let Some(movie_id) = watchlist_values.get(plan.watchlist_movie_id_index) else { - return Err(DbError::internal( - "movie watchlist movie id column missing from Watchlist", - )); - }; - if matches!(movie_id, Value::Null) { - return Ok(()); - } - let Some(priority) = watchlist_values.get(plan.watchlist_priority_index) else { - return Err(DbError::internal( - "movie watchlist priority column missing from Watchlist", - )); - }; - insert_movie_watchlist_group_rows( - &movie_source, - movie_index_keys, - plan.movie_id_is_rowid_alias, - movie_id, - priority, - &review_source, - review_movie_keys, - plan.movie_id_index, - plan.movie_title_index, - plan.review_score_index, - &mut groups, - ) - }; - - match watchlist_user_keys.row_ids_for_value_set(&plan.user_handle_value)? { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(row_id) => { - if let Some(watchlist_row) = watchlist_source.row_by_id(row_id)? { - visit_watchlist_row(watchlist_row)?; - } - } - RuntimeRowIdSet::Contiguous { start, len } => { - for row_id in contiguous_row_ids(start, len) { - if let Some(watchlist_row) = watchlist_source.row_by_id(row_id)? { - visit_watchlist_row(watchlist_row)?; - } - } - } - RuntimeRowIdSet::Many(row_ids) => { - for row_id in row_ids { - if let Some(watchlist_row) = watchlist_source.row_by_id(*row_id)? { - visit_watchlist_row(watchlist_row)?; - } - } - } - RuntimeRowIdSet::Owned(row_ids) => { - for row_id in row_ids { - if let Some(watchlist_row) = watchlist_source.row_by_id(row_id)? { - visit_watchlist_row(watchlist_row)?; - } - } - } - } - - let rows = groups.into_values().collect::>(); - Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - plan.column_names, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - )?)) - } - - fn analyze_movie_watchlist_query<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result>> { - if !query.ctes.is_empty() || query.recursive { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || select.having.is_some() - || select.group_by.len() != 1 - || select.projection.len() != 4 - || select.from.len() != 1 - { - return Ok(None); - } - - let FromItem::Join { - left, - right, - kind: JoinKind::Left, - constraint: JoinConstraint::On(review_join), - } = &select.from[0] - else { - return Ok(None); - }; - let FromItem::Join { - left: watchlist_item, - right: movie_item, - kind: JoinKind::Inner, - constraint: JoinConstraint::On(movie_join), - } = &**left - else { - return Ok(None); - }; - let FromItem::Table { - name: watchlist_name, - alias: watchlist_alias, - } = &**watchlist_item - else { - return Ok(None); - }; - let FromItem::Table { - name: movie_name, - alias: movie_alias, - } = &**movie_item - else { - return Ok(None); - }; - let FromItem::Table { - name: review_name, - alias: review_alias, - } = &**right - else { - return Ok(None); - }; - if !identifiers_equal(watchlist_name, "watchlist") - || !identifiers_equal(movie_name, "movies") - || !identifiers_equal(review_name, "reviews") - { - return Ok(None); - } - - if [ - watchlist_name.as_str(), - movie_name.as_str(), - review_name.as_str(), - ] - .iter() - .any(|table| { - self.visible_view(table, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(table) - }) { - return Ok(None); - } - let Some(watchlist_schema) = self.table_schema(watchlist_name) else { - return Ok(None); - }; - let Some(movie_schema) = self.table_schema(movie_name) else { - return Ok(None); - }; - let Some(review_schema) = self.table_schema(review_name) else { - return Ok(None); - }; - if !generated_columns_are_stored(watchlist_schema) - || !generated_columns_are_stored(movie_schema) - || !generated_columns_are_stored(review_schema) - { - return Ok(None); - } - - let watchlist_binding = TableBindingRef { - name: watchlist_name, - alias: watchlist_alias, - }; - let movie_binding = TableBindingRef { - name: movie_name, - alias: movie_alias, - }; - let review_binding = TableBindingRef { - name: review_name, - alias: review_alias, - }; - - if !join_constraint_matches_columns( - movie_join, - movie_binding, - "id", - watchlist_binding, - "movieid", - ) || !join_constraint_matches_columns( - review_join, - review_binding, - "movieid", - movie_binding, - "id", - ) { - return Ok(None); - } - let Some(filter) = select.filter.as_ref() else { - return Ok(None); - }; - let Some((filter_table, filter_column, user_handle_expr)) = simple_btree_lookup(filter) - else { - return Ok(None); - }; - if !matches_table_binding(watchlist_binding, filter_table) - || !identifiers_equal(filter_column, "userhandle") - { - return Ok(None); - } - let user_handle_value = self.eval_expr( - user_handle_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?; - - if !projection_expr_matches_binding_column(&select.projection[0], movie_binding, "id") - || !projection_expr_matches_binding_column( - &select.projection[1], - movie_binding, - "title", - ) - || !projection_expr_matches_binding_column( - &select.projection[2], - watchlist_binding, - "priority", - ) - || !matches!( - &select.projection[3], - SelectItem::Expr { expr, .. } - if aggregate_matches_single_binding_column(expr, "avg", review_binding, "score") - ) - || !expr_matches_binding_column(&select.group_by[0], movie_binding, "id") - { - return Ok(None); - } - - let watchlist_movie_id_index = schema_column_index(watchlist_schema, "movieid") - .ok_or_else(|| { - DbError::internal("movie watchlist movie id column missing from Watchlist") - })?; - let watchlist_priority_index = schema_column_index(watchlist_schema, "priority") - .ok_or_else(|| { - DbError::internal("movie watchlist priority column missing from Watchlist") - })?; - let movie_id_index = schema_column_index(movie_schema, "id") - .ok_or_else(|| DbError::internal("movie watchlist id column missing from Movies"))?; - let movie_title_index = schema_column_index(movie_schema, "title") - .ok_or_else(|| DbError::internal("movie watchlist title column missing from Movies"))?; - let review_score_index = schema_column_index(review_schema, "score").ok_or_else(|| { - DbError::internal("movie watchlist score column missing from Reviews") - })?; - - let Some(watchlist_user_index_name) = self - .single_column_btree_index(watchlist_name, "userhandle") - .map(|index| index.name.clone()) - else { - return Ok(None); - }; - let Some(review_movie_index_name) = self - .single_column_btree_index(review_name, "movieid") - .map(|index| index.name.clone()) - else { - return Ok(None); - }; - let movie_index_name = self - .single_column_btree_index(movie_name, "id") - .map(|index| index.name.clone()); - let movie_id_is_rowid_alias = row_id_alias_column_name(movie_schema) - .is_some_and(|column| identifiers_equal(column, "id")); - if !movie_id_is_rowid_alias && movie_index_name.is_none() { - return Ok(None); - } - - let column_names = select - .projection - .iter() - .enumerate() - .map(|(index, item)| match item { - SelectItem::Expr { expr, alias } => alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => { - format!("col{}", index + 1) - } - }) - .collect::>(); - let order_by = projection_order_by_plan(&query.order_by, &select.projection); - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - - Ok(Some(MovieWatchlistPlan { - watchlist_table_name: watchlist_name, - watchlist_movie_id_index, - watchlist_priority_index, - watchlist_user_index_name, - user_handle_value, - movie_table_name: movie_name, - movie_id_index, - movie_title_index, - movie_index_name, - movie_id_is_rowid_alias, - review_table_name: review_name, - review_score_index, - review_movie_index_name, - column_names, - order_by, - limit, - offset, - })) - } - - pub(crate) fn try_execute_movie_top_rated_by_year_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_movie_top_rated_by_year_query(query, params)? else { - return Ok(None); - }; - let Some(movie_source) = self.visible_table_row_source(plan.movie_table_name) else { - return Ok(None); - }; - let Some(review_source) = self.visible_table_row_source(plan.review_table_name) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { - keys: review_movie_keys, - .. - }) = self.index(&plan.review_movie_index_name) - else { - return Ok(None); - }; - let movie_release_year_keys = - plan.movie_release_year_index_name - .as_deref() - .and_then(|index_name| match self.index(index_name) { - Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), - _ => None, - }); - - if plan.limit == Some(0) { - return Ok(Some(QueryResult::with_rows(plan.column_names, Vec::new()))); - } - let bounded_order = plan - .order_by - .as_deref() - .zip(plan.limit) - .filter(|(_, _)| plan.offset == 0); - let mut rows = Vec::new(); - - let mut visit_movie_row = |movie_row: TableRowRef<'_>| -> Result<()> { - let movie_values = movie_row.values(); - let Some(movie_id) = movie_values.get(plan.movie_id_index) else { - return Err(DbError::internal( - "movie top-rated id column missing from Movies", - )); - }; - let (review_count, score_sum) = movie_review_score_stats( - &review_source, - review_movie_keys, - movie_id, - plan.review_score_index, - )?; - if review_count < plan.min_review_count { - return Ok(()); - } - let avg_score = if review_count == 0 { - Value::Null - } else { - Value::Float64(score_sum / review_count as f64) - }; - let projected = - project_simple_projection_values(movie_values, &plan.movie_projection_indexes); - let mut values = projected.values().to_vec(); - values.push(avg_score); - values.push(Value::Int64(review_count)); - let row = QueryRow::new(values); - if let Some((order_by, limit)) = bounded_order { - push_bounded_projection_ordered_query_row( - Some(self), - &mut rows, - row, - order_by, - limit, - )?; - } else { - rows.push(row); - } - Ok(()) - }; - - if let Some(keys) = movie_release_year_keys { - match keys.row_ids_for_value_set(&plan.release_year_value)? { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(row_id) => { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - visit_movie_row(movie_row)?; - } - } - RuntimeRowIdSet::Contiguous { start, len } => { - for row_id in contiguous_row_ids(start, len) { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - visit_movie_row(movie_row)?; - } - } - } - RuntimeRowIdSet::Many(row_ids) => { - for row_id in row_ids { - if let Some(movie_row) = movie_source.row_by_id(*row_id)? { - visit_movie_row(movie_row)?; - } - } - } - RuntimeRowIdSet::Owned(row_ids) => { - for row_id in row_ids { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - visit_movie_row(movie_row)?; - } - } - } - } - } else { - for movie_row in movie_source.rows() { - let movie_row = movie_row?; - let Some(release_year) = movie_row.values().get(plan.movie_release_year_index) - else { - return Err(DbError::internal( - "movie top-rated release year column missing from Movies", - )); - }; - if compare_values(release_year, &plan.release_year_value)? - != std::cmp::Ordering::Equal - { - continue; - } - visit_movie_row(movie_row)?; - } - } - - if let Some((order_by, _)) = bounded_order { - sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; - return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); - } - - Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - plan.column_names, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - )?)) - } - - fn analyze_movie_top_rated_by_year_query<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result>> { - if !query.ctes.is_empty() || query.recursive { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || select.group_by.len() != 1 - || select.projection.len() != 11 - || select.from.len() != 1 - { - return Ok(None); - } - - let FromItem::Join { - left, - right, - kind: JoinKind::Inner, - constraint: JoinConstraint::On(join_on), - } = &select.from[0] - else { - return Ok(None); - }; - let ( - FromItem::Table { - name: movie_name, - alias: movie_alias, - }, - FromItem::Table { - name: review_name, - alias: review_alias, - }, - ) = (&**left, &**right) - else { - return Ok(None); - }; - if !identifiers_equal(movie_name, "movies") || !identifiers_equal(review_name, "reviews") { - return Ok(None); - } - if [movie_name.as_str(), review_name.as_str()] - .iter() - .any(|table| { - self.visible_view(table, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(table) - }) - { - return Ok(None); - } - let Some(movie_schema) = self.table_schema(movie_name) else { - return Ok(None); - }; - let Some(review_schema) = self.table_schema(review_name) else { - return Ok(None); - }; - if !generated_columns_are_stored(movie_schema) - || !generated_columns_are_stored(review_schema) - { - return Ok(None); - } - - let movie_binding = TableBindingRef { - name: movie_name, - alias: movie_alias, - }; - let review_binding = TableBindingRef { - name: review_name, - alias: review_alias, - }; - if !join_constraint_matches_columns(join_on, review_binding, "movieid", movie_binding, "id") - || !expr_matches_binding_column(&select.group_by[0], movie_binding, "id") - { - return Ok(None); - } - - let Some(filter) = select.filter.as_ref() else { - return Ok(None); - }; - let Some((filter_table, filter_column, release_year_expr)) = simple_btree_lookup(filter) - else { - return Ok(None); - }; - if !matches_table_binding(movie_binding, filter_table) - || !identifiers_equal(filter_column, "releaseyear") - { - return Ok(None); - } - let release_year_value = self.eval_expr( - release_year_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?; - - let min_review_count = match select.having.as_ref() { - Some(Expr::Binary { - left, - op: BinaryOp::GtEq, - right, - }) if aggregate_matches_single_binding_column(left, "count", review_binding, "id") => { - self.eval_constant_i64(right, params, &BTreeMap::new())? - } - _ => return Ok(None), - }; - - let movie_columns = [ - "id", - "title", - "releaseyear", - "synopsis", - "budgetusd", - "boxofficeusd", - "mpaarating", - "runtimeminutes", - "addedat", - ]; - let mut movie_projection_indexes = Vec::with_capacity(movie_columns.len()); - let mut column_names = Vec::with_capacity(select.projection.len()); - for (index, column) in movie_columns.iter().enumerate() { - if !projection_expr_matches_binding_column( - &select.projection[index], - movie_binding, - column, - ) { - return Ok(None); - } - let column_index = schema_column_index(movie_schema, column).ok_or_else(|| { - DbError::internal(format!( - "movie top-rated column {column} missing from Movies" - )) - })?; - movie_projection_indexes.push(column_index); - if let SelectItem::Expr { expr, alias } = &select.projection[index] { - column_names.push( - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - ); - } - } - let SelectItem::Expr { - expr: avg_expr, - alias: avg_alias, - } = &select.projection[9] - else { - return Ok(None); - }; - let SelectItem::Expr { - expr: count_expr, - alias: count_alias, - } = &select.projection[10] - else { - return Ok(None); - }; - if !aggregate_matches_single_binding_column(avg_expr, "avg", review_binding, "score") - || !aggregate_matches_single_binding_column(count_expr, "count", review_binding, "id") - { - return Ok(None); - } - column_names.push( - avg_alias - .clone() - .unwrap_or_else(|| infer_expr_name(avg_expr, 10)), - ); - column_names.push( - count_alias - .clone() - .unwrap_or_else(|| infer_expr_name(count_expr, 11)), - ); - - let movie_id_index = schema_column_index(movie_schema, "id") - .ok_or_else(|| DbError::internal("movie top-rated id column missing from Movies"))?; - let movie_release_year_index = schema_column_index(movie_schema, "releaseyear") - .ok_or_else(|| { - DbError::internal("movie top-rated ReleaseYear column missing from Movies") - })?; - let review_score_index = schema_column_index(review_schema, "score").ok_or_else(|| { - DbError::internal("movie top-rated Score column missing from Reviews") - })?; - let Some(review_movie_index_name) = self - .single_column_btree_index(review_name, "movieid") - .map(|index| index.name.clone()) - else { - return Ok(None); - }; - let movie_release_year_index_name = self - .single_column_btree_index(movie_name, "releaseyear") - .map(|index| index.name.clone()); - - let order_by = projection_order_by_plan(&query.order_by, &select.projection); - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - - Ok(Some(MovieTopRatedByYearPlan { - movie_table_name: movie_name, - movie_id_index, - movie_release_year_index, - movie_release_year_index_name, - movie_projection_indexes, - release_year_value, - review_table_name: review_name, - review_score_index, - review_movie_index_name, - min_review_count, - column_names, - order_by, - limit, - offset, - })) - } - - pub(crate) fn try_execute_movie_busiest_people_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_movie_busiest_people_query(query, params)? else { - return Ok(None); - }; - let Some(people_source) = self.visible_table_row_source(plan.people_table_name) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { - keys: roles_person_keys, - .. - }) = self.index(&plan.roles_person_index_name) - else { - return Ok(None); - }; - let people_index_keys = plan - .people_index_name - .as_deref() - .and_then(|index_name| match self.index(index_name) { - Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), - _ => None, - }); - if !plan.people_id_is_rowid_alias && people_index_keys.is_none() { - return Ok(None); - } - - if plan.limit == Some(0) { - return Ok(Some(QueryResult::with_rows(plan.column_names, Vec::new()))); - } - - let bounded_count = plan.limit.map(|limit| limit.saturating_add(plan.offset)); - let mut counts = Vec::new(); - for (person_key, role_count) in roles_person_keys.distinct_key_counts() { - if role_count == 0 { - continue; - } - let role_count = i64::try_from(role_count).map_err(|_| { - DbError::sql("role count for person exceeds INT64 limits".to_string()) - })?; - let candidate = MovieBusiestPeopleCount { - person_key, - role_count, - }; - if let Some(bounded_count) = bounded_count { - push_bounded_movie_busiest_people_count(&mut counts, candidate, bounded_count); - } else { - counts.push(candidate); - } - } - sort_movie_busiest_people_counts(&mut counts); - - let take = plan.limit.unwrap_or(usize::MAX); - let mut rows = Vec::with_capacity(take.min(counts.len())); - for candidate in counts.into_iter().skip(plan.offset).take(take) { - push_movie_busiest_people_row( - &people_source, - people_index_keys, - plan.people_id_is_rowid_alias, - &candidate, - &plan.people_projection_indexes, - &mut rows, - )?; - } - - Ok(Some(QueryResult::with_rows(plan.column_names, rows))) - } - - fn analyze_movie_busiest_people_query<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result>> { - if !query.ctes.is_empty() || query.recursive { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || select.filter.is_some() - || select.having.is_some() - || select.group_by.len() != 1 - || select.projection.len() != 5 - || select.from.len() != 1 - { - return Ok(None); - } - - let FromItem::Join { - left, - right, - kind: JoinKind::Inner, - constraint: JoinConstraint::On(join_on), - } = &select.from[0] - else { - return Ok(None); - }; - let ( - FromItem::Table { - name: left_name, - alias: left_alias, - }, - FromItem::Table { - name: right_name, - alias: right_alias, - }, - ) = (&**left, &**right) - else { - return Ok(None); - }; - - let left_binding = TableBindingRef { - name: left_name, - alias: left_alias, - }; - let right_binding = TableBindingRef { - name: right_name, - alias: right_alias, - }; - let (people_binding, roles_binding) = if identifiers_equal(left_name, "people") - && identifiers_equal(right_name, "roles") - { - (left_binding, right_binding) - } else if identifiers_equal(left_name, "roles") && identifiers_equal(right_name, "people") { - (right_binding, left_binding) - } else { - return Ok(None); - }; - let people_name = people_binding.name; - let roles_name = roles_binding.name; - - if [people_name, roles_name].iter().any(|table| { - self.visible_view(table, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(table) - }) { - return Ok(None); - } - let Some(people_schema) = self.table_schema(people_name) else { - return Ok(None); - }; - let Some(roles_schema) = self.table_schema(roles_name) else { - return Ok(None); - }; - if !generated_columns_are_stored(people_schema) - || !generated_columns_are_stored(roles_schema) - { - return Ok(None); - } - - if !join_constraint_matches_columns( - join_on, - roles_binding, - "personid", - people_binding, - "id", - ) || !expr_matches_binding_column_or_unqualified( - &select.group_by[0], - people_binding, - "id", - ) { - return Ok(None); - } - - let people_columns = ["id", "fullname", "birthdate", "biography"]; - let mut people_projection_indexes = Vec::with_capacity(people_columns.len()); - let mut column_names = Vec::with_capacity(select.projection.len()); - for (index, column) in people_columns.iter().enumerate() { - if !projection_expr_matches_binding_column( - &select.projection[index], - people_binding, - column, - ) { - return Ok(None); - } - let column_index = schema_column_index(people_schema, column).ok_or_else(|| { - DbError::internal(format!( - "movie busiest people column {column} missing from People" - )) - })?; - people_projection_indexes.push(column_index); - if let SelectItem::Expr { expr, alias } = &select.projection[index] { - column_names.push( - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - ); - } - } - - let SelectItem::Expr { - expr: count_expr, - alias: count_alias, - } = &select.projection[4] - else { - return Ok(None); - }; - if !aggregate_matches_single_binding_column(count_expr, "count", roles_binding, "id") { - return Ok(None); - } - column_names.push( - count_alias - .clone() - .unwrap_or_else(|| infer_expr_name(count_expr, 5)), - ); - - let roles_id_index = schema_column_index(roles_schema, "id").ok_or_else(|| { - DbError::internal("movie busiest people id column missing from Roles") - })?; - if schema_column_index(people_schema, "id").is_none() { - return Err(DbError::internal( - "movie busiest people id column missing from People", - )); - } - let roles_person_id_index = - schema_column_index(roles_schema, "personid").ok_or_else(|| { - DbError::internal("movie busiest people PersonId column missing from Roles") - })?; - if roles_schema.columns[roles_id_index].nullable - && !roles_schema.columns[roles_id_index].primary_key - { - return Ok(None); - } - if roles_schema.columns[roles_person_id_index].nullable - || !table_has_single_column_foreign_key(roles_schema, "personid", people_schema, "id") - { - return Ok(None); - } - - let Some(roles_person_index_name) = self - .single_column_btree_index(roles_name, "personid") - .map(|index| index.name.clone()) - else { - return Ok(None); - }; - let people_index_name = self - .single_column_btree_index(people_name, "id") - .map(|index| index.name.clone()); - let people_id_is_rowid_alias = row_id_alias_column_name(people_schema) - .is_some_and(|column| identifiers_equal(column, "id")); - if !people_id_is_rowid_alias && people_index_name.is_none() { - return Ok(None); - } - - let Some(order_by) = projection_order_by_plan(&query.order_by, &select.projection) else { - return Ok(None); - }; - if order_by.len() != 1 - || order_by[0].projection_index != 4 - || !order_by[0].descending - || order_by[0].collation.is_some() - { - return Ok(None); - } - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - - Ok(Some(MovieBusiestPeoplePlan { - people_table_name: people_name, - people_projection_indexes, - people_index_name, - people_id_is_rowid_alias, - roles_person_index_name, - column_names, - limit, - offset, - })) - } - - pub(crate) fn try_execute_showdown_window_query( - &self, - query: &Query, - ) -> Result> { - if query.recursive - || !query.ctes.is_empty() - || query.limit.is_some() - || query.offset.is_some() - { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || !select.group_by.is_empty() - || select.having.is_some() - || select.from.len() != 1 - { - return Ok(None); - } - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if !identifiers_equal(name, "reviews") - && !identifiers_equal(name, "roles") - && !identifiers_equal(name, "movies") - { - return Ok(None); - } - if self.security_rules_active()? { - return Ok(None); - } - let binding_name = alias.as_deref().unwrap_or(name.as_str()); - if identifiers_equal(name, "reviews") { - return self.try_execute_showdown_review_ranking_query( - select, - &query.order_by, - name, - binding_name, - ); - } - if identifiers_equal(name, "roles") { - return self.try_execute_showdown_cast_billing_query( - select, - &query.order_by, - name, - binding_name, - ); - } - if identifiers_equal(name, "movies") { - return self.try_execute_showdown_rolling_avg_query( - select, - &query.order_by, - name, - binding_name, - ); - } - Ok(None) - } - - fn try_execute_showdown_review_ranking_query( - &self, - select: &Select, - query_order_by: &[crate::sql::ast::OrderBy], - table_name: &str, - binding_name: &str, - ) -> Result> { - if select.filter.is_some() - || query_order_by.len() != 2 - || !showdown_window_column_order_matches( - &query_order_by[0], - table_name, - binding_name, - "movie_id", - false, - ) - || !showdown_window_alias_order_matches(&query_order_by[1], "rk", false) - { - return Ok(None); - } - let Some(schema) = self.table_schema(table_name) else { - return Ok(None); - }; - let Some(source) = self.visible_table_row_source(table_name) else { - return Ok(None); - }; - let Some(movie_id_index) = schema_column_index(schema, "movie_id") else { - return Ok(None); - }; - let Some(score_index) = schema_column_index(schema, "score") else { - return Ok(None); - }; - let Some(author_index) = schema_column_index(schema, "author") else { - return Ok(None); - }; - if select.projection.len() != 5 - || !showdown_window_projection_column_matches( - &select.projection[0], - table_name, - binding_name, - "movie_id", - ) - || !showdown_window_projection_column_matches( - &select.projection[1], - table_name, - binding_name, - "score", - ) - || !showdown_window_projection_column_matches( - &select.projection[2], - table_name, - binding_name, - "author", - ) - || !showdown_rank_window_projection_matches( - &select.projection[3], - table_name, - binding_name, - "rank", - "rk", - ) - || !showdown_rank_window_projection_matches( - &select.projection[4], - table_name, - binding_name, - "dense_rank", - "drk", - ) - { - return Ok(None); - } - - let mut ordered = Vec::with_capacity(source.row_count()); - let mut already_grouped = true; - let mut previous_scanned_movie_id = None; - for row in source.rows() { - let row = row?; - let values = row.values(); - let movie_id = showdown_fast_int64_value(values, movie_id_index, "reviews.movie_id")?; - if previous_scanned_movie_id.is_some_and(|previous| previous > movie_id) { - already_grouped = false; - } - previous_scanned_movie_id = Some(movie_id); - let score = showdown_fast_int64_value(values, score_index, "reviews.score")?; - let author = values - .get(author_index) - .cloned() - .ok_or_else(|| DbError::internal("showdown review author column missing"))?; - ordered.push(ReviewRankingFastRow { - row_id: row.row_id(), - movie_id, - score, - author, - }); - } - - if already_grouped { - let mut rows = Vec::with_capacity(source.row_count()); - let mut current_movie_id = None; - let mut group = Vec::::new(); - for item in ordered { - if current_movie_id.is_some_and(|current| current != item.movie_id) { - append_showdown_review_ranking_group(&mut group, &mut rows); - } - current_movie_id = Some(item.movie_id); - group.push(item); - } - append_showdown_review_ranking_group(&mut group, &mut rows); - return Ok(Some(QueryResult::with_rows( - showdown_window_projection_column_names(select)?, - rows, - ))); - } - - ordered.sort_unstable_by(|left, right| { - left.movie_id - .cmp(&right.movie_id) - .then_with(|| right.score.cmp(&left.score)) - .then_with(|| left.row_id.cmp(&right.row_id)) - }); - - let mut rows = Vec::with_capacity(ordered.len()); - let mut previous_movie_id: Option = None; - let mut previous_score: Option = None; - let mut partition_ordinal = 0_usize; - let mut current_rank = 1_i64; - let mut current_dense_rank = 1_i64; - for item in ordered { - if previous_movie_id != Some(item.movie_id) { - previous_movie_id = Some(item.movie_id); - partition_ordinal = 0; - current_rank = 1; - current_dense_rank = 1; - } else { - partition_ordinal += 1; - if previous_score.is_some_and(|score| score != item.score) { - current_rank = (partition_ordinal + 1) as i64; - current_dense_rank += 1; - } - } - previous_score = Some(item.score); - rows.push(QueryRow::new(vec![ - Value::Int64(item.movie_id), - Value::Int64(item.score), - item.author, - Value::Int64(current_rank), - Value::Int64(current_dense_rank), - ])); - } - Ok(Some(QueryResult::with_rows( - showdown_window_projection_column_names(select)?, - rows, - ))) - } - - fn try_execute_showdown_cast_billing_query( - &self, - select: &Select, - query_order_by: &[crate::sql::ast::OrderBy], - table_name: &str, - binding_name: &str, - ) -> Result> { - if query_order_by.len() != 2 - || !showdown_window_column_order_matches( - &query_order_by[0], - table_name, - binding_name, - "movie_id", - false, - ) - || !showdown_window_alias_order_matches(&query_order_by[1], "rn", false) - || !showdown_text_eq_filter_matches( - select.filter.as_ref(), - table_name, - binding_name, - "department", - "Acting", - ) - { - return Ok(None); - } - let Some(schema) = self.table_schema(table_name) else { - return Ok(None); - }; - let Some(source) = self.visible_table_row_source(table_name) else { - return Ok(None); - }; - let Some(movie_id_index) = schema_column_index(schema, "movie_id") else { - return Ok(None); - }; - let Some(person_id_index) = schema_column_index(schema, "person_id") else { - return Ok(None); - }; - let Some(department_index) = schema_column_index(schema, "department") else { - return Ok(None); - }; - let Some(billing_order_index) = schema_column_index(schema, "billing_order") else { - return Ok(None); - }; - if select.projection.len() != 5 - || !showdown_window_projection_column_matches( - &select.projection[0], - table_name, - binding_name, - "movie_id", - ) - || !showdown_window_projection_column_matches( - &select.projection[1], - table_name, - binding_name, - "person_id", - ) - || !showdown_window_projection_column_matches( - &select.projection[2], - table_name, - binding_name, - "billing_order", - ) - || !showdown_row_number_projection_matches( - &select.projection[3], - table_name, - binding_name, - "movie_id", - "billing_order", - "rn", - ) - || !showdown_lag_projection_matches( - &select.projection[4], - table_name, - binding_name, - "movie_id", - "billing_order", - "prev", - ) - { - return Ok(None); - } - - let mut ordered = Vec::new(); - for row in source.rows() { - let row = row?; - let values = row.values(); - if !matches!( - values.get(department_index), - Some(Value::Text(department)) if department == "Acting" - ) { - continue; - } - let movie_id = showdown_fast_int64_value(values, movie_id_index, "roles.movie_id")?; - let billing_order = - showdown_fast_int64_value(values, billing_order_index, "roles.billing_order")?; - let person_id = values.get(person_id_index).cloned().ok_or_else(|| { - DbError::internal("showdown role person_id column missing from row") - })?; - let billing_value = values.get(billing_order_index).cloned().ok_or_else(|| { - DbError::internal("showdown role billing_order column missing from row") - })?; - ordered.push(CastBillingFastRow { - row_id: row.row_id(), - movie_id, - person_id, - billing_order, - billing_value, - }); - } - ordered.sort_by(|left, right| { - left.movie_id - .cmp(&right.movie_id) - .then_with(|| left.billing_order.cmp(&right.billing_order)) - .then_with(|| left.row_id.cmp(&right.row_id)) - }); - - let mut rows = Vec::with_capacity(ordered.len()); - let mut previous_movie_id: Option = None; - let mut previous_billing = Value::Null; - let mut partition_ordinal = 0_usize; - for item in ordered { - let prev = if previous_movie_id == Some(item.movie_id) { - partition_ordinal += 1; - previous_billing.clone() - } else { - previous_movie_id = Some(item.movie_id); - partition_ordinal = 0; - Value::Null - }; - previous_billing = item.billing_value.clone(); - rows.push(QueryRow::new(vec![ - Value::Int64(item.movie_id), - item.person_id, - item.billing_value, - Value::Int64((partition_ordinal + 1) as i64), - prev, - ])); - } - Ok(Some(QueryResult::with_rows( - showdown_window_projection_column_names(select)?, - rows, - ))) - } - - fn try_execute_showdown_rolling_avg_query( - &self, - select: &Select, - query_order_by: &[crate::sql::ast::OrderBy], - table_name: &str, - binding_name: &str, - ) -> Result> { - if select.filter.is_some() - || query_order_by.len() != 1 - || !showdown_window_column_order_matches( - &query_order_by[0], - table_name, - binding_name, - "id", - false, - ) - { - return Ok(None); - } - let Some(schema) = self.table_schema(table_name) else { - return Ok(None); - }; - let Some(source) = self.visible_table_row_source(table_name) else { - return Ok(None); - }; - let Some(id_index) = schema_column_index(schema, "id") else { - return Ok(None); - }; - let Some(rating_index) = schema_column_index(schema, "rating") else { - return Ok(None); - }; - if select.projection.len() != 3 - || !showdown_window_projection_column_matches( - &select.projection[0], - table_name, - binding_name, - "id", - ) - || !showdown_window_projection_column_matches( - &select.projection[1], - table_name, - binding_name, - "rating", - ) - || !showdown_avg_window_projection_matches( - &select.projection[2], - table_name, - binding_name, - "id", - "rating", - "rolling", - ) - { - return Ok(None); - } - - if row_id_alias_column_name(schema).is_some_and(|column| identifiers_equal(column, "id")) - && schema.columns[rating_index].column_type == ColumnType::Float64 - && !schema.columns[rating_index].nullable - { - let mut ratings = Vec::with_capacity(source.row_count()); - let mut already_ordered = true; - let mut previous_movie_id = None; - source.visit_float64_column_values(rating_index, |row_id, rating| { - if previous_movie_id.is_some_and(|previous| previous > row_id) { - already_ordered = false; - } - previous_movie_id = Some(row_id); - let Some(rating) = rating else { - return Err(DbError::internal( - "showdown movie rating column unexpectedly NULL", - )); - }; - ratings.push((row_id, rating)); - Ok(()) - })?; - if !already_ordered { - ratings.sort_by_key(|(movie_id, _)| *movie_id); - } - - let mut rows = Vec::with_capacity(ratings.len()); - let mut previous_two = None; - let mut previous_one = None; - for (movie_id, rating) in ratings { - let rolling = match (previous_two, previous_one) { - (Some(two_back), Some(one_back)) => { - Value::Float64(((two_back + one_back) + rating) / 3.0) - } - (None, Some(one_back)) => Value::Float64((one_back + rating) / 2.0), - _ => Value::Float64(rating), - }; - rows.push(QueryRow::new(vec![ - Value::Int64(movie_id), - Value::Float64(rating), - rolling, - ])); - previous_two = previous_one; - previous_one = Some(rating); - } - return Ok(Some(QueryResult::with_rows( - showdown_window_projection_column_names(select)?, - rows, - ))); - } - - let mut ordered = Vec::with_capacity(source.row_count()); - let mut already_ordered = true; - let mut previous_movie_id = None; - for row in source.rows() { - let row = row?; - let values = row.values(); - let movie_id = showdown_fast_int64_value(values, id_index, "movies.id")?; - if previous_movie_id.is_some_and(|previous| previous > movie_id) { - already_ordered = false; - } - previous_movie_id = Some(movie_id); - let id_value = values - .get(id_index) - .cloned() - .ok_or_else(|| DbError::internal("showdown movie id column missing from row"))?; - let rating = values.get(rating_index).cloned().ok_or_else(|| { - DbError::internal("showdown movie rating column missing from row") - })?; - ordered.push(RollingAvgFastRow { - row_id: row.row_id(), - movie_id, - id_value, - rating, - }); - } - if !already_ordered { - ordered.sort_by(|left, right| { - left.movie_id - .cmp(&right.movie_id) - .then_with(|| left.row_id.cmp(&right.row_id)) - }); - } - - let mut rows = Vec::with_capacity(ordered.len()); - for ordinal in 0..ordered.len() { - let start = ordinal.saturating_sub(2); - let mut total = 0.0_f64; - let mut count = 0_i64; - for item in &ordered[start..=ordinal] { - match &item.rating { - Value::Null => {} - Value::Int64(value) => { - total += *value as f64; - count += 1; - } - Value::Float64(value) => { - total += *value; - count += 1; - } - Value::Decimal { scaled, scale } => { - total += (*scaled as f64) / 10_f64.powi(i32::from(*scale)); - count += 1; - } - other => { - return Err(DbError::sql(format!( - "numeric aggregate does not support {other:?}" - ))) - } - } - } - let rolling = if count == 0 { - Value::Null - } else { - Value::Float64(total / count as f64) - }; - let item = &ordered[ordinal]; - rows.push(QueryRow::new(vec![ - item.id_value.clone(), - item.rating.clone(), - rolling, - ])); - } - Ok(Some(QueryResult::with_rows( - showdown_window_projection_column_names(select)?, - rows, - ))) - } - - pub(crate) fn try_execute_showdown_directors_cte_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_showdown_directors_cte_query(query, params)? else { - return Ok(None); - }; - let Some(roles_source) = self.visible_table_row_source(plan.roles_table_name) else { - return Ok(None); - }; - let Some(movie_source) = self.visible_table_row_source(plan.movie_table_name) else { - return Ok(None); - }; - let movie_index_keys = - plan.movie_index_name - .as_deref() - .and_then(|index_name| match self.index(index_name) { - Some(RuntimeIndex::Btree { keys, .. }) => Some(keys), - _ => None, - }); - if !plan.movie_id_is_rowid_alias && movie_index_keys.is_none() { - return Ok(None); - } - - let mut directors = BTreeMap::, DirectorsCteAccumulator>::new(); - for role_row in roles_source.rows() { - let role_row = role_row?; - let role_values = role_row.values(); - if !matches!( - role_values.get(plan.role_job_index), - Some(Value::Text(job)) if job == &plan.director_job - ) { - continue; - } - let Some(person_id) = role_values.get(plan.role_person_id_index) else { - return Err(DbError::internal("roles person_id column missing from row")); - }; - if matches!(person_id, Value::Null) { - continue; - } - let Some(movie_id) = role_values.get(plan.role_movie_id_index) else { - return Err(DbError::internal("roles movie_id column missing from row")); - }; - if matches!(movie_id, Value::Null) { - continue; - } - - let key = row_identity(std::slice::from_ref(person_id))?; - let accumulator = directors - .entry(key) - .or_insert_with(|| DirectorsCteAccumulator::new(person_id.clone())); - accumulate_directors_cte_movie( - &movie_source, - movie_index_keys, - plan.movie_id_is_rowid_alias, - movie_id, - plan.movie_title_index, - plan.movie_rating_index, - accumulator, - )?; - } - - let bounded_order = plan - .order_by - .as_deref() - .zip(plan.limit) - .filter(|(_, _)| plan.offset == 0); - let mut rows = Vec::new(); - for accumulator in directors.into_values() { - if accumulator.films < plan.min_films { - continue; - } - let avg_rating = if accumulator.rating_count == 0 { - Value::Null - } else { - Value::Float64(accumulator.rating_sum / accumulator.rating_count as f64) - }; - let titles = if accumulator.titles.is_empty() { - Value::Null - } else { - Value::Text(accumulator.titles.join(&plan.title_separator)) - }; - let row = QueryRow::new(vec![ - accumulator.person_id, - Value::Int64(accumulator.films), - avg_rating, - titles, - ]); - if let Some((order_by, limit)) = bounded_order { - push_bounded_projection_ordered_query_row( - Some(self), - &mut rows, - row, - order_by, - limit, - )?; - } else { - rows.push(row); - } - } - - if let Some((order_by, _)) = bounded_order { - sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; - return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); - } - - Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - plan.column_names, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - )?)) - } - - fn analyze_showdown_directors_cte_query<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result>> { - if query.recursive || query.ctes.len() != 2 || query.offset.is_some() { - return Ok(None); - } - let directed_cte = &query.ctes[0]; - let top_dirs_cte = &query.ctes[1]; - if !identifiers_equal(&directed_cte.name, "directed") - || !directed_cte.column_names.is_empty() - || !identifiers_equal(&top_dirs_cte.name, "top_dirs") - || !top_dirs_cte.column_names.is_empty() - { - return Ok(None); - } - - let Some(directed_plan) = self.analyze_directed_movies_cte(directed_cte)? else { - return Ok(None); - }; - let Some(top_dirs_plan) = - self.analyze_directors_top_dirs_cte(top_dirs_cte, params, &directed_cte.name)? - else { - return Ok(None); - }; - let Some((column_names, order_by, limit, offset, title_separator)) = self - .analyze_directors_final_select( - query, - params, - &directed_cte.name, - &top_dirs_cte.name, - )? - else { - return Ok(None); - }; - - Ok(Some(DirectorsCtePlan { - roles_table_name: directed_plan.roles_table_name, - role_person_id_index: directed_plan.role_person_id_index, - role_movie_id_index: directed_plan.role_movie_id_index, - role_job_index: directed_plan.role_job_index, - director_job: directed_plan.director_job, - movie_table_name: directed_plan.movie_table_name, - movie_title_index: directed_plan.movie_title_index, - movie_rating_index: directed_plan.movie_rating_index, - movie_index_name: directed_plan.movie_index_name, - movie_id_is_rowid_alias: directed_plan.movie_id_is_rowid_alias, - min_films: top_dirs_plan.min_films, - title_separator, - column_names, - order_by, - limit, - offset, - })) - } - - fn analyze_directed_movies_cte<'a>( - &'a self, - cte: &'a CommonTableExpr, - ) -> Result>> { - if cte.query.recursive - || !cte.query.ctes.is_empty() - || !cte.query.order_by.is_empty() - || cte.query.limit.is_some() - || cte.query.offset.is_some() - { - return Ok(None); - } - let QueryBody::Select(select) = &cte.query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || !select.group_by.is_empty() - || select.having.is_some() - || select.projection.len() != 4 - || select.from.len() != 1 - { - return Ok(None); - } - - let mut tables = Vec::new(); - let mut constraints = Vec::new(); - if !flatten_inner_join_chain(&select.from[0], &mut tables, &mut constraints) - || tables.len() != 2 - { - return Ok(None); - } - let roles_binding = tables - .iter() - .copied() - .find(|binding| identifiers_equal(binding.name, "roles")); - let movie_binding = tables - .iter() - .copied() - .find(|binding| identifiers_equal(binding.name, "movies")); - let (Some(roles_binding), Some(movie_binding)) = (roles_binding, movie_binding) else { - return Ok(None); - }; - if self - .visible_view(roles_binding.name, NameResolutionScope::Session) - .is_some() - || self - .visible_view(movie_binding.name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(roles_binding.name) - || self.visible_table_is_temporary(movie_binding.name) - { - return Ok(None); - } - let Some(roles_schema) = self.table_schema(roles_binding.name) else { - return Ok(None); - }; - let Some(movie_schema) = self.table_schema(movie_binding.name) else { - return Ok(None); - }; - if !generated_columns_are_stored(roles_schema) - || !generated_columns_are_stored(movie_schema) - { - return Ok(None); - } - - if !projection_expr_matches_binding_column( - &select.projection[0], - roles_binding, - "person_id", - ) || !projection_expr_matches_binding_column( - &select.projection[1], - roles_binding, - "movie_id", - ) || !projection_expr_matches_binding_column( - &select.projection[2], - movie_binding, - "title", - ) || !projection_expr_matches_binding_column( - &select.projection[3], - movie_binding, - "rating", - ) || !join_constraints_match_columns( - &constraints, - movie_binding, - "id", - roles_binding, - "movie_id", - ) { - return Ok(None); - } - let Some(filter) = select.filter.as_ref() else { - return Ok(None); - }; - let Some(director_job) = equality_filter_text_literal(filter, roles_binding, "job") else { - return Ok(None); - }; - - let role_person_id_index = - schema_column_index(roles_schema, "person_id").ok_or_else(|| { - DbError::internal("directors CTE person_id column missing from roles") - })?; - let role_movie_id_index = schema_column_index(roles_schema, "movie_id") - .ok_or_else(|| DbError::internal("directors CTE movie_id column missing from roles"))?; - let role_job_index = schema_column_index(roles_schema, "job") - .ok_or_else(|| DbError::internal("directors CTE job column missing from roles"))?; - let movie_title_index = schema_column_index(movie_schema, "title") - .ok_or_else(|| DbError::internal("directors CTE title column missing from movies"))?; - let movie_rating_index = schema_column_index(movie_schema, "rating") - .ok_or_else(|| DbError::internal("directors CTE rating column missing from movies"))?; - if !matches!( - roles_schema.columns[role_job_index].column_type, - ColumnType::Text - ) || !matches!( - movie_schema.columns[movie_title_index].column_type, - ColumnType::Text - ) { - return Ok(None); - } - let movie_index_name = self - .single_column_btree_index(movie_binding.name, "id") - .map(|index| index.name.clone()); - let movie_id_is_rowid_alias = row_id_alias_column_name(movie_schema) - .is_some_and(|column| identifiers_equal(column, "id")); - if !movie_id_is_rowid_alias && movie_index_name.is_none() { - return Ok(None); - } - - Ok(Some(DirectedMoviesCtePlan { - roles_table_name: roles_binding.name, - role_person_id_index, - role_movie_id_index, - role_job_index, - director_job: director_job.to_string(), - movie_table_name: movie_binding.name, - movie_title_index, - movie_rating_index, - movie_index_name, - movie_id_is_rowid_alias, - })) - } - - fn analyze_directors_top_dirs_cte( - &self, - cte: &CommonTableExpr, - params: &[Value], - directed_cte_name: &str, - ) -> Result> { - if cte.query.recursive - || !cte.query.ctes.is_empty() - || !cte.query.order_by.is_empty() - || cte.query.limit.is_some() - || cte.query.offset.is_some() - { - return Ok(None); - } - let QueryBody::Select(select) = &cte.query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || select.filter.is_some() - || select.projection.len() != 3 - || select.group_by.len() != 1 - || select.from.len() != 1 - { - return Ok(None); - } - let FromItem::Table { - name: source_name, - alias, - } = &select.from[0] - else { - return Ok(None); - }; - if !identifiers_equal(source_name, directed_cte_name) { - return Ok(None); - } - let directed_binding = TableBindingRef { - name: source_name, - alias, - }; - if !projection_expr_matches_binding_column( - &select.projection[0], - directed_binding, - "person_id", - ) || !matches!( - &select.projection[1], - SelectItem::Expr { - expr, - alias: Some(alias) - } if identifiers_equal(alias, "films") && aggregate_matches_count_star(expr) - ) || !matches!( - &select.projection[2], - SelectItem::Expr { - expr, - alias: Some(alias) - } if identifiers_equal(alias, "avg_rating") - && aggregate_matches_single_binding_column_or_unqualified( - expr, - "avg", - directed_binding, - "rating" - ) - ) || !expr_matches_binding_column_or_unqualified( - &select.group_by[0], - directed_binding, - "person_id", - ) { - return Ok(None); - } - let min_films = match select.having.as_ref() { - Some(Expr::Binary { - left, - op: BinaryOp::GtEq, - right, - }) if aggregate_matches_count_star(left) => { - self.eval_constant_i64(right, params, &BTreeMap::new())? - } - _ => return Ok(None), - }; - - Ok(Some(DirectorsTopDirsCtePlan { min_films })) - } - - fn analyze_directors_final_select( - &self, - query: &Query, - params: &[Value], - directed_cte_name: &str, - top_dirs_cte_name: &str, - ) -> Result> { - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || select.filter.is_some() - || select.having.is_some() - || select.projection.len() != 4 - || select.group_by.len() != 3 - || select.from.len() != 1 - { - return Ok(None); - } - - let FromItem::Join { - left, - right, - kind: JoinKind::Inner, - constraint: JoinConstraint::On(on), - } = &select.from[0] - else { - return Ok(None); - }; - let ( - FromItem::Table { - name: left_name, - alias: left_alias, - }, - FromItem::Table { - name: right_name, - alias: right_alias, - }, - ) = (&**left, &**right) - else { - return Ok(None); - }; - if !identifiers_equal(left_name, top_dirs_cte_name) - || !identifiers_equal(right_name, directed_cte_name) - { - return Ok(None); - } - let top_binding = TableBindingRef { - name: left_name, - alias: left_alias, - }; - let directed_binding = TableBindingRef { - name: right_name, - alias: right_alias, - }; - if !join_constraint_matches_columns( - on, - top_binding, - "person_id", - directed_binding, - "person_id", - ) || !projection_expr_matches_binding_column( - &select.projection[0], - top_binding, - "person_id", - ) || !projection_expr_matches_binding_column(&select.projection[1], top_binding, "films") - || !projection_expr_matches_binding_column( - &select.projection[2], - top_binding, - "avg_rating", - ) - || !group_exprs_match_binding_columns( - &select.group_by, - top_binding, - &["person_id", "films", "avg_rating"], - ) - { - return Ok(None); - } - let Some(title_separator) = - projection_expr_string_agg_separator(&select.projection[3], directed_binding, "title") - else { - return Ok(None); - }; - - let order_by = projection_order_by_plan(&query.order_by, &select.projection); - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let column_names = select - .projection - .iter() - .enumerate() - .map(|(index, item)| match item { - SelectItem::Expr { expr, alias } => alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => { - format!("col{}", index + 1) - } - }) - .collect::>(); - - Ok(Some(( - column_names, - order_by, - limit, - offset, - title_separator.to_string(), - ))) - } - - fn analyze_three_table_genre_popularity_query<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result>> { - if !query.ctes.is_empty() || query.recursive { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || select.filter.is_some() - || select.having.is_some() - || select.group_by.len() != 1 - || select.projection.len() != 3 - || select.from.len() != 1 - { - return Ok(None); - } - - let mut tables = Vec::new(); - let mut constraints = Vec::new(); - if !flatten_inner_join_chain(&select.from[0], &mut tables, &mut constraints) - || tables.len() != 3 - { - return Ok(None); - } - let genre_binding = tables - .iter() - .copied() - .find(|binding| identifiers_equal(binding.name, "genres")); - let bridge_binding = tables - .iter() - .copied() - .find(|binding| identifiers_equal(binding.name, "movie_genres")); - let movie_binding = tables - .iter() - .copied() - .find(|binding| identifiers_equal(binding.name, "movies")); - let (Some(genre_binding), Some(bridge_binding), Some(movie_binding)) = - (genre_binding, bridge_binding, movie_binding) - else { - return Ok(None); - }; - - if [genre_binding.name, bridge_binding.name, movie_binding.name] - .iter() - .any(|table| { - self.visible_view(table, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(table) - }) - { - return Ok(None); - } - let Some(genre_schema) = self.table_schema(genre_binding.name) else { - return Ok(None); - }; - let Some(bridge_schema) = self.table_schema(bridge_binding.name) else { - return Ok(None); - }; - let Some(movie_schema) = self.table_schema(movie_binding.name) else { - return Ok(None); - }; - if !generated_columns_are_stored(genre_schema) - || !generated_columns_are_stored(bridge_schema) - || !generated_columns_are_stored(movie_schema) - { - return Ok(None); - } - - let SelectItem::Expr { - expr: name_expr, - alias: name_alias, - } = &select.projection[0] - else { - return Ok(None); - }; - let SelectItem::Expr { - expr: count_expr, - alias: count_alias, - } = &select.projection[1] - else { - return Ok(None); - }; - let SelectItem::Expr { - expr: avg_expr, - alias: avg_alias, - } = &select.projection[2] - else { - return Ok(None); - }; - - if !grouped_projection_expr_matches_group_expr( - name_expr, - &select.group_by[0], - genre_binding, - ) || !expr_matches_binding_column(name_expr, genre_binding, "name") - || !aggregate_matches_count_star(count_expr) - || !aggregate_matches_single_binding_column(avg_expr, "avg", movie_binding, "rating") - { - return Ok(None); - } - - if !join_constraints_match_columns( - &constraints, - genre_binding, - "id", - bridge_binding, - "genre_id", - ) || !join_constraints_match_columns( - &constraints, - movie_binding, - "id", - bridge_binding, - "movie_id", - ) { - return Ok(None); - } - - let genre_id_index = schema_column_index(genre_schema, "id") - .ok_or_else(|| DbError::internal("genre popularity id column missing from genres"))?; - let genre_name_index = schema_column_index(genre_schema, "name") - .ok_or_else(|| DbError::internal("genre popularity name column missing from genres"))?; - let bridge_movie_id_index = - schema_column_index(bridge_schema, "movie_id").ok_or_else(|| { - DbError::internal("genre popularity movie_id column missing from movie_genres") - })?; - let movie_rating_index = schema_column_index(movie_schema, "rating").ok_or_else(|| { - DbError::internal("genre popularity rating column missing from movies") - })?; - - let Some(bridge_genre_index_name) = self - .single_column_btree_index(bridge_binding.name, "genre_id") - .map(|index| index.name.clone()) - else { - return Ok(None); - }; - let movie_index_name = self - .single_column_btree_index(movie_binding.name, "id") - .map(|index| index.name.clone()); - let movie_id_is_rowid_alias = row_id_alias_column_name(movie_schema) - .is_some_and(|column| identifiers_equal(column, "id")); - if !movie_id_is_rowid_alias && movie_index_name.is_none() { - return Ok(None); - } - - let column_names = vec![ - name_alias - .clone() - .unwrap_or_else(|| infer_expr_name(name_expr, 1)), - count_alias - .clone() - .unwrap_or_else(|| infer_expr_name(count_expr, 2)), - avg_alias - .clone() - .unwrap_or_else(|| infer_expr_name(avg_expr, 3)), - ]; - - let order_by = projection_order_by_plan(&query.order_by, &select.projection); - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - - Ok(Some(ThreeTableGenrePopularityPlan { - genre_table_name: genre_binding.name, - genre_id_index, - genre_name_index, - bridge_table_name: bridge_binding.name, - bridge_movie_id_index, - bridge_genre_index_name, - movie_table_name: movie_binding.name, - movie_rating_index, - movie_index_name, - movie_id_is_rowid_alias, - column_names, - order_by, - limit, - offset, - })) - } - - fn analyze_left_join_aggregate_query<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result>> { - if !query.ctes.is_empty() || query.recursive { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || select.filter.is_some() - || select.having.is_some() - || select.group_by.is_empty() - || select.from.len() != 1 - { - return Ok(None); - } - let FromItem::Join { - left, - right, - kind, - constraint, - } = &select.from[0] - else { - return Ok(None); - }; - let include_empty_parent = match kind { - JoinKind::Left => true, - JoinKind::Inner => false, - _ => return Ok(None), - }; - let (left_name, left_alias) = match &**left { - FromItem::Table { name, alias } => (name.as_str(), alias), - _ => return Ok(None), - }; - let (right_name, right_alias) = match &**right { - FromItem::Table { name, alias } => (name.as_str(), alias), - _ => return Ok(None), - }; - if self - .visible_view(left_name, NameResolutionScope::Session) - .is_some() - || self - .visible_view(right_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(left_name) - || self.visible_table_is_temporary(right_name) - { - return Ok(None); - } - let Some(left_schema) = self.table_schema(left_name) else { - return Ok(None); - }; - let Some(right_schema) = self.table_schema(right_name) else { - return Ok(None); - }; - if !generated_columns_are_stored(left_schema) || !generated_columns_are_stored(right_schema) - { - return Ok(None); - } - let left_binding = TableBindingRef { - name: left_name, - alias: left_alias, - }; - let right_binding = TableBindingRef { - name: right_name, - alias: right_alias, - }; - let left_group_indexes = - indexed_join_group_column_indexes(&select.group_by, left_binding, left_schema); - let right_group_indexes = - indexed_join_group_column_indexes(&select.group_by, right_binding, right_schema); - let (parent_name, parent_binding, parent_schema, child_name, child_binding, child_schema) = - match (left_group_indexes, right_group_indexes) { - (Some(_group_column_indexes), None) => ( - left_name, - left_binding, - left_schema, - right_name, - right_binding, - right_schema, - ), - (None, Some(_group_column_indexes)) if !include_empty_parent => ( - right_name, - right_binding, - right_schema, - left_name, - left_binding, - left_schema, - ), - _ => return Ok(None), - }; - - let group_column_indexes = - indexed_join_group_column_indexes(&select.group_by, parent_binding, parent_schema) - .ok_or_else(|| DbError::internal("group column indexes mismatch"))?; - - let num_group_cols = select.group_by.len(); - if select.projection.len() <= num_group_cols { - return Ok(None); - } - - for projection_item in select - .projection - .iter() - .take(num_group_cols) - .zip(&select.group_by) - { - let (projection_item, group_expr) = projection_item; - let SelectItem::Expr { - expr: projection_expr, - .. - } = projection_item - else { - return Ok(None); - }; - if !grouped_projection_expr_matches_group_expr( - projection_expr, - group_expr, - parent_binding, - ) { - return Ok(None); - } - } - - let mut aggregate_kinds = Vec::with_capacity(select.projection.len() - num_group_cols); - for projection_item in select.projection.iter().skip(num_group_cols) { - let SelectItem::Expr { expr, .. } = projection_item else { - return Ok(None); - }; - let Some(kind) = classify_indexed_join_aggregate(expr, child_binding, child_schema) - else { - return Ok(None); - }; - aggregate_kinds.push(kind); - } - - let mut column_names = Vec::with_capacity(select.projection.len()); - for (index, projection_item) in select.projection.iter().enumerate() { - let SelectItem::Expr { expr, alias } = projection_item else { - return Ok(None); - }; - column_names.push( - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - ); - } - - let Some(join_equalities) = simple_indexed_join_constraint_equalities( - constraint, - left_binding, - right_binding, - left_schema, - right_schema, - ) else { - return Ok(None); - }; - let Some((left_join_columns, right_join_columns)) = - orient_join_equalities(&join_equalities, left_binding, right_binding) - else { - return Ok(None); - }; - if left_join_columns.len() != 1 || right_join_columns.len() != 1 { - return Ok(None); - } - - let (parent_join_column, child_join_column) = if identifiers_equal(parent_name, left_name) { - (left_join_columns[0], right_join_columns[0]) - } else { - (right_join_columns[0], left_join_columns[0]) - }; - - let parent_join_index = parent_schema - .columns - .iter() - .position(|column| identifiers_equal(&column.name, parent_join_column)) - .ok_or_else(|| { - DbError::internal(format!( - "join column {}.{} not found", - parent_name, parent_join_column - )) - })?; - let child_join_index = child_schema - .columns - .iter() - .position(|column| identifiers_equal(&column.name, child_join_column)) - .ok_or_else(|| { - DbError::internal(format!( - "join column {}.{} not found", - child_name, child_join_column - )) - })?; - - let child_index_name = self - .single_column_btree_index(child_name, child_join_column) - .map(|index| index.name.clone()); - - let order_by = projection_order_by_plan(&query.order_by, &select.projection); - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - - Ok(Some(LeftJoinAggregatePlan { - parent_table_name: parent_name, - parent_join_index, - child_table_name: child_name, - child_join_index, - child_index_name, - group_column_indexes, - aggregate_kinds, - column_names, - order_by, - limit, - offset, - include_empty_parent, - })) - } - - fn analyze_left_join_status_aggregate_query<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result>> { - if !query.ctes.is_empty() || query.recursive { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || query.order_by.len() > 2 - || select.filter.is_some() - || select.having.is_some() - || select.group_by.len() != 2 - || select.projection.len() != 7 - || select.from.len() != 1 - { - return Ok(None); - } - - let FromItem::Join { - left, - right, - kind, - constraint, - } = &select.from[0] - else { - return Ok(None); - }; - if !matches!(kind, JoinKind::Left) { - return Ok(None); - } - - let (left_name, left_alias) = match &**left { - FromItem::Table { name, alias } => (name.as_str(), alias), - _ => return Ok(None), - }; - let (right_name, right_alias) = match &**right { - FromItem::Table { name, alias } => (name.as_str(), alias), - _ => return Ok(None), - }; - if self - .visible_view(left_name, NameResolutionScope::Session) - .is_some() - || self - .visible_view(right_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(left_name) - || self.visible_table_is_temporary(right_name) - { - return Ok(None); - } - let Some(left_schema) = self.table_schema(left_name) else { - return Ok(None); - }; - let Some(right_schema) = self.table_schema(right_name) else { - return Ok(None); - }; - if !generated_columns_are_stored(left_schema) || !generated_columns_are_stored(right_schema) - { - return Ok(None); - } - - let left_binding = TableBindingRef { - name: left_name, - alias: left_alias, - }; - let right_binding = TableBindingRef { - name: right_name, - alias: right_alias, - }; - let left_group_indexes = - indexed_join_group_column_indexes(&select.group_by, left_binding, left_schema); - let right_group_indexes = - indexed_join_group_column_indexes(&select.group_by, right_binding, right_schema); - let ( - parent_name, - parent_alias, - parent_schema, - child_name, - child_alias, - child_schema, - group_column_indexes, - ) = match (left_group_indexes, right_group_indexes) { - (Some(group_column_indexes), None) => ( - left_name, - left_alias, - left_schema, - right_name, - right_alias, - right_schema, - group_column_indexes, - ), - _ => return Ok(None), - }; - - let parent_binding = TableBindingRef { - name: parent_name, - alias: parent_alias, - }; - let child_binding = TableBindingRef { - name: child_name, - alias: child_alias, - }; - - for (projection_item, group_expr) in select - .projection - .iter() - .take(select.group_by.len()) - .zip(&select.group_by) - { - let SelectItem::Expr { - expr: projection_expr, - .. - } = projection_item - else { - return Ok(None); - }; - if !grouped_projection_expr_matches_group_expr( - projection_expr, - group_expr, - parent_binding, - ) { - return Ok(None); - } - } - - let SelectItem::Expr { - expr: open_expr, .. - } = &select.projection[2] - else { - return Ok(None); - }; - let SelectItem::Expr { - expr: in_progress_expr, - .. - } = &select.projection[3] - else { - return Ok(None); - }; - let SelectItem::Expr { - expr: resolved_expr, - .. - } = &select.projection[4] - else { - return Ok(None); - }; - let SelectItem::Expr { - expr: closed_expr, .. - } = &select.projection[5] - else { - return Ok(None); - }; - let SelectItem::Expr { - expr: count_expr, .. - } = &select.projection[6] - else { - return Ok(None); - }; - - if !aggregate_matches_status_case_sum(open_expr, "sum", child_binding, "status", "open") - || !aggregate_matches_status_case_sum( - in_progress_expr, - "sum", - child_binding, - "status", - "in_progress", - ) - || !aggregate_matches_status_case_sum( - resolved_expr, - "sum", - child_binding, - "status", - "resolved", - ) - || !aggregate_matches_status_case_sum( - closed_expr, - "sum", - child_binding, - "status", - "closed", - ) - || !aggregate_matches_single_binding_column(count_expr, "count", child_binding, "id") - { - return Ok(None); - } - - let mut column_names = Vec::with_capacity(select.projection.len()); - for (index, projection_item) in select.projection.iter().enumerate() { - let SelectItem::Expr { expr, alias } = projection_item else { - return Ok(None); - }; - column_names.push( - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - ); - } - - let Some(join_equalities) = simple_indexed_join_constraint_equalities( - constraint, - left_binding, - right_binding, - left_schema, - right_schema, - ) else { - return Ok(None); - }; - let Some((left_join_columns, right_join_columns)) = - orient_join_equalities(&join_equalities, left_binding, right_binding) - else { - return Ok(None); - }; - if left_join_columns.len() != 1 || right_join_columns.len() != 1 { - return Ok(None); - } - - let (parent_join_column, child_join_column) = if identifiers_equal(parent_name, left_name) { - (left_join_columns[0], right_join_columns[0]) - } else { - (right_join_columns[0], left_join_columns[0]) - }; - - let parent_join_index = parent_schema - .columns - .iter() - .position(|column| identifiers_equal(&column.name, parent_join_column)) - .ok_or_else(|| { - DbError::internal(format!( - "join column {}.{} not found", - parent_name, parent_join_column - )) - })?; - let child_join_index = child_schema - .columns - .iter() - .position(|column| identifiers_equal(&column.name, child_join_column)) - .ok_or_else(|| { - DbError::internal(format!( - "join column {}.{} not found", - child_name, child_join_column - )) - })?; - - let child_status_index = schema_column_index(child_schema, "status").ok_or_else(|| { - DbError::internal(format!( - "column status not found in child table {}", - child_name - )) - })?; - let child_id_index = schema_column_index(child_schema, "id").ok_or_else(|| { - DbError::internal(format!("column id not found in child table {}", child_name)) - })?; - let child_index_name = self - .single_column_btree_index(child_name, child_join_column) - .map(|index| index.name.clone()); - - let order_by = projection_order_by_plan(&query.order_by, &select.projection); - if !query.order_by.is_empty() { - let Some(order_by) = order_by.as_ref() else { - return Ok(None); - }; - if order_by.len() != 2 - || order_by[0].projection_index != 6 - || !order_by[0].descending - || order_by[1].projection_index != 0 - || order_by[1].descending - { - return Ok(None); - } - } - - if child_schema.columns[child_status_index].column_type != crate::catalog::ColumnType::Text - { - return Ok(None); - } - - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - - Ok(Some(LeftJoinStatusAggregatePlan { - parent_table_name: parent_name, - parent_join_index, - child_table_name: child_name, - child_join_index, - child_status_index, - child_id_index, - child_index_name, - group_column_indexes, - column_names, - order_by, - limit, - offset, - })) - } - - pub(crate) fn try_execute_indexed_join_grouped_count_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_indexed_join_grouped_count_query(query, params)? else { - return Ok(None); - }; - let Some(parent_source) = self.visible_table_row_source(plan.parent_table_name) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&plan.child_index_name) else { - return Ok(None); - }; - let child_source = self - .catalog - .index(&plan.child_index_name) - .and_then(|index| self.visible_table_row_source(&index.table_name)); - let parent_table = self.table_schema(plan.parent_table_name).ok_or_else(|| { - DbError::internal(format!( - "table {} not found for indexed grouped join count", - plan.parent_table_name - )) - })?; - let parent_join_index = parent_table - .columns - .iter() - .position(|column| identifiers_equal(&column.name, plan.parent_join_column)) - .ok_or_else(|| { - DbError::internal(format!( - "join column {}.{} not found", - plan.parent_table_name, plan.parent_join_column - )) - })?; - - let bounded_order = plan - .order_by - .as_deref() - .zip(plan.limit) - .filter(|(_, _)| plan.offset == 0); - let scalar_count_top_n_limit = plan.scalar_count_top_n_limit(); - let mut scalar_count_top_n_rows: Vec<(i64, QueryRow)> = Vec::new(); - let mut rows = Vec::new(); - for parent_row in parent_source.rows() { - let parent_row = parent_row?; - let parent_values = parent_row.values(); - let Some(join_value) = parent_values.get(parent_join_index) else { - return Err(DbError::internal("parent join row is shorter than schema")); - }; - if matches!(join_value, Value::Null) { - continue; - } - let child_row_ids = keys.row_ids_for_value_set(join_value)?; - let child_count = if let Some(child_source) = child_source { - visible_row_id_set_count(child_source, child_row_ids)? - } else { - child_row_ids.len() - }; - if child_count == 0 { - continue; - } - let child_count = i64::try_from(child_count).map_err(|_| { - DbError::sql(format!( - "join count for table {} exceeds INT64 limits", - plan.parent_table_name - )) - })?; - - let scalar_count_top_n_slot = if let Some(limit) = scalar_count_top_n_limit { - if limit == 0 { - continue; - } - if scalar_count_top_n_rows.len() < limit { - Some(scalar_count_top_n_rows.len()) - } else { - let mut worst_index = 0; - for index in 1..scalar_count_top_n_rows.len() { - if scalar_count_top_n_rows[index].0 < scalar_count_top_n_rows[worst_index].0 - { - worst_index = index; - } - } - if child_count <= scalar_count_top_n_rows[worst_index].0 { - continue; - } - Some(worst_index) - } - } else { - None - }; - - let mut output = Vec::with_capacity(plan.group_column_indexes.len() + 1); - for index in &plan.group_column_indexes { - output.push(parent_values[*index].clone()); - } - output.push(Value::Int64(child_count)); - let row = QueryRow::new(output); - if let Some(slot) = scalar_count_top_n_slot { - if slot == scalar_count_top_n_rows.len() { - scalar_count_top_n_rows.push((child_count, row)); - } else { - scalar_count_top_n_rows[slot] = (child_count, row); - } - } else if let Some((order_by, limit)) = bounded_order { - push_bounded_projection_ordered_query_row( - Some(self), - &mut rows, - row, - order_by, - limit, - )?; - } else { - rows.push(row); - } - } - - if scalar_count_top_n_limit.is_some() { - scalar_count_top_n_rows.sort_by_key(|row| std::cmp::Reverse(row.0)); - let rows = scalar_count_top_n_rows - .into_iter() - .map(|(_, row)| row) - .collect(); - return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); - } - - if let Some((order_by, _)) = bounded_order { - sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; - return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); - } - - Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - plan.column_names, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - )?)) - } - - pub(crate) fn indexed_join_grouped_count_parent_table_name<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result> { - Ok(self - .analyze_indexed_join_grouped_count_query(query, params)? - .map(|plan| plan.parent_table_name)) - } - - fn analyze_indexed_join_grouped_count_query<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result>> { - if !query.ctes.is_empty() || query.recursive { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || select.filter.is_some() - || select.having.is_some() - || select.group_by.is_empty() - || select.from.len() != 1 - || select.projection.len() != select.group_by.len() + 1 - { - return Ok(None); - } - let FromItem::Join { - left, - right, - kind, - constraint, - } = &select.from[0] - else { - return Ok(None); - }; - if !matches!(kind, JoinKind::Inner) { - return Ok(None); - } - let (left_name, left_alias) = match &**left { - FromItem::Table { name, alias } => (name.as_str(), alias), - _ => return Ok(None), - }; - let (right_name, right_alias) = match &**right { - FromItem::Table { name, alias } => (name.as_str(), alias), - _ => return Ok(None), - }; - if self - .visible_view(left_name, NameResolutionScope::Session) - .is_some() - || self - .visible_view(right_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(left_name) - || self.visible_table_is_temporary(right_name) - { - return Ok(None); - } - let Some(left_schema) = self.table_schema(left_name) else { - return Ok(None); - }; - let Some(right_schema) = self.table_schema(right_name) else { - return Ok(None); - }; - if !generated_columns_are_stored(left_schema) || !generated_columns_are_stored(right_schema) - { - return Ok(None); - } - - let left_binding = TableBindingRef { - name: left_name, - alias: left_alias, - }; - let right_binding = TableBindingRef { - name: right_name, - alias: right_alias, - }; - let left_group_indexes = - indexed_join_group_column_indexes(&select.group_by, left_binding, left_schema); - let right_group_indexes = - indexed_join_group_column_indexes(&select.group_by, right_binding, right_schema); - let ( - parent_name, - parent_binding, - parent_schema, - child_name, - child_binding, - child_schema, - group_column_indexes, - ) = match (left_group_indexes, right_group_indexes) { - (Some(group_column_indexes), None) => ( - left_name, - left_binding, - left_schema, - right_name, - right_binding, - right_schema, - group_column_indexes, - ), - (None, Some(group_column_indexes)) => ( - right_name, - right_binding, - right_schema, - left_name, - left_binding, - left_schema, - group_column_indexes, - ), - _ => return Ok(None), - }; - - let SelectItem::Expr { - expr: count_expr, - alias: count_alias, - } = &select.projection[select.group_by.len()] - else { - return Ok(None); - }; - if !indexed_join_grouped_count_is_safe(count_expr, child_binding, child_schema) { - return Ok(None); - } - - let mut column_names = Vec::with_capacity(select.projection.len()); - for (projection_item, group_expr) in select - .projection - .iter() - .take(select.group_by.len()) - .zip(&select.group_by) - { - let SelectItem::Expr { - expr: projection_expr, - alias, - } = projection_item - else { - return Ok(None); - }; - if !grouped_projection_expr_matches_group_expr( - projection_expr, - group_expr, - parent_binding, - ) { - return Ok(None); - } - column_names.push( - alias - .clone() - .unwrap_or_else(|| infer_expr_name(projection_expr, column_names.len() + 1)), - ); - } - column_names.push( - count_alias - .clone() - .unwrap_or_else(|| infer_expr_name(count_expr, select.group_by.len() + 1)), - ); - - let Some(join_equalities) = simple_indexed_join_constraint_equalities( - constraint, - left_binding, - right_binding, - left_schema, - right_schema, - ) else { - return Ok(None); - }; - let Some((parent_join_columns, child_join_columns)) = - orient_join_equalities(&join_equalities, parent_binding, child_binding) - else { - return Ok(None); - }; - if parent_join_columns.len() != 1 || child_join_columns.len() != 1 { - return Ok(None); - } - let parent_join_column = parent_join_columns[0]; - let child_join_column = child_join_columns[0]; - if parent_schema - .columns - .iter() - .all(|column| !identifiers_equal(&column.name, parent_join_column)) - { - return Ok(None); - } - let Some(child_index_name) = self - .catalog - .indexes - .values() - .find(|index| { - identifiers_equal(&index.table_name, child_name) - && index.fresh - && index.kind == IndexKind::Btree - && index.predicate_sql.is_none() - && index.columns.len() == 1 - && index.columns[0].expression_sql.is_none() - && index.columns[0] - .column_name - .as_deref() - .is_some_and(|column| identifiers_equal(column, child_join_column)) - }) - .map(|index| index.name.clone()) - else { - return Ok(None); - }; - - let order_by = projection_order_by_plan(&query.order_by, &select.projection); - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - - Ok(Some(IndexedJoinGroupedCountPlan { - parent_table_name: parent_name, - parent_join_column, - child_index_name, - group_column_indexes, - column_names, - order_by, - limit, - offset, - })) - } - - fn try_execute_simple_view_projection_limit_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - if query.recursive || !query.ctes.is_empty() { - return Ok(None); - } - let Some(limit_expr) = query.limit.as_ref() else { - return Ok(None); - }; - let ctes = BTreeMap::new(); - let limit_value = match simple_int64_constant_expr_value(limit_expr, params)? { - Some(value) => value, - None => self.eval_constant_i64(limit_expr, params, &ctes)?, - }; - let limit = usize::try_from(limit_value.max(0)).unwrap_or(usize::MAX); - let offset = query - .offset - .as_ref() - .map(|expr| { - simple_int64_constant_expr_value(expr, params)? - .map(Ok) - .unwrap_or_else(|| self.eval_constant_i64(expr, params, &ctes)) - }) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || select.filter.is_some() - || !select.group_by.is_empty() - || select.having.is_some() - || projection_has_aggregate_items(&select.projection) - || select.from.len() != 1 - { - return Ok(None); - } - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - let Some(view) = self.visible_view(name, NameResolutionScope::Session) else { - return Ok(None); - }; - let view_binding = alias.as_deref().unwrap_or(name.as_str()); - let view_query = self.cached_view_query(view)?; - if view_query.recursive - || !view_query.ctes.is_empty() - || !view_query.order_by.is_empty() - || view_query.limit.is_some() - || view_query.offset.is_some() - { - return Ok(None); - } - let QueryBody::Select(view_select) = &view_query.body else { - return Ok(None); - }; - if view_select.distinct - || !view_select.distinct_on.is_empty() - || !view_select.group_by.is_empty() - || view_select.having.is_some() - || projection_has_aggregate_items(&view_select.projection) - { - return Ok(None); - } - if !query.order_by.is_empty() { - return if view.temporary { - self.try_execute_ordered_view_projection_limit_select( - select, - view_select, - &view.name, - &view.column_names, - view_binding, - &query.order_by, - limit, - offset, - params, - ) - } else { - let persistent_runtime = self.persistent_resolution_runtime(); - persistent_runtime.try_execute_ordered_view_projection_limit_select( - select, - view_select, - &view.name, - &view.column_names, - view_binding, - &query.order_by, - limit, - offset, - params, - ) - }; - } - if view_select.filter.is_some() { - return Ok(None); - } - - let mut pushed_projection = Vec::with_capacity(select.projection.len()); - for (index, item) in select.projection.iter().enumerate() { - let SelectItem::Expr { expr, alias } = item else { - return Ok(None); - }; - let Expr::Column { table, column } = expr else { - return Ok(None); - }; - if table - .as_deref() - .is_some_and(|qualifier| !identifiers_equal(qualifier, view_binding)) - { - return Ok(None); - } - let Some(view_expr) = - view_projection_expr_for_output_column(&view_select.projection, column) - else { - return Ok(None); - }; - pushed_projection.push(SelectItem::Expr { - expr: view_expr, - alias: Some( - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - ), - }); - } - - if view.temporary { - self.try_execute_indexed_join_limit_projection_select( - view_select, - &pushed_projection, - limit, - offset, - ) - } else { - let persistent_runtime = self.persistent_resolution_runtime(); - persistent_runtime.try_execute_indexed_join_limit_projection_select( - view_select, - &pushed_projection, - limit, - offset, - ) - } - } - - #[allow(clippy::too_many_arguments)] - fn try_execute_ordered_view_projection_limit_select( - &self, - outer_select: &Select, - view_select: &Select, - view_name: &str, - view_column_names: &[String], - view_binding: &str, - order_by: &[OrderBy], - limit: usize, - offset: usize, - params: &[Value], - ) -> Result> { - if order_by.len() != 1 || order_by[0].collation.is_some() { - return Ok(None); - } - let Some(pushed_projection) = pushed_view_projection_for_outer_projection( - &outer_select.projection, - view_select, - view_name, - view_binding, - view_column_names, - ) else { - return Ok(None); - }; - - let mut join_select = view_select.clone(); - join_select.filter = None; - let Some(plan) = self.analyze_indexed_join_limit_projection_select( - &join_select, - &pushed_projection, - limit, - offset, - )? - else { - return Ok(None); - }; - - let Expr::Column { - table: order_table, - column: order_column, - } = &order_by[0].expr - else { - return Ok(None); - }; - if order_table.as_deref().is_some_and(|qualifier| { - !identifiers_equal(qualifier, view_binding) && !identifiers_equal(qualifier, view_name) - }) { - return Ok(None); - } - let Some(order_expr) = view_projection_expr_for_output_column_with_names( - &view_select.projection, - view_column_names, - order_column, - ) else { - return Ok(None); - }; - let Some((order_table_index, order_column_index)) = - indexed_join_limit_projection_column(&order_expr, &plan.tables, self) - else { - return Ok(None); - }; - if order_table_index != 0 { - return Ok(None); - } - - let root_table = plan.tables[0]; - let root_binding = root_table.alias.as_deref().unwrap_or(root_table.name); - let Some(root_schema) = self.table_schema(root_table.name) else { - return Ok(None); - }; - let Some(order_column_schema) = root_schema.columns.get(order_column_index) else { - return Ok(None); - }; - - let root_filter_columns = if let Some(filter) = view_select.filter.as_ref() { - let Some(root_columns) = indexed_join_table_eval_columns(&plan.tables[..1], self) - else { - return Ok(None); - }; - let Some(join_columns) = indexed_join_table_eval_columns(&plan.tables, self) else { - return Ok(None); - }; - let root_dataset = Dataset::with_rows(root_columns.clone(), Vec::new()); - let join_dataset = Dataset::with_rows(join_columns, Vec::new()); - if !expr_resolves_against_dataset(filter, &root_dataset) - || !expr_resolves_against_dataset(filter, &join_dataset) - { - return Ok(None); - } - Some(root_columns) - } else { - None - }; - - let Some(index) = self.ordered_view_root_btree_index( - root_table.name, - &order_column_schema.name, - view_select.filter.as_ref(), - root_binding, - )? - else { - return Ok(None); - }; - - self.execute_ordered_indexed_join_limit_projection_plan( - &plan, - view_select.filter.as_ref(), - root_filter_columns, - &index.name, - order_by[0].descending, - params, - ) - .map(Some) - } - - fn try_execute_indexed_join_limit_projection_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - if query.recursive || !query.ctes.is_empty() || !query.order_by.is_empty() { - return Ok(None); - } - let Some(limit_expr) = query.limit.as_ref() else { - return Ok(None); - }; - let ctes = BTreeMap::new(); - let limit_value = match simple_int64_constant_expr_value(limit_expr, params)? { - Some(value) => value, - None => self.eval_constant_i64(limit_expr, params, &ctes)?, - }; - let limit = usize::try_from(limit_value.max(0)).unwrap_or(usize::MAX); - let offset = query - .offset - .as_ref() - .map(|expr| { - simple_int64_constant_expr_value(expr, params)? - .map(Ok) - .unwrap_or_else(|| self.eval_constant_i64(expr, params, &ctes)) - }) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - self.try_execute_indexed_join_limit_projection_select( - select, - &select.projection, - limit, - offset, - ) - } - - fn try_execute_indexed_join_limit_projection_select( - &self, - select: &Select, - projection: &[SelectItem], - limit: usize, - offset: usize, - ) -> Result> { - if limit == 0 { - let column_names = projection - .iter() - .enumerate() - .map(|(index, item)| match item { - SelectItem::Expr { expr, alias } => alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - _ => format!("col{}", index + 1), - }) - .collect(); - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - } - let Some(plan) = - self.analyze_indexed_join_limit_projection_select(select, projection, limit, offset)? - else { - return Ok(None); - }; - self.execute_indexed_join_limit_projection_plan(&plan) - .map(Some) - } - - fn analyze_indexed_join_limit_projection_select<'a>( - &'a self, - select: &'a Select, - projection: &'a [SelectItem], - limit: usize, - offset: usize, - ) -> Result>> { - if select.distinct - || !select.distinct_on.is_empty() - || select.filter.is_some() - || !select.group_by.is_empty() - || select.having.is_some() - || projection_has_aggregate_items(projection) - || select.from.len() != 1 - { - return Ok(None); - } - let mut tables = Vec::new(); - let mut constraints = Vec::new(); - if !flatten_left_deep_inner_join_tables(&select.from[0], &mut tables, &mut constraints) { - return Ok(None); - } - if !(2..=3).contains(&tables.len()) || constraints.len() + 1 != tables.len() { - return Ok(None); - } - for table in &tables { - if self - .visible_view(table.name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(table.name) - { - return Ok(None); - } - let Some(schema) = self.table_schema(table.name) else { - return Ok(None); - }; - if !generated_columns_are_stored(schema) - || self.visible_table_row_source(table.name).is_none() - { - return Ok(None); - } - } - - let mut steps = Vec::with_capacity(constraints.len()); - for (right_table_index, constraint) in - constraints.iter().enumerate().map(|(i, c)| (i + 1, c)) - { - let Some(step) = self.indexed_join_limit_step_for_constraint( - &tables, - right_table_index, - constraint, - )? - else { - return Ok(None); - }; - steps.push(step); - } - - let mut projections = Vec::with_capacity(projection.len()); - for (index, item) in projection.iter().enumerate() { - let SelectItem::Expr { expr, alias } = item else { - return Ok(None); - }; - let Some((table_index, column_index)) = - indexed_join_limit_projection_column(expr, &tables, self) - else { - return Ok(None); - }; - projections.push(IndexedJoinLimitProjection { - table_index, - column_index, - column_name: alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - }); - } - - Ok(Some(IndexedJoinLimitPlan { - tables, - steps, - projections, - limit, - offset, - })) - } - - fn indexed_join_limit_step_for_constraint( - &self, - tables: &[IndexedJoinLimitTablePlan<'_>], - right_table_index: usize, - constraint: &JoinConstraint, - ) -> Result> { - let JoinConstraint::On(on) = constraint else { - return Ok(None); - }; - let Some(equalities) = simple_join_equalities(on) else { - return Ok(None); - }; - if equalities.len() != 1 { - return Ok(None); - } - let right_table = tables[right_table_index]; - let right_binding = TableBindingRef { - name: right_table.name, - alias: right_table.alias, - }; - let (left_ref, right_ref) = equalities[0]; - let (previous_ref, right_ref) = if matches_table_binding(right_binding, right_ref.table) { - (left_ref, right_ref) - } else if matches_table_binding(right_binding, left_ref.table) { - (right_ref, left_ref) - } else { - return Ok(None); - }; - - let Some(previous_table_index) = (0..right_table_index).find(|index| { - let table = tables[*index]; - matches_table_binding( - TableBindingRef { - name: table.name, - alias: table.alias, - }, - previous_ref.table, - ) - }) else { - return Ok(None); - }; - let previous_schema = self - .table_schema(tables[previous_table_index].name) - .ok_or_else(|| DbError::internal("indexed join previous table missing"))?; - let right_schema = self - .table_schema(right_table.name) - .ok_or_else(|| DbError::internal("indexed join right table missing"))?; - let Some(previous_column_index) = schema_column_index(previous_schema, previous_ref.column) - else { - return Ok(None); - }; - let Some(right_column_index) = schema_column_index(right_schema, right_ref.column) else { - return Ok(None); - }; - if crate::exec::dml::row_id_alias_column_name(right_schema) - .is_some_and(|column| identifiers_equal(column, right_ref.column)) - { - let _ = right_column_index; - return Ok(Some(IndexedJoinLimitStep { - previous_table_index, - previous_column_index, - right_index_name: None, - })); - } - let Some(index) = self.catalog.indexes.values().find(|index| { - identifiers_equal(&index.table_name, right_table.name) - && index.fresh - && index.kind == IndexKind::Btree - && index.predicate_sql.is_none() - && index.columns.len() == 1 - && index.columns[0].expression_sql.is_none() - && index.columns[0] - .column_name - .as_deref() - .is_some_and(|column| identifiers_equal(column, right_ref.column)) - }) else { - let _ = right_column_index; - return Ok(None); - }; - let _ = right_column_index; - Ok(Some(IndexedJoinLimitStep { - previous_table_index, - previous_column_index, - right_index_name: Some(index.name.clone()), - })) - } - - fn execute_indexed_join_limit_projection_plan( - &self, - plan: &IndexedJoinLimitPlan<'_>, - ) -> Result { - let sources = plan - .tables - .iter() - .map(|table| { - self.visible_table_row_source(table.name).ok_or_else(|| { - DbError::internal(format!("table {} row source is missing", table.name)) - }) - }) - .collect::>>()?; - let keys = plan - .steps - .iter() - .map(|step| { - let Some(index_name) = step.right_index_name.as_deref() else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(index_name) else { - return Err(DbError::internal(format!( - "index {index_name} is missing for indexed join limit plan", - ))); - }; - Ok(Some(keys)) - }) - .collect::>>()?; - - let mut rows = Vec::new(); - let mut offset_remaining = plan.offset; - let mut limit_remaining = plan.limit; - for root_row in sources[0].rows() { - let root_row = root_row?; - let current0 = root_row.values().to_vec(); - if plan.tables.len() == 2 { - let step0 = &plan.steps[0]; - let Some(probe_value) = current0.get(step0.previous_column_index) else { - return Err(DbError::internal("join probe row is shorter than schema")); - }; - for row1 in indexed_join_limit_rows_for_value(sources[1], keys[0], probe_value)? { - let current = [¤t0[..], &row1[..]]; - if push_indexed_join_limit_projection( - ¤t, - &plan.projections, - &mut offset_remaining, - &mut limit_remaining, - &mut rows, - ) { - return Ok(indexed_join_limit_result(plan, rows)); - } - } - } else { - let step0 = &plan.steps[0]; - let Some(probe_value0) = current0.get(step0.previous_column_index) else { - return Err(DbError::internal("join probe row is shorter than schema")); - }; - for row1 in indexed_join_limit_rows_for_value(sources[1], keys[0], probe_value0)? { - let current01 = [¤t0[..], &row1[..]]; - let step1 = &plan.steps[1]; - let Some(probe_value1) = current01 - .get(step1.previous_table_index) - .and_then(|row| row.get(step1.previous_column_index)) - else { - return Err(DbError::internal("join probe row is shorter than schema")); - }; - for row2 in - indexed_join_limit_rows_for_value(sources[2], keys[1], probe_value1)? - { - let current = [¤t0[..], &row1[..], &row2[..]]; - if push_indexed_join_limit_projection( - ¤t, - &plan.projections, - &mut offset_remaining, - &mut limit_remaining, - &mut rows, - ) { - return Ok(indexed_join_limit_result(plan, rows)); - } - } - } - } - } - Ok(indexed_join_limit_result(plan, rows)) - } - - fn execute_ordered_indexed_join_limit_projection_plan( - &self, - plan: &IndexedJoinLimitPlan<'_>, - root_filter: Option<&Expr>, - root_filter_columns: Option>, - order_index_name: &str, - descending: bool, - params: &[Value], - ) -> Result { - let sources = plan - .tables - .iter() - .map(|table| { - self.visible_table_row_source(table.name).ok_or_else(|| { - DbError::internal(format!("table {} row source is missing", table.name)) - }) - }) - .collect::>>()?; - let keys = plan - .steps - .iter() - .map(|step| { - let Some(index_name) = step.right_index_name.as_deref() else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(index_name) else { - return Err(DbError::internal(format!( - "index {index_name} is missing for ordered indexed join limit plan", - ))); - }; - Ok(Some(keys)) - }) - .collect::>>()?; - let Some(RuntimeIndex::Btree { - keys: order_keys, .. - }) = self.index(order_index_name) - else { - return Err(DbError::internal(format!( - "ordered index {order_index_name} is missing for ordered view limit plan", - ))); - }; - - let root_filter_dataset = - root_filter_columns.map(|columns| Dataset::with_rows(columns, Vec::new())); - let mut rows = Vec::new(); - let mut offset_remaining = plan.offset; - let mut limit_remaining = plan.limit; - let ctes = BTreeMap::new(); - - visit_runtime_btree_row_ids_in_order(order_keys, descending, |root_row_id| { - let Some(root_row) = sources[0].row_by_id(root_row_id)? else { - return Ok(false); - }; - if let (Some(filter), Some(dataset)) = (root_filter, root_filter_dataset.as_ref()) { - if !matches!( - self.eval_expr(filter, dataset, root_row.values(), params, &ctes, None)?, - Value::Bool(true) - ) { - return Ok(false); - } - } - - if plan.tables.len() == 2 { - let step0 = &plan.steps[0]; - let Some(probe_value) = root_row.values().get(step0.previous_column_index) else { - return Err(DbError::internal("join probe row is shorter than schema")); - }; - for row1_id in indexed_join_row_ids_for_value(keys[0], probe_value)? { - let Some(row1) = sources[1].row_by_id(row1_id)? else { - continue; - }; - let current = [root_row.values(), row1.values()]; - if push_indexed_join_limit_projection( - ¤t, - &plan.projections, - &mut offset_remaining, - &mut limit_remaining, - &mut rows, - ) { - return Ok(true); - } - } - } else { - let step0 = &plan.steps[0]; - let Some(probe_value0) = root_row.values().get(step0.previous_column_index) else { - return Err(DbError::internal("join probe row is shorter than schema")); - }; - for row1_id in indexed_join_row_ids_for_value(keys[0], probe_value0)? { - let Some(row1) = sources[1].row_by_id(row1_id)? else { - continue; - }; - let current01 = [root_row.values(), row1.values()]; - let step1 = &plan.steps[1]; - let Some(probe_value1) = current01 - .get(step1.previous_table_index) - .and_then(|row| row.get(step1.previous_column_index)) - else { - return Err(DbError::internal("join probe row is shorter than schema")); - }; - for row2_id in indexed_join_row_ids_for_value(keys[1], probe_value1)? { - let Some(row2) = sources[2].row_by_id(row2_id)? else { - continue; - }; - let current = [root_row.values(), row1.values(), row2.values()]; - if push_indexed_join_limit_projection( - ¤t, - &plan.projections, - &mut offset_remaining, - &mut limit_remaining, - &mut rows, - ) { - return Ok(true); - } - } - } - } - Ok(false) - })?; - - Ok(indexed_join_limit_result(plan, rows)) - } - - fn ordered_view_root_btree_index( - &self, - table_name: &str, - column_name: &str, - root_filter: Option<&Expr>, - root_binding: &str, - ) -> Result> { - let mut full_index = None; - for index in self.catalog.indexes.values() { - if !single_plain_btree_index_matches_column(index, table_name, column_name) { - continue; - } - let Some(predicate_sql) = index.predicate_sql.as_deref() else { - if full_index.is_none() { - full_index = Some(index); - } - continue; - }; - let Some(root_filter) = root_filter else { - continue; - }; - let predicate = crate::sql::parser::parse_expression_sql(predicate_sql)?; - if filter_contains_partial_index_predicate(root_filter, &predicate, root_binding) { - return Ok(Some(index)); - } - } - Ok(full_index) - } - - fn execute_indexed_join_projection_rows( - &self, - plan: &IndexedJoinLimitPlan<'_>, - enforce_root_rowid_order: bool, - second_table_order_column: Option, - ) -> Result> { - if plan.tables.len() != 3 || plan.steps.len() != 2 { - return Err(DbError::internal( - "indexed join projection rows path expects a three-table chain", - )); - } - let sources = plan - .tables - .iter() - .map(|table| { - self.visible_table_row_source(table.name).ok_or_else(|| { - DbError::internal(format!("table {} row source is missing", table.name)) - }) - }) - .collect::>>()?; - let keys = plan - .steps - .iter() - .map(|step| { - let Some(index_name) = step.right_index_name.as_deref() else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(index_name) else { - return Err(DbError::internal(format!( - "index {index_name} is missing for indexed join projection plan", - ))); - }; - Ok(Some(keys)) - }) - .collect::>>()?; - - let mut root_row_ids = Vec::with_capacity(sources[0].row_count()); - for root_row in sources[0].rows() { - root_row_ids.push(root_row?.row_id()); - } - if enforce_root_rowid_order { - root_row_ids.sort_unstable(); - } - - let mut rows = Vec::new(); - for root_row_id in root_row_ids { - let Some(root_row) = sources[0].row_by_id(root_row_id)? else { - continue; - }; - let step0 = &plan.steps[0]; - let Some(probe_value0) = root_row.values().get(step0.previous_column_index) else { - return Err(DbError::internal("join probe row is shorter than schema")); - }; - let mut row1_ids = indexed_join_row_ids_for_value(keys[0], probe_value0)?; - if let Some(column_index) = second_table_order_column { - row1_ids = sort_join_row_ids_by_column(sources[1], row1_ids, column_index)?; - } - for row1_id in row1_ids { - let Some(row1) = sources[1].row_by_id(row1_id)? else { - continue; - }; - let current01 = [root_row.values(), row1.values()]; - let step1 = &plan.steps[1]; - let Some(probe_value1) = current01 - .get(step1.previous_table_index) - .and_then(|row| row.get(step1.previous_column_index)) - else { - return Err(DbError::internal("join probe row is shorter than schema")); - }; - for row2_id in indexed_join_row_ids_for_value(keys[1], probe_value1)? { - let Some(row2) = sources[2].row_by_id(row2_id)? else { - continue; - }; - let current = [root_row.values(), row1.values(), row2.values()]; - rows.push(project_indexed_join_row(¤t, &plan.projections)?); - } - } - } - Ok(rows) - } - - fn try_execute_three_table_indexed_join_projection_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - if query.recursive || !query.ctes.is_empty() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.from.len() != 1 { - return Ok(None); - } - let mut tables = Vec::new(); - let mut constraints = Vec::new(); - if !flatten_left_deep_inner_join_tables(&select.from[0], &mut tables, &mut constraints) - || tables.len() != 3 - || constraints.len() != 2 - { - return Ok(None); - } - let natural_order = if query.order_by.is_empty() { - None - } else { - self.three_table_join_natural_order(query, &tables) - }; - let order_by = if query.order_by.is_empty() || natural_order.is_some() { - None - } else { - let Some(order_by) = projection_order_by_plan(&query.order_by, &select.projection) - else { - return Ok(None); - }; - Some(order_by) - }; - let Some(plan) = self.analyze_indexed_join_limit_projection_select( - select, - &select.projection, - usize::MAX, - 0, - )? - else { - return Ok(None); - }; - if plan.tables.len() != 3 { - return Ok(None); - } - - let mut rows = self.execute_indexed_join_projection_rows( - &plan, - natural_order.is_some(), - natural_order.flatten(), - )?; - if let Some(order_by) = order_by.as_deref() { - sort_query_rows_by_projection_order(Some(self), &mut rows, order_by)?; - } - - let ctes = BTreeMap::new(); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &ctes)) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &ctes)) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - if offset > 0 || limit.is_some() { - rows = rows - .into_iter() - .skip(offset) - .take(limit.unwrap_or(usize::MAX)) - .collect(); - } - Ok(Some(QueryResult::with_rows( - plan.projections - .iter() - .map(|projection| projection.column_name.clone()) - .collect(), - rows, - ))) - } - - fn three_table_join_natural_order( - &self, - query: &Query, - tables: &[IndexedJoinLimitTablePlan<'_>], - ) -> Option> { - if !(1..=2).contains(&query.order_by.len()) { - return None; - } - if query - .order_by - .iter() - .any(|order| order.descending || order.collation.is_some()) - { - return None; - } - let first_schema = self.table_schema(tables[0].name)?; - let first_rowid_column = crate::exec::dml::row_id_alias_column_name(first_schema)?; - let Expr::Column { - table: first_table, - column: first_column, - } = &query.order_by[0].expr - else { - return None; - }; - if !matches_table_binding( - TableBindingRef { - name: tables[0].name, - alias: tables[0].alias, - }, - first_table.as_deref(), - ) || !identifiers_equal(first_column, first_rowid_column) - { - return None; - } - if query.order_by.len() == 1 { - return Some(None); - } - - let second_schema = self.table_schema(tables[1].name)?; - let Expr::Column { - table: second_table, - column: second_column, - } = &query.order_by[1].expr - else { - return None; - }; - if !matches_table_binding( - TableBindingRef { - name: tables[1].name, - alias: tables[1].alias, - }, - second_table.as_deref(), - ) { - return None; - } - schema_column_index(second_schema, second_column).map(Some) - } - - fn try_execute_base_table_join( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_base_table_join_query(query) else { - return Ok(None); - }; - let Some(left_source) = self.visible_table_row_source(plan.left_name) else { - return Ok(None); - }; - let Some(right_source) = self.visible_table_row_source(plan.right_name) else { - return Ok(None); - }; - Ok(Some(self.execute_base_table_join_from_sources( - left_source, - right_source, - &plan, - params, - )?)) - } - - fn analyze_base_table_join_query<'a>( - &'a self, - query: &'a Query, - ) -> Option> { - if !query.ctes.is_empty() || query.recursive { - return None; - } - let QueryBody::Select(select) = &query.body else { - return None; - }; - if !select.group_by.is_empty() - || select.having.is_some() - || projection_has_aggregate_items(&select.projection) - { - return None; - } - if select.from.len() != 1 { - return None; - } - let FromItem::Join { - left, - right, - kind, - constraint, - } = &select.from[0] - else { - return None; - }; - if !matches!( - kind, - JoinKind::Inner | JoinKind::Left | JoinKind::Right | JoinKind::Full - ) { - return None; - } - let (left_name, left_alias) = match &**left { - FromItem::Table { name, alias } => (name, alias.as_deref()), - _ => return None, - }; - let (right_name, right_alias) = match &**right { - FromItem::Table { name, alias } => (name, alias.as_deref()), - _ => return None, - }; - if self - .visible_view(left_name, NameResolutionScope::Session) - .is_some() - || self - .visible_view(right_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(left_name) - || self.visible_table_is_temporary(right_name) - { - return None; - } - if self.table_schema(left_name).is_none() || self.table_schema(right_name).is_none() { - return None; - } - if select.projection.iter().any(|item| { - matches!( - item, - SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) - ) - }) { - return None; - } - for item in &select.projection { - if let SelectItem::Expr { expr, .. } = item { - if expr_contains_window(expr) { - return None; - } - } - } - for order in &query.order_by { - if expr_contains_window(&order.expr) { - return None; - } - } - if select.filter.as_ref().is_some_and(expr_contains_window) { - return None; - } - Some(BaseTableJoinPlan { - left_name, - left_alias, - right_name, - right_alias, - kind: *kind, - constraint, - filter: select.filter.as_ref(), - projection: &select.projection, - order_by: &query.order_by, - distinct: select.distinct, - limit: query.limit.as_ref(), - offset: query.offset.as_ref(), - }) - } - - fn execute_base_table_join_from_sources( - &self, - left_source: VisibleTableRowSource<'_>, - right_source: VisibleTableRowSource<'_>, - plan: &BaseTableJoinPlan<'_>, - params: &[Value], - ) -> Result { - let left_table = self.table_schema(plan.left_name).ok_or_else(|| { - DbError::internal(format!("table {} not found for join", plan.left_name)) - })?; - let right_table = self.table_schema(plan.right_name).ok_or_else(|| { - DbError::internal(format!("table {} not found for join", plan.right_name)) - })?; - - let left_binding_name = plan.left_alias.unwrap_or(plan.left_name); - let right_binding_name = plan.right_alias.unwrap_or(plan.right_name); - - let left_columns: Vec = left_table - .columns - .iter() - .map(|c| { - ColumnBinding::visible_source( - Some(left_binding_name.to_string()), - Some(plan.left_name.to_string()), - c.name.clone(), - ) - }) - .collect(); - let right_columns: Vec = right_table - .columns - .iter() - .map(|c| { - ColumnBinding::visible_source( - Some(right_binding_name.to_string()), - Some(plan.right_name.to_string()), - c.name.clone(), - ) - }) - .collect(); - - let using_columns = resolve_join_using_columns_for_schemas( - &left_columns, - &right_columns, - plan.constraint, - left_table, - right_table, - )?; - - let eval_columns: Vec = left_columns - .iter() - .cloned() - .chain(right_columns.iter().cloned()) - .collect(); - let eval_dataset = Dataset::with_rows(eval_columns, Vec::new()); - let ctes = BTreeMap::new(); - - let left_needs_virtual_generated = !generated_columns_are_stored(left_table); - let right_needs_virtual_generated = !generated_columns_are_stored(right_table); - - let left_nulls = vec![Value::Null; left_columns.len()]; - let right_nulls = vec![Value::Null; right_columns.len()]; - - let mut right_rows: Vec> = Vec::new(); - for row_result in right_source.rows() { - let row_ref = row_result?; - let mut values = row_ref.values().to_vec(); - if right_needs_virtual_generated { - self.apply_virtual_generated_columns(right_table, &mut values)?; - } - right_rows.push(values); - } - - let mut matched_right = vec![false; right_rows.len()]; - let mut join_output: Vec> = Vec::new(); - - for left_row_result in left_source.rows() { - let mut left_values = left_row_result?.values().to_vec(); - if left_needs_virtual_generated { - self.apply_virtual_generated_columns(left_table, &mut left_values)?; - } - - let mut matched = false; - for (right_index, right_values) in right_rows.iter().enumerate() { - let mut eval_row = left_values.clone(); - eval_row.extend(right_values.clone()); - if join_rows_match( - plan.constraint, - &using_columns, - &eval_row, - &left_values, - right_values, - &JoinEvalContext { - dataset: &eval_dataset, - runtime: self, - params, - ctes: &ctes, - }, - )? { - matched = true; - matched_right[right_index] = true; - join_output.push(join_output_row(&left_values, right_values, &using_columns)?); - } - } - if !matched && matches!(plan.kind, JoinKind::Left | JoinKind::Full) { - join_output.push(join_output_row(&left_values, &right_nulls, &using_columns)?); - } - } - if matches!(plan.kind, JoinKind::Right | JoinKind::Full) { - for (matched, right_values) in matched_right.iter().zip(right_rows.iter()) { - if !matched { - join_output.push(join_output_row(&left_nulls, right_values, &using_columns)?); - } - } - } - - let result_columns: Vec = plan - .projection - .iter() - .enumerate() - .map(|(index, item)| match item { - SelectItem::Expr { expr, alias } => ColumnBinding::visible( - None, - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - ), - _ => ColumnBinding::visible(None, format!("col{}", index + 1)), - }) - .collect(); - - let join_ds_columns: Vec = left_columns - .iter() - .cloned() - .chain(right_columns.iter().cloned()) - .collect(); - let mut join_dataset = Dataset::with_rows(join_ds_columns, join_output); - if let Some(filter) = plan.filter { - let filter_ds = Dataset::with_rows(join_dataset.columns.clone(), Vec::new()); - let mut filtered = Vec::with_capacity(join_dataset.rows.len()); - for row in join_dataset.take_rows() { - let val = self.eval_expr(filter, &filter_ds, &row, params, &ctes, None)?; - if matches!(val, Value::Bool(true)) { - filtered.push(row); - } - } - join_dataset.set_rows(filtered); - } - - let mut output_rows: Vec> = Vec::new(); - for row in join_dataset.rows.iter() { - let mut projected = Vec::with_capacity(plan.projection.len()); - for item in plan.projection { - let SelectItem::Expr { expr, .. } = item else { - return Err(DbError::sql( - "wildcards not supported in join SELECT output", - )); - }; - projected.push(self.eval_expr(expr, &join_dataset, row, params, &ctes, None)?); - } - output_rows.push(projected); - } - - let has_order_by = !plan.order_by.is_empty(); - let mut rows_with_order: Vec<(Vec, Vec)> = if has_order_by { - let order_ds = Dataset::with_rows(result_columns.clone(), output_rows.clone()); - output_rows - .into_iter() - .map(|row| { - let order_values: Vec = plan - .order_by - .iter() - .map(|order| { - self.eval_expr(&order.expr, &order_ds, &row, params, &ctes, None) - }) - .collect::>>()?; - Ok((row, order_values)) - }) - .collect::>>()? - } else { - output_rows - .into_iter() - .map(|row| (row, Vec::new())) - .collect() - }; - - if has_order_by { - let mut sort_error = None; - rows_with_order.sort_by(|(_, left_order), (_, right_order)| { - match compare_query_row_order_values( - Some(self), - left_order, - right_order, - plan.order_by, - ) { - Ok(ordering) => ordering, - Err(error) => { - if sort_error.is_none() { - sort_error = Some(error); - } - std::cmp::Ordering::Equal - } - } - }); - if let Some(error) = sort_error { - return Err(error); - } - } - - let mut rows: Vec = if plan.distinct { - let mut seen = BTreeSet::new(); - let mut distinct_rows = Vec::new(); - for (output, _) in rows_with_order { - if seen.insert(row_identity(&output)?) { - distinct_rows.push(QueryRow::new(output)); - } - } - distinct_rows - } else { - rows_with_order - .into_iter() - .map(|(output, _)| QueryRow::new(output)) - .collect() - }; - - let offset_val = plan - .offset - .map(|expr| self.eval_constant_i64(expr, params, &ctes)) - .transpose()? - .unwrap_or(0); - let limit_val = plan - .limit - .map(|expr| self.eval_constant_i64(expr, params, &ctes)) - .transpose()?; - - let start = usize::try_from(offset_val.max(0)).unwrap_or(usize::MAX); - if start > 0 || limit_val.is_some() { - let take = limit_val - .map(|l| usize::try_from(l.max(0)).unwrap_or(0)) - .unwrap_or(usize::MAX); - rows = rows.into_iter().skip(start).take(take).collect(); - } - - let column_names: Vec = result_columns.into_iter().map(|c| c.name).collect(); - Ok(QueryResult::with_rows(column_names, rows)) - } - - pub(crate) fn try_execute_simple_indexed_join_projection_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - if !query.ctes.is_empty() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if !select.group_by.is_empty() - || select.having.is_some() - || projection_has_aggregate_items(&select.projection) - || select.from.len() != 1 - { - return Ok(None); - } - if !select.distinct_on.is_empty() { - return Ok(None); - } - let FromItem::Join { - left, - right, - kind, - constraint, - } = &select.from[0] - else { - return Ok(None); - }; - if !matches!( - kind, - JoinKind::Inner | JoinKind::Left | JoinKind::Right | JoinKind::Full - ) { - return Ok(None); - } - let is_left_outer = matches!(kind, JoinKind::Left | JoinKind::Full); - let is_right_outer = matches!(kind, JoinKind::Right | JoinKind::Full); - let (left_name, left_alias) = match &**left { - FromItem::Table { name, alias } => (name, alias), - _ => return Ok(None), - }; - let (right_name, right_alias) = match &**right { - FromItem::Table { name, alias } => (name, alias), - _ => return Ok(None), - }; - if self - .visible_view(left_name, NameResolutionScope::Session) - .is_some() - || self - .visible_view(right_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(left_name) - || self.visible_table_is_temporary(right_name) - { - return Ok(None); - } - - let left_binding = TableBindingRef { - name: left_name, - alias: left_alias, - }; - let right_binding = TableBindingRef { - name: right_name, - alias: right_alias, - }; - let left_schema = match self.table_schema(left_name) { - Some(table) => table, - None => return Ok(None), - }; - let right_schema = match self.table_schema(right_name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(left_schema) || !generated_columns_are_stored(right_schema) - { - return Ok(None); - } - let Some(join_equalities) = simple_indexed_join_constraint_equalities( - constraint, - left_binding, - right_binding, - left_schema, - right_schema, - ) else { - return Ok(None); - }; - let Some((left_join_columns, right_join_columns)) = - orient_join_equalities(&join_equalities, left_binding, right_binding) - else { - return Ok(None); - }; - let using_join_columns = - simple_indexed_join_using_columns(constraint, left_schema, right_schema); - - let left_source = match self.visible_table_row_source(left_name) { - Some(source) => source, - None => return Ok(None), - }; - let right_source = match self.visible_table_row_source(right_name) { - Some(source) => source, - None => return Ok(None), - }; - let join_eval_bindings = simple_join_projection_eval_bindings( - left_name, - left_alias, - left_schema, - right_name, - right_alias, - right_schema, - ); - let join_eval_dataset = Dataset::with_rows(join_eval_bindings, Vec::new()); - let default_source_is_left = if matches!(kind, JoinKind::Full) { - true - } else { - !is_right_outer - }; - let (source_is_left, source_filter, post_join_filter) = if let Some(filter) = - select.filter.as_ref() - { - if let Some((filter_table, filter_column, value_expr)) = simple_btree_lookup(filter) { - if matches_table_binding(left_binding, filter_table) && !is_right_outer { - (true, Some((filter_column, value_expr)), None) - } else if !is_left_outer && matches_table_binding(right_binding, filter_table) { - (false, Some((filter_column, value_expr)), None) - } else if !expr_contains_recursive_unsupported_feature(filter) - && expr_resolves_against_dataset(filter, &join_eval_dataset) - { - (default_source_is_left, None, Some(filter.clone())) - } else { - return Ok(None); - } - } else if !expr_contains_recursive_unsupported_feature(filter) - && expr_resolves_against_dataset(filter, &join_eval_dataset) - { - (default_source_is_left, None, Some(filter.clone())) - } else { - return Ok(None); - } - } else { - (default_source_is_left, None, None) - }; - let Some((projection_plan, column_names)) = simple_join_projection_plan( - &select.projection, - &join_eval_dataset, - left_name, - left_alias, - left_schema, - right_name, - right_alias, - right_schema, - &using_join_columns, - ) else { - return Ok(None); - }; - let order_by = simple_join_projection_order_by_plan( - query, - &select.projection, - &projection_plan, - &column_names, - left_name, - left_alias, - left_schema, - right_name, - right_alias, - right_schema, - )?; - - let ( - source_table, - source_schema, - source_join_columns, - source_source, - probe_table, - probe_schema, - probe_join_columns, - probe_source, - ) = if source_is_left { - ( - left_name, - left_schema, - left_join_columns, - left_source, - right_name, - right_schema, - right_join_columns, - right_source, - ) - } else { - ( - right_name, - right_schema, - right_join_columns, - right_source, - left_name, - left_schema, - left_join_columns, - left_source, - ) - }; - let mut source_join_indexes = Vec::with_capacity(source_join_columns.len()); - for source_join_column in &source_join_columns { - let source_join_index = source_schema - .columns - .iter() - .position(|column| identifiers_equal(&column.name, source_join_column)) - .ok_or_else(|| DbError::sql(format!("unknown column {source_join_column}")))?; - source_join_indexes.push(source_join_index); - } - let source_row_ids = if let Some((filter_column, value_expr)) = source_filter { - let filter_value = self.eval_expr( - value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?; - if crate::exec::dml::row_id_alias_column_name(source_schema) - .is_some_and(|name| identifiers_equal(name, filter_column)) - { - Some(match filter_value { - Value::Int64(row_id) => RuntimeRowIdSet::Single(row_id), - _ => RuntimeRowIdSet::Empty, - }) - } else { - let Some(filter_index) = self.catalog.indexes.values().find(|index| { - identifiers_equal(&index.table_name, source_table) - && index.fresh - && index.kind == IndexKind::Btree - && index.predicate_sql.is_none() - && index.columns.len() == 1 - && index.columns[0] - .column_name - .as_deref() - .is_some_and(|index_column| { - identifiers_equal(index_column, filter_column) - }) - && index.columns[0].expression_sql.is_none() - }) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&filter_index.name) else { - return Ok(None); - }; - Some(keys.row_ids_for_value_set(&filter_value)?) - } - } else { - None - }; - - let is_probe_rowid_alias = probe_join_columns.len() == 1 - && crate::exec::dml::row_id_alias_column_name(probe_schema) - .is_some_and(|name| identifiers_equal(name, probe_join_columns[0])); - let (probe_index, ordered_source_join_indexes) = if is_probe_rowid_alias { - (None, source_join_indexes) - } else { - match self.catalog.indexes.values().find_map(|index| { - if !identifiers_equal(&index.table_name, probe_table) - || !index.fresh - || index.kind != IndexKind::Btree - || index.predicate_sql.is_some() - || index.columns.len() != probe_join_columns.len() - { - return None; - } - let mut ordered_source_join_indexes = Vec::with_capacity(index.columns.len()); - for index_column in &index.columns { - if index_column.expression_sql.is_some() { - return None; - } - let index_column_name = index_column.column_name.as_deref()?; - let join_position = probe_join_columns.iter().position(|join_column| { - identifiers_equal(join_column, index_column_name) - })?; - ordered_source_join_indexes.push(source_join_indexes[join_position]); - } - Some((index, ordered_source_join_indexes)) - }) { - Some((probe_index, ordered_source_join_indexes)) => { - (Some(probe_index), ordered_source_join_indexes) - } - None => (None, source_join_indexes), - } - }; - let keys = if let Some(index) = probe_index { - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { - return Ok(None); - }; - Some(keys) - } else { - None - }; - let probe_join_indexes = if probe_index.is_none() && !is_probe_rowid_alias { - let mut probe_join_indexes = Vec::with_capacity(probe_join_columns.len()); - for probe_join_column in &probe_join_columns { - let Some(probe_join_index) = probe_schema - .columns - .iter() - .position(|column| identifiers_equal(&column.name, probe_join_column)) - else { - return Ok(None); - }; - probe_join_indexes.push(probe_join_index); - } - Some(probe_join_indexes) - } else { - None - }; - - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let early_stop_limit = if order_by.is_none() && !select.distinct { - limit.map(|limit| limit.saturating_add(offset)) - } else { - None - }; - - let use_probe_row_position_map = probe_source - .row_count() - .saturating_mul(source_source.row_count()) - > 8_192; - let probe_row_positions = if use_probe_row_position_map { - let mut positions = Int64Map::::default(); - for (position, row) in probe_source.rows().enumerate() { - positions.insert(row?.row_id(), position); - } - Some(positions) - } else { - None - }; - let probe_hash_rows = if let Some(probe_join_indexes) = probe_join_indexes.as_ref() { - let mut hashed = SimpleJoinHashRows::new(); - for probe_row in probe_source.rows() { - let probe_row = probe_row?; - let Some(join_key) = - simple_join_key_from_indexes(probe_row.values(), probe_join_indexes)? - else { - continue; - }; - hashed - .entry(join_key) - .or_default() - .push((probe_row.row_id(), probe_row.values().to_vec())); - } - Some(hashed) - } else { - None - }; - - let mut rows = Vec::new(); - let mut stop = false; - let left_width = left_schema.columns.len(); - let right_width = right_schema.columns.len(); - let mut matched_probe_row_ids = - matches!(kind, JoinKind::Full).then(Int64Map::<()>::default); - if let Some(source_row_ids) = source_row_ids { - match source_row_ids { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(source_row_id) => { - if let Some(source_row) = source_source.row_by_id(source_row_id)? { - let _ = self.process_simple_indexed_join_source_row( - *kind, - source_is_left, - source_row.values(), - &ordered_source_join_indexes, - probe_source, - probe_row_positions.as_ref(), - keys, - probe_hash_rows.as_ref(), - matched_probe_row_ids.as_mut(), - post_join_filter.as_ref(), - &projection_plan, - &join_eval_dataset, - left_width, - right_width, - params, - early_stop_limit, - &mut rows, - )?; - } - } - RuntimeRowIdSet::Contiguous { start, len } => { - for source_row_id in contiguous_row_ids(start, len) { - if stop { - break; - } - let Some(source_row) = source_source.row_by_id(source_row_id)? else { - continue; - }; - stop = self.process_simple_indexed_join_source_row( - *kind, - source_is_left, - source_row.values(), - &ordered_source_join_indexes, - probe_source, - probe_row_positions.as_ref(), - keys, - probe_hash_rows.as_ref(), - matched_probe_row_ids.as_mut(), - post_join_filter.as_ref(), - &projection_plan, - &join_eval_dataset, - left_width, - right_width, - params, - early_stop_limit, - &mut rows, - )?; - } - } - RuntimeRowIdSet::Many(source_row_ids) => { - for source_row_id in source_row_ids { - if stop { - break; - } - let Some(source_row) = source_source.row_by_id(*source_row_id)? else { - continue; - }; - stop = self.process_simple_indexed_join_source_row( - *kind, - source_is_left, - source_row.values(), - &ordered_source_join_indexes, - probe_source, - probe_row_positions.as_ref(), - keys, - probe_hash_rows.as_ref(), - matched_probe_row_ids.as_mut(), - post_join_filter.as_ref(), - &projection_plan, - &join_eval_dataset, - left_width, - right_width, - params, - early_stop_limit, - &mut rows, - )?; - } - } - RuntimeRowIdSet::Owned(source_row_ids) => { - for source_row_id in source_row_ids { - if stop { - break; - } - let Some(source_row) = source_source.row_by_id(source_row_id)? else { - continue; - }; - stop = self.process_simple_indexed_join_source_row( - *kind, - source_is_left, - source_row.values(), - &ordered_source_join_indexes, - probe_source, - probe_row_positions.as_ref(), - keys, - probe_hash_rows.as_ref(), - matched_probe_row_ids.as_mut(), - post_join_filter.as_ref(), - &projection_plan, - &join_eval_dataset, - left_width, - right_width, - params, - early_stop_limit, - &mut rows, - )?; - } - } - } - } else { - for source_row in source_source.rows() { - let source_row = source_row?; - stop = self.process_simple_indexed_join_source_row( - *kind, - source_is_left, - source_row.values(), - &ordered_source_join_indexes, - probe_source, - probe_row_positions.as_ref(), - keys, - probe_hash_rows.as_ref(), - matched_probe_row_ids.as_mut(), - post_join_filter.as_ref(), - &projection_plan, - &join_eval_dataset, - left_width, - right_width, - params, - early_stop_limit, - &mut rows, - )?; - if stop { - break; - } - } - } - if matches!(kind, JoinKind::Full) && source_is_left && !stop { - if let Some(matched_probe_row_ids) = matched_probe_row_ids.as_ref() { - for probe_row in probe_source.rows() { - let probe_row = probe_row?; - if matched_probe_row_ids.contains_key(&probe_row.row_id()) { - continue; - } - if !simple_join_filter_matches( - self, - post_join_filter.as_ref(), - &join_eval_dataset, - None, - left_width, - Some(probe_row.values()), - right_width, - params, - )? { - continue; - } - rows.push(project_simple_join_row( - self, - &projection_plan, - &join_eval_dataset, - None, - left_width, - Some(probe_row.values()), - right_width, - params, - )?); - if early_stop_limit.is_some_and(|limit| rows.len() >= limit) { - break; - } - } - } - } - let mut rows = rows.into_iter().map(QueryRow::new).collect::>(); - if select.distinct { - rows = dedup_query_rows(rows)?; - } - Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - column_names, - order_by.as_deref(), - limit, - offset, - )?)) - } - - #[allow(clippy::too_many_arguments)] - fn process_simple_indexed_join_source_row( - &self, - kind: JoinKind, - source_is_left: bool, - source_values: &[Value], - source_join_indexes: &[usize], - probe_source: VisibleTableRowSource<'_>, - probe_row_positions: Option<&Int64Map>, - probe_keys: Option<&RuntimeBtreeKeys>, - probe_hash_rows: Option<&SimpleJoinHashRows>, - mut matched_probe_row_ids: Option<&mut Int64Map<()>>, - post_join_filter: Option<&Expr>, - projection_plan: &[SimpleJoinProjectionSource], - join_eval_dataset: &Dataset, - left_width: usize, - right_width: usize, - params: &[Value], - early_stop_limit: Option, - rows: &mut Vec>, - ) -> Result { - let join_values = source_join_indexes - .iter() - .map(|source_join_index| { - source_values - .get(*source_join_index) - .ok_or_else(|| DbError::internal("join row is shorter than table schema")) - }) - .collect::>>()?; - if join_values - .iter() - .any(|join_value| matches!(join_value, Value::Null)) - { - if matches!(kind, JoinKind::Inner) { - return Ok(false); - } - if !simple_join_filter_matches( - self, - post_join_filter, - join_eval_dataset, - if source_is_left { - Some(source_values) - } else { - None - }, - left_width, - if source_is_left { - None - } else { - Some(source_values) - }, - right_width, - params, - )? { - return Ok(false); - } - rows.push(project_simple_join_row( - self, - projection_plan, - join_eval_dataset, - if source_is_left { - Some(source_values) - } else { - None - }, - left_width, - if source_is_left { - None - } else { - Some(source_values) - }, - right_width, - params, - )?); - return Ok(early_stop_limit.is_some_and(|limit| rows.len() >= limit)); - } - - let join_key = Row::new(join_values.iter().cloned().cloned().collect()).encode()?; - let probe_row_ids = if let Some(keys) = probe_keys { - if join_values.len() == 1 { - keys.row_ids_for_value_set(join_values[0])? - } else { - keys.row_id_set_for_key(&RuntimeBtreeKey::Encoded(RuntimeEncodedKey::from_vec( - Row::new(join_values.into_iter().cloned().collect()).encode()?, - ))) - } - } else if join_values.len() == 1 { - let join_value = join_values[0]; - if let Value::Int64(val) = join_value { - RuntimeRowIdSet::Single(*val) - } else { - RuntimeRowIdSet::Empty - } - } else { - RuntimeRowIdSet::Empty - }; - let rows_before = rows.len(); - let mut should_stop = false; - let mut row_error = None; - - if let Some(probe_hash_rows) = probe_hash_rows { - if let Some(matching_probe_rows) = probe_hash_rows.get(&join_key) { - for (row_id, probe_row) in matching_probe_rows { - match simple_join_filter_matches( - self, - post_join_filter, - join_eval_dataset, - if source_is_left { - Some(source_values) - } else { - Some(probe_row.as_slice()) - }, - left_width, - if source_is_left { - Some(probe_row.as_slice()) - } else { - Some(source_values) - }, - right_width, - params, - ) { - Ok(true) => {} - Ok(false) => continue, - Err(error) => { - row_error = Some(error); - break; - } - } - match project_simple_join_row( - self, - projection_plan, - join_eval_dataset, - if source_is_left { - Some(source_values) - } else { - Some(probe_row.as_slice()) - }, - left_width, - if source_is_left { - Some(probe_row.as_slice()) - } else { - Some(source_values) - }, - right_width, - params, - ) { - Ok(projected) => { - if let Some(matched_probe_row_ids) = matched_probe_row_ids.as_mut() { - matched_probe_row_ids.insert(*row_id, ()); - } - rows.push(projected); - } - Err(error) => { - row_error = Some(error); - break; - } - } - if early_stop_limit.is_some_and(|limit| rows.len() >= limit) { - should_stop = true; - break; - } - } - } - if let Some(error) = row_error { - return Err(error); - } - if !should_stop && !matches!(kind, JoinKind::Inner) && rows.len() == rows_before { - if !simple_join_filter_matches( - self, - post_join_filter, - join_eval_dataset, - if source_is_left { - Some(source_values) - } else { - None - }, - left_width, - if source_is_left { - None - } else { - Some(source_values) - }, - right_width, - params, - )? { - return Ok(false); - } - rows.push(project_simple_join_row( - self, - projection_plan, - join_eval_dataset, - if source_is_left { - Some(source_values) - } else { - None - }, - left_width, - if source_is_left { - None - } else { - Some(source_values) - }, - right_width, - params, - )?); - should_stop = early_stop_limit.is_some_and(|limit| rows.len() >= limit); - } - return Ok(should_stop); - } - - probe_row_ids.for_each(|row_id| { - if should_stop || row_error.is_some() { - return; - } - let probe_row = if let Some(positions) = probe_row_positions { - let Some(probe_position) = positions.get(&row_id).copied() else { - return; - }; - match probe_source.row_at_position(probe_position) { - Ok(Some(probe_row)) => probe_row.values().to_vec(), - Ok(None) => return, - Err(error) => { - row_error = Some(error); - return; - } - } - } else { - match probe_source.row_by_id(row_id) { - Ok(Some(probe_row)) => probe_row.values().to_vec(), - Ok(None) => return, - Err(error) => { - row_error = Some(error); - return; - } - } - }; - - match simple_join_filter_matches( - self, - post_join_filter, - join_eval_dataset, - if source_is_left { - Some(source_values) - } else { - Some(&probe_row) - }, - left_width, - if source_is_left { - Some(&probe_row) - } else { - Some(source_values) - }, - right_width, - params, - ) { - Ok(true) => {} - Ok(false) => return, - Err(error) => { - row_error = Some(error); - return; - } - } - match project_simple_join_row( - self, - projection_plan, - join_eval_dataset, - if source_is_left { - Some(source_values) - } else { - Some(&probe_row) - }, - left_width, - if source_is_left { - Some(&probe_row) - } else { - Some(source_values) - }, - right_width, - params, - ) { - Ok(projected) => { - if let Some(matched_probe_row_ids) = matched_probe_row_ids.as_mut() { - matched_probe_row_ids.insert(row_id, ()); - } - rows.push(projected) - } - Err(error) => { - row_error = Some(error); - return; - } - } - if early_stop_limit.is_some_and(|limit| rows.len() >= limit) { - should_stop = true; - } - }); - if let Some(error) = row_error { - return Err(error); - } - if !should_stop && !matches!(kind, JoinKind::Inner) && rows.len() == rows_before { - if !simple_join_filter_matches( - self, - post_join_filter, - join_eval_dataset, - if source_is_left { - Some(source_values) - } else { - None - }, - left_width, - if source_is_left { - None - } else { - Some(source_values) - }, - right_width, - params, - )? { - return Ok(false); - } - rows.push(project_simple_join_row( - self, - projection_plan, - join_eval_dataset, - if source_is_left { - Some(source_values) - } else { - None - }, - left_width, - if source_is_left { - None - } else { - Some(source_values) - }, - right_width, - params, - )?); - should_stop = early_stop_limit.is_some_and(|limit| rows.len() >= limit); - } - Ok(should_stop) - } - - fn try_execute_benchmark_history_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - if !query.ctes.is_empty() - || query.limit.is_some() - || query.offset.is_some() - || query.order_by.len() != 1 - { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if !select.group_by.is_empty() - || select.having.is_some() - || select.distinct - || !select.distinct_on.is_empty() - || select.from.len() != 1 - || select.projection.len() != 6 - { - return Ok(None); - } - - let Some(filter) = select.filter.as_ref() else { - return Ok(None); - }; - let Some((filter_table, filter_column, value_expr)) = simple_btree_lookup(filter) else { - return Ok(None); - }; - - let FromItem::Join { - left: order_payment_items, - right: item_item, - kind: JoinKind::Inner, - constraint: JoinConstraint::On(item_join_on), - } = &select.from[0] - else { - return Ok(None); - }; - let FromItem::Table { - name: item_name, - alias: item_alias, - } = &**item_item - else { - return Ok(None); - }; - let FromItem::Join { - left: order_payment, - right: order_item_item, - kind: JoinKind::Inner, - constraint: JoinConstraint::On(order_item_join_on), - } = &**order_payment_items - else { - return Ok(None); - }; - let FromItem::Table { - name: order_item_name, - alias: order_item_alias, - } = &**order_item_item - else { - return Ok(None); - }; - let FromItem::Join { - left: order_item, - right: payment_item, - kind: JoinKind::Inner, - constraint: JoinConstraint::On(payment_join_on), - } = &**order_payment - else { - return Ok(None); - }; - let FromItem::Table { - name: order_name, - alias: order_alias, - } = &**order_item - else { - return Ok(None); - }; - let FromItem::Table { - name: payment_name, - alias: payment_alias, - } = &**payment_item - else { - return Ok(None); - }; - - let order_binding = TableBindingRef { - name: order_name, - alias: order_alias, - }; - let payment_binding = TableBindingRef { - name: payment_name, - alias: payment_alias, - }; - let order_item_binding = TableBindingRef { - name: order_item_name, - alias: order_item_alias, - }; - let item_binding = TableBindingRef { - name: item_name, - alias: item_alias, - }; - - if self - .visible_view(order_name, NameResolutionScope::Session) - .is_some() - || self - .visible_view(payment_name, NameResolutionScope::Session) - .is_some() - || self - .visible_view(order_item_name, NameResolutionScope::Session) - .is_some() - || self - .visible_view(item_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(order_name) - || self.visible_table_is_temporary(payment_name) - || self.visible_table_is_temporary(order_item_name) - || self.visible_table_is_temporary(item_name) - { - return Ok(None); - } - - if !matches_filter_binding(order_name, order_alias, filter_table) - || !identifiers_equal(filter_column, "user_id") - { - return Ok(None); - } - if !join_constraint_matches_columns( - payment_join_on, - order_binding, - "id", - payment_binding, - "order_id", - ) || !join_constraint_matches_columns( - order_item_join_on, - order_binding, - "id", - order_item_binding, - "order_id", - ) || !join_constraint_matches_columns( - item_join_on, - order_item_binding, - "item_id", - item_binding, - "id", - ) { - return Ok(None); - } - - if !query.order_by[0].descending - || !expr_matches_binding_column(&query.order_by[0].expr, order_binding, "id") - { - return Ok(None); - } - - let projection = [ - (0, order_binding, "id"), - (1, order_binding, "total_amount"), - (2, payment_binding, "status"), - (3, item_binding, "name"), - (4, order_item_binding, "quantity"), - (5, order_item_binding, "price"), - ]; - for (index, binding, column) in projection { - let SelectItem::Expr { expr, .. } = &select.projection[index] else { - return Ok(None); - }; - if !expr_matches_binding_column(expr, binding, column) { - return Ok(None); - } - } - - let order_schema = match self.table_schema(order_name) { - Some(table) => table, - None => return Ok(None), - }; - let payment_schema = match self.table_schema(payment_name) { - Some(table) => table, - None => return Ok(None), - }; - let order_item_schema = match self.table_schema(order_item_name) { - Some(table) => table, - None => return Ok(None), - }; - let item_schema = match self.table_schema(item_name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(order_schema) - || !generated_columns_are_stored(payment_schema) - || !generated_columns_are_stored(order_item_schema) - || !generated_columns_are_stored(item_schema) - { - return Ok(None); - } - - let Some(order_source) = self.visible_table_row_source(order_name) else { - return Ok(None); - }; - let Some(payment_source) = self.visible_table_row_source(payment_name) else { - return Ok(None); - }; - let Some(order_item_source) = self.visible_table_row_source(order_item_name) else { - return Ok(None); - }; - let Some(item_source) = self.visible_table_row_source(item_name) else { - return Ok(None); - }; - - let Some(order_user_keys) = self.single_column_btree_keys(order_name, "user_id") else { - return Ok(None); - }; - let Some(payment_order_keys) = self.single_column_btree_keys(payment_name, "order_id") - else { - return Ok(None); - }; - let Some(order_item_order_keys) = - self.single_column_btree_keys(order_item_name, "order_id") - else { - return Ok(None); - }; - let Some(item_id_keys) = self.single_column_btree_keys(item_name, "id") else { - return Ok(None); - }; - - let Some(order_id_index) = schema_column_index(order_schema, "id") else { - return Ok(None); - }; - let Some(order_total_amount_index) = schema_column_index(order_schema, "total_amount") - else { - return Ok(None); - }; - let Some(payment_status_index) = schema_column_index(payment_schema, "status") else { - return Ok(None); - }; - let Some(order_item_item_id_index) = schema_column_index(order_item_schema, "item_id") - else { - return Ok(None); - }; - let Some(order_item_quantity_index) = schema_column_index(order_item_schema, "quantity") - else { - return Ok(None); - }; - let Some(order_item_price_index) = schema_column_index(order_item_schema, "price") else { - return Ok(None); - }; - let Some(item_name_index) = schema_column_index(item_schema, "name") else { - return Ok(None); - }; - - if order_schema.columns[order_id_index].column_type != crate::catalog::ColumnType::Int64 - || order_item_schema.columns[order_item_item_id_index].column_type - != crate::catalog::ColumnType::Int64 - || order_item_schema.columns[order_item_quantity_index].column_type - != crate::catalog::ColumnType::Int64 - || payment_schema.columns[payment_status_index].column_type - != crate::catalog::ColumnType::Text - || item_schema.columns[item_name_index].column_type != crate::catalog::ColumnType::Text - { - return Ok(None); - } - - let filter_value = self.eval_expr( - value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?; - let mut matching_orders = Vec::new(); - for order_row_id in order_user_keys.row_ids_for_value(&filter_value)? { - let Some(order_row) = order_source.row_by_id(order_row_id)? else { - continue; - }; - let Some(order_id) = order_row - .values() - .get(order_id_index) - .and_then(value_as_int64) - else { - return Ok(None); - }; - matching_orders.push(( - order_row_id, - order_id, - order_row.values()[order_total_amount_index].clone(), - )); - } - matching_orders.sort_by(|(_, left_id, _), (_, right_id, _)| right_id.cmp(left_id)); - - let mut column_names = Vec::with_capacity(select.projection.len()); - for (index, item) in select.projection.iter().enumerate() { - match item { - SelectItem::Expr { expr, alias } => { - column_names.push( - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - ); - } - SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => { - return Err(DbError::internal( - "internal: history fast path expects explicit projection expressions", - )); - } - } - } - - let mut rows = Vec::new(); - for (_order_row_id, order_id, order_total_amount) in matching_orders { - let order_id_value = Value::Int64(order_id); - let payment_row_ids = payment_order_keys.row_ids_for_value(&order_id_value)?; - if payment_row_ids.is_empty() { - continue; - } - let order_item_row_ids = order_item_order_keys.row_ids_for_value(&order_id_value)?; - if order_item_row_ids.is_empty() { - continue; - } - - for payment_row_id in payment_row_ids { - let Some(payment_row) = payment_source.row_by_id(payment_row_id)? else { - continue; - }; - let Some(payment_status) = payment_row.values().get(payment_status_index) else { - return Ok(None); - }; - - for order_item_row_id in &order_item_row_ids { - let Some(order_item_row) = order_item_source.row_by_id(*order_item_row_id)? - else { - continue; - }; - let Some(item_id_value) = order_item_row.values().get(order_item_item_id_index) - else { - return Ok(None); - }; - let item_row_ids = item_id_keys.row_ids_for_value(item_id_value)?; - if item_row_ids.is_empty() { - continue; - } - for item_row_id in item_row_ids { - let Some(item_row) = item_source.row_by_id(item_row_id)? else { - continue; - }; - let Some(item_name_value) = item_row.values().get(item_name_index) else { - return Ok(None); - }; - let Some(quantity_value) = - order_item_row.values().get(order_item_quantity_index) - else { - return Ok(None); - }; - let Some(price_value) = order_item_row.values().get(order_item_price_index) - else { - return Ok(None); - }; - - rows.push(QueryRow::new(vec![ - Value::Int64(order_id), - order_total_amount.clone(), - payment_status.clone(), - item_name_value.clone(), - quantity_value.clone(), - price_value.clone(), - ])); - } - } - } - } - - Ok(Some(QueryResult::with_rows(column_names, rows))) - } - - fn try_execute_benchmark_report_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - if !query.ctes.is_empty() - || query.offset.is_some() - || query.order_by.len() != 1 - || query.limit.is_none() - { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.from.len() != 1 - || select.filter.is_none() - || select.having.is_some() - || select.distinct - || !select.distinct_on.is_empty() - || select.group_by.len() != 2 - || select.projection.len() != 3 - { - return Ok(None); - } - - let Some(filter) = select.filter.as_ref() else { - return Ok(None); - }; - let Some((filter_table, filter_column, value_expr)) = simple_btree_lookup(filter) else { - return Ok(None); - }; - - let FromItem::Join { - left: item_order_items, - right: order_item, - kind: JoinKind::Inner, - constraint: JoinConstraint::On(order_join_on), - } = &select.from[0] - else { - return Ok(None); - }; - let FromItem::Table { - name: order_name, - alias: order_alias, - } = &**order_item - else { - return Ok(None); - }; - let FromItem::Join { - left: item_item, - right: order_item_item, - kind: JoinKind::Inner, - constraint: JoinConstraint::On(order_item_join_on), - } = &**item_order_items - else { - return Ok(None); - }; - let FromItem::Table { - name: item_name, - alias: item_alias, - } = &**item_item - else { - return Ok(None); - }; - let FromItem::Table { - name: order_item_name, - alias: order_item_alias, - } = &**order_item_item - else { - return Ok(None); - }; - - let item_binding = TableBindingRef { - name: item_name, - alias: item_alias, - }; - let order_item_binding = TableBindingRef { - name: order_item_name, - alias: order_item_alias, - }; - let order_binding = TableBindingRef { - name: order_name, - alias: order_alias, - }; - - if self - .visible_view(item_name, NameResolutionScope::Session) - .is_some() - || self - .visible_view(order_item_name, NameResolutionScope::Session) - .is_some() - || self - .visible_view(order_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(item_name) - || self.visible_table_is_temporary(order_item_name) - || self.visible_table_is_temporary(order_name) - { - return Ok(None); - } - - if !matches_filter_binding(order_name, order_alias, filter_table) - || !identifiers_equal(filter_column, "status") - || !join_constraint_matches_columns( - order_item_join_on, - item_binding, - "id", - order_item_binding, - "item_id", - ) - || !join_constraint_matches_columns( - order_join_on, - order_item_binding, - "order_id", - order_binding, - "id", - ) - { - return Ok(None); - } - - let SelectItem::Expr { - expr: item_name_expr, - alias: item_name_alias, - } = &select.projection[0] - else { - return Ok(None); - }; - if !expr_matches_binding_column(item_name_expr, item_binding, "name") { - return Ok(None); - } - - let SelectItem::Expr { - expr: quantity_sum_expr, - alias: quantity_sum_alias, - } = &select.projection[1] - else { - return Ok(None); - }; - if !aggregate_matches_single_binding_column( - quantity_sum_expr, - "sum", - order_item_binding, - "quantity", - ) { - return Ok(None); - } - - let SelectItem::Expr { - expr: revenue_expr, - alias: revenue_alias, - } = &select.projection[2] - else { - return Ok(None); - }; - if !aggregate_matches_binding_product( - revenue_expr, - "sum", - order_item_binding, - "quantity", - order_item_binding, - "price", - ) { - return Ok(None); - } - if !order_by_matches_alias_or_projection( - &query.order_by[0], - revenue_alias.as_deref(), - revenue_expr, - true, - ) { - return Ok(None); - } - - if select.group_by.len() != 2 - || !expr_matches_binding_column(&select.group_by[0], item_binding, "id") - || !expr_matches_binding_column(&select.group_by[1], item_binding, "name") - { - return Ok(None); - } - - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(usize::MAX); - - let item_schema = match self.table_schema(item_name) { - Some(table) => table, - None => return Ok(None), - }; - let order_item_schema = match self.table_schema(order_item_name) { - Some(table) => table, - None => return Ok(None), - }; - let order_schema = match self.table_schema(order_name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(item_schema) - || !generated_columns_are_stored(order_item_schema) - || !generated_columns_are_stored(order_schema) - { - return Ok(None); - } - - let Some(item_source) = self.visible_table_row_source(item_name) else { - return Ok(None); - }; - let Some(order_item_source) = self.visible_table_row_source(order_item_name) else { - return Ok(None); - }; - let Some(order_source) = self.visible_table_row_source(order_name) else { - return Ok(None); - }; - - let Some(order_status_keys) = self.single_column_btree_keys(order_name, "status") else { - return Ok(None); - }; - let Some(order_item_order_keys) = - self.single_column_btree_keys(order_item_name, "order_id") - else { - return Ok(None); - }; - let Some(item_id_keys) = self.single_column_btree_keys(item_name, "id") else { - return Ok(None); - }; - - let Some(order_id_index) = schema_column_index(order_schema, "id") else { - return Ok(None); - }; - let Some(order_item_item_id_index) = schema_column_index(order_item_schema, "item_id") - else { - return Ok(None); - }; - let Some(order_item_quantity_index) = schema_column_index(order_item_schema, "quantity") - else { - return Ok(None); - }; - let Some(order_item_price_index) = schema_column_index(order_item_schema, "price") else { - return Ok(None); - }; - let Some(item_name_index) = schema_column_index(item_schema, "name") else { - return Ok(None); - }; - - if order_schema.columns[order_id_index].column_type != crate::catalog::ColumnType::Int64 - || order_item_schema.columns[order_item_item_id_index].column_type - != crate::catalog::ColumnType::Int64 - || order_item_schema.columns[order_item_quantity_index].column_type - != crate::catalog::ColumnType::Int64 - || item_schema.columns[item_name_index].column_type != crate::catalog::ColumnType::Text - { - return Ok(None); - } - - let filter_value = self.eval_expr( - value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?; - let matching_order_row_ids = order_status_keys.row_ids_for_value(&filter_value)?; - let mut aggregates = BTreeMap::::new(); - for order_row_id in matching_order_row_ids { - let Some(order_row) = order_source.row_by_id(order_row_id)? else { - continue; - }; - let Some(order_id) = order_row - .values() - .get(order_id_index) - .and_then(value_as_int64) - else { - return Ok(None); - }; - let order_id_value = Value::Int64(order_id); - let order_item_row_ids = order_item_order_keys.row_ids_for_value(&order_id_value)?; - for order_item_row_id in order_item_row_ids { - let Some(order_item_row) = order_item_source.row_by_id(order_item_row_id)? else { - continue; - }; - let Some(item_id) = order_item_row - .values() - .get(order_item_item_id_index) - .and_then(value_as_int64) - else { - return Ok(None); - }; - let Some(quantity) = order_item_row - .values() - .get(order_item_quantity_index) - .and_then(value_as_int64) - else { - return Ok(None); - }; - let Some(price) = order_item_row - .values() - .get(order_item_price_index) - .and_then(value_as_f64) - else { - return Ok(None); - }; - - let item_row_ids = item_id_keys.row_ids_for_value(&Value::Int64(item_id))?; - if item_row_ids.is_empty() { - continue; - } - for item_row_id in item_row_ids { - let Some(item_row) = item_source.row_by_id(item_row_id)? else { - continue; - }; - let Some(item_name_value) = item_row.values().get(item_name_index) else { - return Ok(None); - }; - let Some(item_name_text) = value_as_text(item_name_value) else { - return Ok(None); - }; - let aggregate = aggregates.entry(item_id).or_insert_with(|| { - BenchmarkReportAggregate::new(item_name_text.to_string()) - }); - aggregate.quantity_total += quantity; - aggregate.revenue_total += quantity as f64 * price; - } - } - } - - let column_names = vec![ - item_name_alias - .clone() - .unwrap_or_else(|| infer_expr_name(item_name_expr, 1)), - quantity_sum_alias - .clone() - .unwrap_or_else(|| infer_expr_name(quantity_sum_expr, 2)), - revenue_alias - .clone() - .unwrap_or_else(|| infer_expr_name(revenue_expr, 3)), - ]; - - let mut rows = aggregates - .into_values() - .map(|aggregate| { - QueryRow::new(vec![ - Value::Text(aggregate.item_name), - Value::Int64(aggregate.quantity_total), - Value::Float64(aggregate.revenue_total), - ]) - }) - .collect::>(); - rows.sort_by(|left, right| { - let revenue_ordering = compare_values(&left.values()[2], &right.values()[2]) - .unwrap_or(std::cmp::Ordering::Equal) - .reverse(); - if revenue_ordering != std::cmp::Ordering::Equal { - return revenue_ordering; - } - compare_values(&left.values()[0], &right.values()[0]) - .unwrap_or(std::cmp::Ordering::Equal) - }); - if rows.len() > limit { - rows.truncate(limit); - } - Ok(Some(QueryResult::with_rows(column_names, rows))) - } - - fn single_column_btree_keys( - &self, - table_name: &str, - column_name: &str, - ) -> Option<&RuntimeBtreeKeys> { - let index = self.catalog.indexes.values().find(|index| { - identifiers_equal(&index.table_name, table_name) - && index.fresh - && index.kind == IndexKind::Btree - && index.predicate_sql.is_none() - && index.columns.len() == 1 - && index.columns[0] - .column_name - .as_deref() - .is_some_and(|index_column| identifiers_equal(index_column, column_name)) - && index.columns[0].expression_sql.is_none() - })?; - let RuntimeIndex::Btree { keys, .. } = self.index(&index.name)? else { - return None; - }; - Some(keys) - } - - fn execute_analyze(&mut self, table_name: Option<&str>) -> Result<()> { - let target_tables = if let Some(table_name) = table_name { - if self.visible_table_is_temporary(table_name) { - return Err(DbError::sql( - "ANALYZE is not supported for temporary tables", - )); - } - if self - .visible_view(table_name, NameResolutionScope::Session) - .is_some() - && self.catalog.table(table_name).is_none() - { - return Err(DbError::sql(format!("unknown table {table_name}"))); - } - let table = self - .catalog - .table(table_name) - .ok_or_else(|| DbError::sql(format!("unknown table {table_name}")))?; - vec![table.name.clone()] - } else { - self.catalog.tables.keys().cloned().collect::>() - }; - for table_name in target_tables { - self.refresh_table_stats(&table_name)?; - } - Ok(()) - } - - fn refresh_table_stats(&mut self, table_name: &str) -> Result<()> { - let row_count = self - .table_row_source(table_name) - .map(TableRowSource::row_count) - .or_else(|| self.table_data(table_name).map(|table| table.rows.len())) - .map(i64::try_from) - .transpose() - .map_err(|_| { - DbError::sql(format!( - "table {table_name} exceeds ANALYZE row-count limits" - )) - })? - .unwrap_or(0); - self.catalog_mut() - .table_stats - .insert(table_name.to_string(), TableStats { row_count }); - - let index_names = self - .catalog - .indexes - .values() - .filter(|index| identifiers_equal(&index.table_name, table_name)) - .map(|index| index.name.clone()) - .collect::>(); - for index_name in index_names { - self.catalog_mut().index_stats.remove(&index_name); - let Some(index) = self.catalog.index(&index_name).cloned() else { - continue; - }; - let (entry_count, distinct_key_count) = match self.index(&index.name) { - Some(RuntimeIndex::Btree { keys, .. }) => { - let entry_count = i64::try_from(keys.total_row_id_count()).map_err(|_| { - DbError::sql(format!( - "index {} exceeds ANALYZE entry-count limits", - index.name - )) - })?; - let distinct_key_count = - i64::try_from(keys.distinct_key_count()).map_err(|_| { - DbError::sql(format!( - "index {} exceeds ANALYZE distinct-count limits", - index.name - )) - })?; - (entry_count, distinct_key_count) - } - Some(RuntimeIndex::Spatial { index: spatial }) => { - let entry_count = i64::try_from(spatial.len()).map_err(|_| { - DbError::sql(format!( - "index {} exceeds ANALYZE entry-count limits", - index.name - )) - })?; - (entry_count, entry_count) - } - Some(RuntimeIndex::Trigram { .. }) | None => continue, - Some(RuntimeIndex::FullText { index: fulltext }) => { - let entry_count = i64::try_from(fulltext.entry_count()).map_err(|_| { - DbError::sql(format!( - "index {} exceeds ANALYZE entry-count limits", - index.name - )) - })?; - let distinct_key_count = - i64::try_from(fulltext.term_count()).map_err(|_| { - DbError::sql(format!( - "index {} exceeds ANALYZE distinct-count limits", - index.name - )) - })?; - (entry_count, distinct_key_count) - } - }; - self.catalog_mut().index_stats.insert( - index.name.clone(), - IndexStats { - entry_count, - distinct_key_count, - }, - ); - } - Ok(()) - } - - fn try_execute_simple_table_projection_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - if !query.ctes.is_empty() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.filter.is_some() - || !select.group_by.is_empty() - || select.having.is_some() - || select.distinct - || select.from.len() != 1 - { - return Ok(None); - } - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - { - return Ok(None); - } - - let table_schema = match self.table_schema(name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - let Some((projection_indexes, column_names)) = - self.simple_projection_plan(select, name, alias, table_schema) - else { - return Ok(None); - }; - let order_by = self.simple_projection_order_by_plan( - query, - table_schema, - name, - alias.as_deref().unwrap_or(name), - &projection_indexes, - )?; - let row_id_order = if query.order_by.len() == 1 { - if let Expr::Column { - table: order_table, - column: order_column, - } = &query.order_by[0].expr - { - if order_table.as_deref().is_some_and(|qualifier| { - !matches_table_binding(TableBindingRef { name, alias }, Some(qualifier)) - }) { - None - } else if let Some(filter_column_index) = - schema_column_index(table_schema, order_column) - { - if table_schema - .primary_key_columns - .iter() - .any(|column| identifiers_equal(column, order_column)) - && table_schema.columns[filter_column_index].column_type - == crate::catalog::ColumnType::Int64 - { - Some((order_column.as_str(), query.order_by[0].descending)) - } else { - None - } - } else { - None - } - } else { - None - } - } else { - None - }; - - if !query.order_by.is_empty() && order_by.is_none() && row_id_order.is_none() { - return Ok(None); - } - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let _row_source = self.visible_table_row_source(name); - let Some(row_source) = _row_source else { - return Ok(None); - }; - if let Some((filter_column, descending)) = row_id_order { - if limit != Some(0) { - if let Some(row_ids) = self.ordered_runtime_btree_row_ids( - name, - filter_column, - limit, - offset, - descending, - )? { - let mut rows = Vec::with_capacity(row_ids.len().min(64)); - for row_id in row_ids { - if let Some(row) = - row_source.projected_query_row_by_id(row_id, &projection_indexes)? - { - rows.push(row); - } - } - return Ok(Some(QueryResult::with_rows(column_names, rows))); - } - let mut ordered_row_ids = Vec::with_capacity(row_source.row_count()); - for stored_row in row_source.rows() { - ordered_row_ids.push(stored_row?.row_id()); - } - ordered_row_ids.sort_unstable(); - if descending { - ordered_row_ids.reverse(); - } - let take = limit.unwrap_or(usize::MAX); - let mut rows = Vec::with_capacity(take.min(ordered_row_ids.len())); - for row_id in ordered_row_ids.into_iter().skip(offset).take(take) { - if let Some(row) = - row_source.projected_query_row_by_id(row_id, &projection_indexes)? - { - rows.push(row); - } - } - return Ok(Some(QueryResult::with_rows(column_names, rows))); - } - } - - Ok(Some(self.simple_projection_result_from_source( - row_source, - &projection_indexes, - column_names, - order_by, - limit, - offset, - )?)) - } - - fn try_execute_simple_filtered_projection_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - if !query.ctes.is_empty() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if !select.group_by.is_empty() - || select.having.is_some() - || !select.distinct_on.is_empty() - || select.from.len() != 1 - { - return Ok(None); - } - if select_requires_grouped_evaluation(self, select)? { - return Ok(None); - } - if select.distinct - && (!query.order_by.is_empty() || query.limit.is_some() || query.offset.is_some()) - { - return Ok(None); - } - let Some(filter) = select.filter.as_ref() else { - return Ok(None); - }; - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - { - return Ok(None); - } - - let table_schema = match self.table_schema(name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - let Some((projection_indexes, column_names)) = - self.simple_projection_plan(select, name, alias, table_schema) - else { - return Ok(None); - }; - let binding_name = alias.as_deref().unwrap_or(name); - let order_by = self.simple_projection_order_by_plan( - query, - table_schema, - name, - binding_name, - &projection_indexes, - )?; - let order_by_requires_rowid_range = !query.order_by.is_empty() && order_by.is_none(); - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let _row_source = self.visible_table_row_source(name); - if !select.distinct { - if let Some(row_source) = _row_source { - if !order_by_requires_rowid_range { - if let Some(result) = self.try_simple_filtered_projection_exact_index_result( - row_source, - name, - table_schema, - filter, - &projection_indexes, - column_names.clone(), - order_by.as_deref(), - params, - limit, - offset, - )? { - return Ok(Some(result)); - } - } - } - } - let Some(row_source) = _row_source else { - return Ok(None); - }; - - if !select.distinct - && query.order_by.is_empty() - && limit.is_none() - && offset == 0 - && residual_like_filter_can_use_direct_scan(filter) - { - if let Some((filter_table, filter_column, literal)) = - simple_contains_like_projection_filter(filter) - { - if filter_table.is_none_or(|table_name| { - identifiers_equal(table_name, name) - || identifiers_equal(table_name, binding_name) - }) { - let filter_column_index = table_schema - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, filter_column)) - .ok_or_else(|| { - DbError::internal(format!( - "simple LIKE projection column {filter_column} missing from {name}" - )) - })?; - return Ok(Some( - self.simple_contains_like_projection_result_from_source( - row_source, - filter_column_index, - literal, - &projection_indexes, - column_names, - )?, - )); - } - } - } - - let Some(range_filter) = simple_range_projection_filter(filter) else { - return Ok(None); - }; - let filter_table = range_filter.table; - let filter_column = range_filter.column; - let lower_bound = range_filter.lower; - let upper_bound = range_filter.upper; - if let Some(table_name) = filter_table { - if !identifiers_equal(table_name, name) && !identifiers_equal(table_name, binding_name) - { - return Ok(None); - } - } - let filter_column_index = table_schema - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, filter_column)) - .ok_or_else(|| { - DbError::internal(format!( - "simple filtered projection column {filter_column} missing from {name}" - )) - })?; - - let lower_bound = lower_bound - .map(|bound| { - Ok(SimpleRangeBoundValue { - inclusive: bound.inclusive, - value: self.eval_expr( - bound.value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?, - }) - }) - .transpose()?; - let upper_bound = upper_bound - .map(|bound| { - Ok(SimpleRangeBoundValue { - inclusive: bound.inclusive, - value: self.eval_expr( - bound.value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?, - }) - }) - .transpose()?; - - let residual_plans = self.build_simple_residual_plans( - table_schema, - name, - binding_name, - &range_filter.residual, - params, - )?; - // If a residual predicate references an unknown/external table the - // builder silently stops; bail to the generic executor in that case. - if residual_plans.len() != range_filter.residual.len() { - return Ok(None); - } - if !select.distinct && residual_plans.is_empty() { - if let Some(result) = self.try_simple_rowid_range_projection_result( - row_source, - table_schema, - TableBindingRef { name, alias }, - filter_column, - lower_bound.as_ref(), - upper_bound.as_ref(), - &projection_indexes, - column_names.clone(), - &query.order_by, - limit, - offset, - )? { - return Ok(Some(result)); - } - } - if order_by_requires_rowid_range { - return Ok(None); - } - if order_by.is_none() { - if let Some(result) = self.try_simple_filtered_projection_range_index_result( - row_source, - name, - table_schema, - filter_column_index, - filter_column, - lower_bound.as_ref(), - upper_bound.as_ref(), - &residual_plans, - &projection_indexes, - column_names.clone(), - limit, - offset, - )? { - return Ok(Some(result)); - } - } - if let Some(result) = self.try_simple_filtered_projection_ordered_index_result( - row_source, - name, - table_schema, - filter_column_index, - lower_bound.as_ref(), - upper_bound.as_ref(), - &residual_plans, - &projection_indexes, - column_names.clone(), - order_by.as_deref(), - limit, - offset, - )? { - return Ok(Some(result)); - } - Ok(Some(self.simple_filtered_projection_result_from_source( - row_source, - filter_column_index, - lower_bound.as_ref(), - upper_bound.as_ref(), - &residual_plans, - &projection_indexes, - column_names, - order_by, - limit, - offset, - )?)) - } - - #[allow(clippy::too_many_arguments)] - fn try_simple_rowid_range_projection_result( - &self, - row_source: VisibleTableRowSource<'_>, - table_schema: &TableSchema, - table_binding: TableBindingRef<'_>, - filter_column: &str, - lower_bound: Option<&SimpleRangeBoundValue>, - upper_bound: Option<&SimpleRangeBoundValue>, - projection_indexes: &[usize], - column_names: Vec, - order_by: &[crate::sql::ast::OrderBy], - limit: Option, - offset: usize, - ) -> Result> { - let lower_only_limited = lower_bound.is_some() && upper_bound.is_none() && limit.is_some(); - let bounded_range = lower_bound.is_some() && upper_bound.is_some(); - if !bounded_range && !lower_only_limited { - return Ok(None); - } - if !order_by.is_empty() { - if order_by.len() != 1 || order_by[0].descending { - return Ok(None); - } - let Expr::Column { - table: order_table, - column: order_column, - } = &order_by[0].expr - else { - return Ok(None); - }; - if !identifiers_equal(order_column, filter_column) - || order_table - .as_deref() - .is_some_and(|qualifier| !matches_table_binding(table_binding, Some(qualifier))) - { - return Ok(None); - } - } - if !table_schema - .primary_key_columns - .iter() - .any(|column| identifiers_equal(column, filter_column)) - { - return Ok(None); - } - let Some(filter_column_index) = schema_column_index(table_schema, filter_column) else { - return Ok(None); - }; - if table_schema.columns[filter_column_index].column_type - != crate::catalog::ColumnType::Int64 - { - return Ok(None); - } - let Some(start) = simple_int64_range_start(lower_bound) else { - return Ok(None); - }; - let Some(end_exclusive) = simple_int64_range_end_exclusive(upper_bound) else { - return Ok(None); - }; - if end_exclusive <= start || limit == Some(0) { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - } - - let take = limit.unwrap_or(usize::MAX); - if let Some(rows) = row_source.projected_query_rows_in_id_range( - start, - end_exclusive, - take, - offset, - projection_indexes, - ) { - return Ok(Some(QueryResult::with_rows(column_names, rows))); - } - let max_probe_steps = if upper_bound.is_none() { - Some( - row_source - .row_count() - .saturating_add(offset) - .saturating_add(take), - ) - } else { - None - }; - let mut skipped = 0usize; - let mut rows = Vec::with_capacity(take.min(64)); - let mut row_id = start; - let mut probe_steps = 0usize; - while row_id < end_exclusive && rows.len() < take { - if max_probe_steps.is_some_and(|max_probe_steps| probe_steps >= max_probe_steps) { - return Ok(None); - } - probe_steps = probe_steps.saturating_add(1); - if let Some(row) = row_source.projected_query_row_by_id(row_id, projection_indexes)? { - if skipped < offset { - skipped += 1; - } else { - rows.push(row); - } - } - let Some(next_row_id) = row_id.checked_add(1) else { - break; - }; - row_id = next_row_id; - } - Ok(Some(QueryResult::with_rows(column_names, rows))) - } - - #[allow(clippy::too_many_arguments)] - fn try_simple_deferred_rowid_range_projection_result( - &self, - store: &S, - state: PersistedTableState, - table_schema: &TableSchema, - table_binding: TableBindingRef<'_>, - filter_column: &str, - lower_bound: Option<&SimpleRangeBoundValue>, - upper_bound: Option<&SimpleRangeBoundValue>, - projection_indexes: &[usize], - column_names: Vec, - order_by: &[crate::sql::ast::OrderBy], - limit: Option, - offset: usize, - use_persistent_pk_index: bool, - paged_locator_cache: Option<&DeferredPagedRowLocatorCache>, - ) -> Result> { - let lower_only_limited = lower_bound.is_some() && upper_bound.is_none() && limit.is_some(); - let bounded_range = lower_bound.is_some() && upper_bound.is_some(); - if !bounded_range && !lower_only_limited { - return Ok(None); - } - if !order_by.is_empty() { - if order_by.len() != 1 || order_by[0].descending { - return Ok(None); - } - let Expr::Column { - table: order_table, - column: order_column, - } = &order_by[0].expr - else { - return Ok(None); - }; - if !identifiers_equal(order_column, filter_column) - || order_table - .as_deref() - .is_some_and(|qualifier| !matches_table_binding(table_binding, Some(qualifier))) - { - return Ok(None); - } - } - if !table_schema - .primary_key_columns - .iter() - .any(|column| identifiers_equal(column, filter_column)) - { - return Ok(None); - } - let Some(filter_column_index) = schema_column_index(table_schema, filter_column) else { - return Ok(None); - }; - if table_schema.columns[filter_column_index].column_type - != crate::catalog::ColumnType::Int64 - { - return Ok(None); - } - - let has_matching_locator_cache = - paged_locator_cache.is_some_and(|cache| cache.matches_state(state)); - let has_persistent_pk_locator = - use_persistent_pk_index && table_schema.pk_index_root.is_some(); - let has_compressed_lookup = state.pointer.is_compressed(); - if !has_matching_locator_cache && !has_persistent_pk_locator && !has_compressed_lookup { - return Ok(None); - } - - let Some(start) = simple_int64_range_start(lower_bound) else { - return Ok(None); - }; - let Some(end_exclusive) = simple_int64_range_end_exclusive(upper_bound) else { - return Ok(None); - }; - if end_exclusive <= start || limit == Some(0) { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - } - - let take = limit.unwrap_or(usize::MAX); - let max_probe_steps = if upper_bound.is_none() { - Some(state.row_count.saturating_add(offset).saturating_add(take)) - } else { - None - }; - let mut skipped = 0usize; - let mut rows = Vec::with_capacity(take.min(64)); - let mut row_id = start; - let mut probe_steps = 0usize; - while row_id < end_exclusive && rows.len() < take { - if max_probe_steps.is_some_and(|max_probe_steps| probe_steps >= max_probe_steps) { - return Ok(None); - } - probe_steps = probe_steps.saturating_add(1); - if let Some(values) = read_deferred_projected_values_by_id( - store, - state, - table_schema, - row_id, - use_persistent_pk_index, - paged_locator_cache, - projection_indexes, - )? { - if skipped < offset { - skipped += 1; - } else { - rows.push(QueryRow::new(values)); - } - } - let Some(next_row_id) = row_id.checked_add(1) else { - break; - }; - row_id = next_row_id; - } - Ok(Some(QueryResult::with_rows(column_names, rows))) - } - - fn try_execute_simple_distinct_projection_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - if !query.ctes.is_empty() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.filter.is_some() - || !select.group_by.is_empty() - || select.having.is_some() - || !select.distinct - || !select.distinct_on.is_empty() - || select.from.len() != 1 - { - return Ok(None); - } - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - { - return Ok(None); - } - - let table_schema = match self.table_schema(name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - let Some((projection_indexes, column_names)) = - self.simple_projection_plan(select, name, alias, table_schema) - else { - return Ok(None); - }; - let order_by = self.simple_projection_order_by_plan( - query, - table_schema, - name, - alias.as_deref().unwrap_or(name), - &projection_indexes, - )?; - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let _row_source = self.visible_table_row_source(name); - let Some(row_source) = _row_source else { - return Ok(None); - }; - Ok(Some(self.simple_distinct_projection_result_from_source( - row_source, - &projection_indexes, - column_names, - order_by, - limit, - offset, - )?)) - } - - fn try_execute_simple_distinct_filtered_projection_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - if !query.ctes.is_empty() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if !select.group_by.is_empty() - || select.having.is_some() - || !select.distinct - || !select.distinct_on.is_empty() - || select.from.len() != 1 - { - return Ok(None); - } - let Some(filter) = select.filter.as_ref() else { - return Ok(None); - }; - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - { - return Ok(None); - } - - let table_schema = match self.table_schema(name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - let Some((projection_indexes, column_names)) = - self.simple_projection_plan(select, name, alias, table_schema) - else { - return Ok(None); - }; - let binding_name = alias.as_deref().unwrap_or(name); - let Some(range_filter) = simple_range_projection_filter(filter) else { - return Ok(None); - }; - let filter_table = range_filter.table; - let filter_column = range_filter.column; - let lower_bound = range_filter.lower; - let upper_bound = range_filter.upper; - if let Some(table_name) = filter_table { - if !identifiers_equal(table_name, name) && !identifiers_equal(table_name, binding_name) - { - return Ok(None); - } - } - let filter_column_index = table_schema - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, filter_column)) - .ok_or_else(|| { - DbError::internal(format!( - "simple filtered distinct projection column {filter_column} missing from {name}" - )) - })?; - let lower_bound = lower_bound - .map(|bound| { - Ok(SimpleRangeBoundValue { - inclusive: bound.inclusive, - value: self.eval_expr( - bound.value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?, - }) - }) - .transpose()?; - let upper_bound = upper_bound - .map(|bound| { - Ok(SimpleRangeBoundValue { - inclusive: bound.inclusive, - value: self.eval_expr( - bound.value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?, - }) - }) - .transpose()?; - if !range_filter.residual.is_empty() { - // The distinct filtered fast path does not yet evaluate residual - // predicates; bail to the generic executor to preserve correctness. - return Ok(None); - } - let order_by = self.simple_projection_order_by_plan( - query, - table_schema, - name, - binding_name, - &projection_indexes, - )?; - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let _row_source = self.visible_table_row_source(name); - let Some(row_source) = _row_source else { - return Ok(None); - }; - Ok(Some( - self.simple_distinct_filtered_projection_result_from_source( - row_source, - filter_column_index, - lower_bound.as_ref(), - upper_bound.as_ref(), - &projection_indexes, - column_names, - order_by, - limit, - offset, - )?, - )) - } - - fn try_execute_simple_union_range_projection_query( - &self, - query: &Query, - params: &[Value], - ctes: &BTreeMap, - ) -> Result> { - if !query.ctes.is_empty() || query.order_by.len() != 1 { - return Ok(None); - } - - let QueryBody::SetOperation { - op: crate::sql::ast::SetOperation::Union, - all: false, - left, - right, - } = &query.body - else { - return Ok(None); - }; - - let analyze_side = |body: &QueryBody| -> Result> { - let QueryBody::Select(select) = body else { - return Ok(None); - }; - if select.filter.is_none() - || !select.group_by.is_empty() - || select.having.is_some() - || select.distinct - || !select.distinct_on.is_empty() - || select.from.len() != 1 - { - return Ok(None); - } - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if ctes.contains_key(name) - || self - .visible_view(name, NameResolutionScope::Session) - .is_some() - { - return Ok(None); - } - let Some(table_schema) = self.table_schema(name) else { - return Ok(None); - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - let Some((projection_indexes, column_names)) = - self.simple_projection_plan(select, name, alias, table_schema) - else { - return Ok(None); - }; - if projection_indexes.len() != 1 { - return Ok(None); - } - let Some(filter) = select.filter.as_ref() else { - return Ok(None); - }; - let Some(range_filter) = simple_range_projection_filter(filter) else { - return Ok(None); - }; - if !range_filter.residual.is_empty() { - return Ok(None); - } - let binding_name = alias.as_deref().unwrap_or(name); - if let Some(filter_table) = range_filter.table { - if !identifiers_equal(filter_table, name) - && !identifiers_equal(filter_table, binding_name) - { - return Ok(None); - } - } - let Some(filter_column_index) = table_schema - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, range_filter.column)) - else { - return Ok(None); - }; - if projection_indexes[0] != filter_column_index { - return Ok(None); - } - let lower_bound = range_filter - .lower - .map(|bound| { - Ok(SimpleRangeBoundValue { - inclusive: bound.inclusive, - value: self.eval_expr( - bound.value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?, - }) - }) - .transpose()?; - let upper_bound = range_filter - .upper - .map(|bound| { - Ok(SimpleRangeBoundValue { - inclusive: bound.inclusive, - value: self.eval_expr( - bound.value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?, - }) - }) - .transpose()?; - if !simple_range_bounds_match_column_type( - table_schema.columns[filter_column_index].column_type, - lower_bound.as_ref(), - upper_bound.as_ref(), - ) { - return Ok(None); - } - Ok(Some(SimpleUnionRangeProjectionSide { - table_name: name.clone(), - alias: alias.clone(), - projection_indexes, - column_names, - filter_column_index, - lower_bound, - upper_bound, - })) - }; - - let Some(left_side) = analyze_side(left)? else { - return Ok(None); - }; - let Some(right_side) = analyze_side(right)? else { - return Ok(None); - }; - if !identifiers_equal(&left_side.table_name, &right_side.table_name) - || left_side.projection_indexes != right_side.projection_indexes - || left_side.filter_column_index != right_side.filter_column_index - { - return Ok(None); - } - - let order_by = &query.order_by[0]; - if order_by.collation.is_some() || order_by.descending { - return Ok(None); - } - let Expr::Column { - table: order_table, - column: order_column, - } = &order_by.expr - else { - return Ok(None); - }; - if let Some(order_table) = order_table.as_deref() { - if !identifiers_equal(order_table, &left_side.table_name) - && !left_side - .alias - .as_deref() - .is_some_and(|alias| identifiers_equal(order_table, alias)) - { - return Ok(None); - } - } - if !identifiers_equal(order_column, &left_side.column_names[0]) { - return Ok(None); - } - - let Some(index) = - self.single_column_btree_index(&left_side.table_name, &left_side.column_names[0]) - else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { - return Ok(None); - }; - let Some(left_start) = simple_int64_range_start(left_side.lower_bound.as_ref()) else { - return Ok(None); - }; - let Some(left_end_exclusive) = - simple_int64_range_end_exclusive(left_side.upper_bound.as_ref()) - else { - return Ok(None); - }; - let Some(right_start) = simple_int64_range_start(right_side.lower_bound.as_ref()) else { - return Ok(None); - }; - let Some(right_end_exclusive) = - simple_int64_range_end_exclusive(right_side.upper_bound.as_ref()) - else { - return Ok(None); - }; - - let mut distinct_values = BTreeSet::new(); - let mut supported = true; - let mut collect_range = |range_start: i64, range_end_exclusive: i64| { - if range_start >= range_end_exclusive { - return; - } - match keys { - RuntimeBtreeKeys::UniqueInt64(entries, deleted) => { - for (value, row_id) in entries.iter() { - if deleted.contains(&row_id) { - continue; - } - if value >= range_start && value < range_end_exclusive { - distinct_values.insert(value); - } - } - } - RuntimeBtreeKeys::NonUniqueInt64(entries, deleted) => { - for (value, row_ids) in entries.iter() { - if row_ids.iter().any(|row_id| !deleted.contains(&row_id)) - && value >= range_start - && value < range_end_exclusive - { - distinct_values.insert(value); - } - } - } - _ => supported = false, - } - }; - collect_range(left_start, left_end_exclusive); - collect_range(right_start, right_end_exclusive); - if !supported { - return Ok(None); - } - - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let rows = distinct_values - .into_iter() - .skip(offset) - .take(limit.unwrap_or(usize::MAX)) - .map(|value| vec![Value::Int64(value)]) - .collect(); - let columns = left_side - .column_names - .into_iter() - .map(|name| ColumnBinding::visible(None, name)) - .collect(); - Ok(Some(Dataset::with_rows(columns, rows))) - } - - fn try_execute_simple_expression_projection_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - if !query.ctes.is_empty() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if !select.group_by.is_empty() - || select.having.is_some() - || !select.distinct_on.is_empty() - || select.from.len() != 1 - { - return Ok(None); - } - if select_requires_grouped_evaluation(self, select)? { - return Ok(None); - } - if select.distinct - && (!query.order_by.is_empty() || query.limit.is_some() || query.offset.is_some()) - { - return Ok(None); - } - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(name) - { - return Ok(None); - } - let table_schema = match self.table_schema(name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - if select - .projection - .iter() - .any(select_item_contains_window_or_subquery) - || select - .filter - .as_ref() - .is_some_and(expr_contains_recursive_unsupported_feature) - || query - .order_by - .iter() - .any(|order| expr_contains_recursive_unsupported_feature(&order.expr)) - { - return Ok(None); - } - if select - .projection - .iter() - .any(select_item_contains_fulltext_function) - || select - .filter - .as_ref() - .is_some_and(expr_contains_fulltext_function) - || query - .order_by - .iter() - .any(|order| expr_contains_fulltext_function(&order.expr)) - { - return Ok(None); - } - let has_expression_projection = select.projection.iter().any(|item| match item { - SelectItem::Expr { expr, .. } => !matches!(expr, Expr::Column { .. }), - SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => true, - }); - if !has_expression_projection - && select.filter.is_none() - && query.order_by.is_empty() - && query.limit.is_none() - && query.offset.is_none() - { - return Ok(None); - } - - let binding_name = alias.as_deref().unwrap_or(name); - let Some(projection_plan) = - simple_expression_projection_plan(table_schema, name, binding_name, &select.projection) - else { - return Ok(None); - }; - let ctes = BTreeMap::new(); - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &ctes)) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &ctes)) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let Some(row_source) = self.visible_table_row_source(name) else { - return Ok(None); - }; - if let Some(row_ids) = select - .filter - .as_ref() - .map(|filter| { - self.trigram_candidate_row_ids_for_filter(name, alias, filter, params, &ctes) - }) - .transpose()? - .flatten() - { - return Ok(Some( - self.simple_expression_projection_result_from_row_ids( - row_source, - table_schema, - binding_name, - &select.projection, - &projection_plan, - select.filter.as_ref(), - select.distinct, - &query.order_by, - params, - limit, - offset, - &row_ids, - )?, - )); - } - Ok(Some(self.simple_expression_projection_result_from_source( - row_source, - table_schema, - binding_name, - &select.projection, - &projection_plan, - select.filter.as_ref(), - select.distinct, - &query.order_by, - params, - limit, - offset, - )?)) - } - - fn simple_projection_result_from_source( - &self, - row_source: VisibleTableRowSource<'_>, - projection_indexes: &[usize], - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, - ) -> Result { - let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); - let mut rows = Vec::with_capacity( - bounded_row_count - .unwrap_or(row_source.row_count()) - .min(row_source.row_count()), - ); - if order_by.is_none() { - let mut skipped = 0usize; - for stored_row in row_source.rows() { - let stored_row = stored_row?; - if skipped < offset { - skipped = skipped.saturating_add(1); - continue; - } - if limit.is_some_and(|limit| rows.len() >= limit) { - break; - } - rows.push(project_simple_projection_values( - stored_row.values(), - projection_indexes, - )); - } - return Ok(QueryResult::with_rows(column_names, rows)); - } - - for stored_row in row_source.rows() { - let stored_row = stored_row?; - rows.push(project_simple_projection_values( - stored_row.values(), - projection_indexes, - )); - } - apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - column_names, - order_by.as_deref(), - limit, - offset, - ) - } - - fn simple_distinct_projection_result_from_source( - &self, - row_source: VisibleTableRowSource<'_>, - projection_indexes: &[usize], - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, - ) -> Result { - let mut rows = Vec::new(); - let mut seen = BTreeSet::new(); - for stored_row in row_source.rows() { - let stored_row = stored_row?; - let projected = - project_simple_projection_values(stored_row.values(), projection_indexes); - if seen.insert(row_identity(projected.values())?) { - rows.push(projected); - } - } - apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - column_names, - order_by.as_deref(), - limit, - offset, - ) - } - - #[allow(clippy::too_many_arguments)] - fn simple_projection_result_from_persisted_state( - &self, - store: &S, - state: PersistedTableState, - projection_indexes: &[usize], - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, - ) -> Result { - let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); - let mut rows = Vec::with_capacity( - bounded_row_count - .unwrap_or(state.row_count) - .min(state.row_count), - ); - if order_by.is_none() { - let mut skipped = 0usize; - visit_persisted_table_rows_until(store, state, |_, values| { - if skipped < offset { - skipped = skipped.saturating_add(1); - return Ok(false); - } - if limit.is_some_and(|limit| rows.len() >= limit) { - return Ok(true); - } - rows.push(project_simple_projection_values(values, projection_indexes)); - Ok(limit.is_some_and(|limit| rows.len() >= limit)) - })?; - return Ok(QueryResult::with_rows(column_names, rows)); - } - visit_persisted_table_rows(store, state, |_, values| { - rows.push(project_simple_projection_values(values, projection_indexes)); - Ok(()) - })?; - apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - column_names, - order_by.as_deref(), - limit, - offset, - ) - } - - #[allow(clippy::too_many_arguments)] - fn simple_distinct_projection_result_from_persisted_state( - &self, - store: &S, - state: PersistedTableState, - projection_indexes: &[usize], - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, - ) -> Result { - let mut rows = Vec::new(); - let mut seen = BTreeSet::new(); - visit_persisted_table_rows(store, state, |_, values| { - let projected = project_simple_projection_values(values, projection_indexes); - if seen.insert(row_identity(projected.values())?) { - rows.push(projected); - } - Ok(()) - })?; - apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - column_names, - order_by.as_deref(), - limit, - offset, - ) - } - - #[allow(clippy::too_many_arguments)] - fn simple_expression_projection_result_from_persisted_state( - &self, - store: &S, - state: PersistedTableState, - table_schema: &TableSchema, - binding_name: &str, - projection: &[SelectItem], - projection_plan: &SimpleExpressionProjectionPlan<'_>, - filter: Option<&Expr>, - distinct: bool, - order_by: &[crate::sql::ast::OrderBy], - params: &[Value], - limit: Option, - offset: usize, - ) -> Result { - let dataset = Dataset::with_rows( - table_bindings_with_hidden_row_id(table_schema, binding_name), - Vec::new(), - ); - let projection_order_by = projection_order_by_plan(order_by, projection); - let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); - let mut rows = Vec::with_capacity(bounded_row_count.unwrap_or(state.row_count)); - let mut seen = BTreeSet::new(); - visit_persisted_table_rows(store, state, |row_id, values| { - let mut eval_values = values.to_vec(); - eval_values.push(Value::Int64(row_id)); - if let Some(filter) = filter { - if !matches!( - self.eval_expr( - filter, - &dataset, - &eval_values, - params, - &BTreeMap::new(), - None, - )?, - Value::Bool(true) - ) { - return Ok(()); - } - } - let output = self.project_simple_expression_row( - projection_plan, - &dataset, - &eval_values, - params, - )?; - if distinct && !seen.insert(row_identity(&output)?) { - return Ok(()); - } - - let mut order_values = Vec::with_capacity(order_by.len()); - if let Some(order_by_plan) = projection_order_by.as_deref() { - for order in order_by_plan { - order_values.push(output[order.projection_index].clone()); - } - } else { - for order in order_by { - order_values.push(self.eval_expr( - &order.expr, - &dataset, - &eval_values, - params, - &BTreeMap::new(), - None, - )?); - } - } - let row = (QueryRow::new(output), order_values); - if let Some(bounded_row_count) = bounded_row_count { - if order_by.is_empty() { - if rows.len() < bounded_row_count { - rows.push(row); - } - } else { - push_bounded_ordered_query_row( - Some(self), - &mut rows, - row, - order_by, - bounded_row_count, - )?; - } - } else { - rows.push(row); - } - Ok(()) - })?; - - if !order_by.is_empty() { - sort_query_rows_by_order_values(Some(self), &mut rows, order_by)?; - } - - let rows = rows - .into_iter() - .skip(offset) - .take(limit.unwrap_or(usize::MAX)) - .map(|(row, _)| row) - .collect(); - Ok(QueryResult::with_rows( - projection_plan.column_names.clone(), - rows, - )) - } - - #[allow(clippy::too_many_arguments)] - fn simple_expression_projection_result_from_deferred_row_ids( - &self, - store: &S, - state: PersistedTableState, - table_schema: &TableSchema, - binding_name: &str, - projection: &[SelectItem], - projection_plan: &SimpleExpressionProjectionPlan<'_>, - filter: Option<&Expr>, - distinct: bool, - order_by: &[crate::sql::ast::OrderBy], - params: &[Value], - limit: Option, - offset: usize, - row_ids: &[i64], - use_persistent_pk_index: bool, - paged_locator_cache: Option<&DeferredPagedRowLocatorCache>, - ) -> Result { - let dataset = Dataset::with_rows( - table_bindings_with_hidden_row_id(table_schema, binding_name), - Vec::new(), - ); - let projection_order_by = projection_order_by_plan(order_by, projection); - let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); - let mut rows = Vec::with_capacity(bounded_row_count.unwrap_or(row_ids.len())); - let mut seen = BTreeSet::new(); - for row_id in row_ids { - let Some(stored_row) = read_deferred_stored_row_by_id( - store, - state, - table_schema, - *row_id, - use_persistent_pk_index, - paged_locator_cache, - )? - else { - continue; - }; - let mut eval_values = stored_row.values; - eval_values.push(Value::Int64(stored_row.row_id)); - if let Some(filter) = filter { - if !matches!( - self.eval_expr( - filter, - &dataset, - &eval_values, - params, - &BTreeMap::new(), - None, - )?, - Value::Bool(true) - ) { - continue; - } - } - let output = self.project_simple_expression_row( - projection_plan, - &dataset, - &eval_values, - params, - )?; - if distinct && !seen.insert(row_identity(&output)?) { - continue; - } - - let mut order_values = Vec::with_capacity(order_by.len()); - if let Some(order_by_plan) = projection_order_by.as_deref() { - for order in order_by_plan { - order_values.push(output[order.projection_index].clone()); - } - } else { - for order in order_by { - order_values.push(self.eval_expr( - &order.expr, - &dataset, - &eval_values, - params, - &BTreeMap::new(), - None, - )?); - } - } - let row = (QueryRow::new(output), order_values); - if let Some(bounded_row_count) = bounded_row_count { - if order_by.is_empty() { - if rows.len() < bounded_row_count { - rows.push(row); - } else { - break; - } - } else { - push_bounded_ordered_query_row( - Some(self), - &mut rows, - row, - order_by, - bounded_row_count, - )?; - } - } else { - rows.push(row); - } - } - - if !order_by.is_empty() { - sort_query_rows_by_order_values(Some(self), &mut rows, order_by)?; - } - - let rows = rows - .into_iter() - .skip(offset) - .take(limit.unwrap_or(usize::MAX)) - .map(|(row, _)| row) - .collect(); - Ok(QueryResult::with_rows( - projection_plan.column_names.clone(), - rows, - )) - } - - #[allow(clippy::too_many_arguments)] - fn simple_expression_projection_result_from_source( - &self, - row_source: VisibleTableRowSource<'_>, - table_schema: &TableSchema, - binding_name: &str, - projection: &[SelectItem], - projection_plan: &SimpleExpressionProjectionPlan<'_>, - filter: Option<&Expr>, - distinct: bool, - order_by: &[crate::sql::ast::OrderBy], - params: &[Value], - limit: Option, - offset: usize, - ) -> Result { - let dataset = Dataset::with_rows( - table_bindings_with_hidden_row_id(table_schema, binding_name), - Vec::new(), - ); - let projection_order_by = projection_order_by_plan(order_by, projection); - let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); - let mut rows = Vec::with_capacity(bounded_row_count.unwrap_or(row_source.row_count())); - let mut seen = BTreeSet::new(); - for stored_row in row_source.rows() { - let stored_row = stored_row?; - let mut values = stored_row.values().to_vec(); - values.push(Value::Int64(stored_row.row_id())); - if let Some(filter) = filter { - if !matches!( - self.eval_expr(filter, &dataset, &values, params, &BTreeMap::new(), None)?, - Value::Bool(true) - ) { - continue; - } - } - - let output = - self.project_simple_expression_row(projection_plan, &dataset, &values, params)?; - if distinct && !seen.insert(row_identity(&output)?) { - continue; - } - - let mut order_values = Vec::with_capacity(order_by.len()); - if let Some(order_by_plan) = projection_order_by.as_deref() { - for order in order_by_plan { - order_values.push(output[order.projection_index].clone()); - } - } else { - for order in order_by { - order_values.push(self.eval_expr( - &order.expr, - &dataset, - &values, - params, - &BTreeMap::new(), - None, - )?); - } - } - let row = (QueryRow::new(output), order_values); - if let Some(bounded_row_count) = bounded_row_count { - if order_by.is_empty() { - if rows.len() < bounded_row_count { - rows.push(row); - } else { - break; - } - } else { - push_bounded_ordered_query_row( - Some(self), - &mut rows, - row, - order_by, - bounded_row_count, - )?; - } - } else { - rows.push(row); - } - } - - if !order_by.is_empty() { - sort_query_rows_by_order_values(Some(self), &mut rows, order_by)?; - } - - let rows = rows - .into_iter() - .skip(offset) - .take(limit.unwrap_or(usize::MAX)) - .map(|(row, _)| row) - .collect(); - Ok(QueryResult::with_rows( - projection_plan.column_names.clone(), - rows, - )) - } - - #[allow(clippy::too_many_arguments)] - fn simple_expression_projection_result_from_row_ids( - &self, - row_source: VisibleTableRowSource<'_>, - table_schema: &TableSchema, - binding_name: &str, - projection: &[SelectItem], - projection_plan: &SimpleExpressionProjectionPlan<'_>, - filter: Option<&Expr>, - distinct: bool, - order_by: &[crate::sql::ast::OrderBy], - params: &[Value], - limit: Option, - offset: usize, - row_ids: &[i64], - ) -> Result { - let dataset = Dataset::with_rows( - table_bindings_with_hidden_row_id(table_schema, binding_name), - Vec::new(), - ); - let projection_order_by = projection_order_by_plan(order_by, projection); - let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); - let mut rows = Vec::with_capacity(bounded_row_count.unwrap_or(row_ids.len())); - let mut seen = BTreeSet::new(); - for row_id in row_ids { - let Some(stored_row) = row_source.row_by_id(*row_id)? else { - continue; - }; - let mut values = stored_row.values().to_vec(); - values.push(Value::Int64(stored_row.row_id())); - if let Some(filter) = filter { - if !matches!( - self.eval_expr(filter, &dataset, &values, params, &BTreeMap::new(), None)?, - Value::Bool(true) - ) { - continue; - } - } - - let output = - self.project_simple_expression_row(projection_plan, &dataset, &values, params)?; - if distinct && !seen.insert(row_identity(&output)?) { - continue; - } - - let mut order_values = Vec::with_capacity(order_by.len()); - if let Some(order_by_plan) = projection_order_by.as_deref() { - for order in order_by_plan { - order_values.push(output[order.projection_index].clone()); - } - } else { - for order in order_by { - order_values.push(self.eval_expr( - &order.expr, - &dataset, - &values, - params, - &BTreeMap::new(), - None, - )?); - } - } - let row = (QueryRow::new(output), order_values); - if let Some(bounded_row_count) = bounded_row_count { - if order_by.is_empty() { - if rows.len() < bounded_row_count { - rows.push(row); - } else { - break; - } - } else { - push_bounded_ordered_query_row( - Some(self), - &mut rows, - row, - order_by, - bounded_row_count, - )?; - } - } else { - rows.push(row); - } - } - - if !order_by.is_empty() { - sort_query_rows_by_order_values(Some(self), &mut rows, order_by)?; - } - - let rows = rows - .into_iter() - .skip(offset) - .take(limit.unwrap_or(usize::MAX)) - .map(|(row, _)| row) - .collect(); - Ok(QueryResult::with_rows( - projection_plan.column_names.clone(), - rows, - )) - } - - fn project_simple_expression_row( - &self, - projection_plan: &SimpleExpressionProjectionPlan<'_>, - dataset: &Dataset, - values: &[Value], - params: &[Value], - ) -> Result> { - let mut output = Vec::with_capacity(projection_plan.sources.len()); - for source in &projection_plan.sources { - match source { - SimpleExpressionProjectionSource::Column(index) => { - let value = values.get(*index).ok_or_else(|| { - DbError::internal("expression projection column index exceeds row width") - })?; - output.push(value.clone()); - } - SimpleExpressionProjectionSource::Expr(expr) => { - output.push(self.eval_expr( - expr, - dataset, - values, - params, - &BTreeMap::new(), - None, - )?); - } - } - } - Ok(output) - } - - #[allow(clippy::too_many_arguments)] - fn try_simple_filtered_projection_exact_index_result( - &self, - row_source: VisibleTableRowSource<'_>, - table_name: &str, - table_schema: &TableSchema, - filter: &Expr, - projection_indexes: &[usize], - column_names: Vec, - order_by: Option<&[SimpleOrderByPlan]>, - params: &[Value], - limit: Option, - offset: usize, - ) -> Result> { - if limit == Some(0) { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - } - let Some((filter_table, filter_column, value_expr)) = simple_btree_lookup(filter) else { - return Ok(None); - }; - if let Some(filter_table) = filter_table { - if !identifiers_equal(filter_table, table_name) { - return Ok(None); - } - } - - let value = self.eval_expr( - value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?; - if matches!(value, Value::Null) { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - } - - let mut rows = Vec::new(); - if row_id_alias_column_name(table_schema) - .is_some_and(|column_name| identifiers_equal(column_name, filter_column)) - { - if let Value::Int64(row_id) = value { - if let Some(stored_row) = row_source.row_by_id(row_id)? { - rows.push(project_simple_projection_values( - stored_row.values(), - projection_indexes, - )); - } - } - return Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - column_names, - order_by, - limit, - offset, - )?)); - } - - let Some(index) = self.single_column_btree_index(table_name, filter_column) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { - return Ok(None); - }; - match keys.row_ids_for_value_set(&value)? { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(row_id) => { - if let Some(stored_row) = row_source.row_by_id(row_id)? { - rows.push(project_simple_projection_values( - stored_row.values(), - projection_indexes, - )); - } - } - RuntimeRowIdSet::Contiguous { start, len } => { - rows.reserve(len); - for row_id in contiguous_row_ids(start, len) { - if let Some(stored_row) = row_source.row_by_id(row_id)? { - rows.push(project_simple_projection_values( - stored_row.values(), - projection_indexes, - )); - } - } - } - RuntimeRowIdSet::Many(row_ids) => { - rows.reserve(row_ids.len()); - for row_id in row_ids { - if let Some(stored_row) = row_source.row_by_id(*row_id)? { - rows.push(project_simple_projection_values( - stored_row.values(), - projection_indexes, - )); - } - } - } - RuntimeRowIdSet::Owned(row_ids) => { - rows.reserve(row_ids.len()); - for row_id in row_ids { - if let Some(stored_row) = row_source.row_by_id(row_id)? { - rows.push(project_simple_projection_values( - stored_row.values(), - projection_indexes, - )); - } - } - } - } - Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - column_names, - order_by, - limit, - offset, - )?)) - } - - #[allow(clippy::too_many_arguments)] - fn try_simple_filtered_projection_range_index_result( - &self, - row_source: VisibleTableRowSource<'_>, - table_name: &str, - table_schema: &TableSchema, - filter_column_index: usize, - filter_column_name: &str, - lower_bound: Option<&SimpleRangeBoundValue>, - upper_bound: Option<&SimpleRangeBoundValue>, - residual_plans: &[SimpleResidualPlan], - projection_indexes: &[usize], - column_names: Vec, - limit: Option, - offset: usize, - ) -> Result> { - if limit == Some(0) { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - } - let Some(filter_column) = table_schema.columns.get(filter_column_index) else { - return Ok(None); - }; - if !simple_range_bounds_match_column_type( - filter_column.column_type, - lower_bound, - upper_bound, - ) { - return Ok(None); - } - let Some(index) = self.single_column_btree_index(table_name, filter_column_name) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { - return Ok(None); - }; - - let lower_key = lower_bound - .map(|bound| encode_runtime_index_key(&bound.value).map(|key| (key, bound.inclusive))) - .transpose()?; - let upper_key = upper_bound - .map(|bound| encode_runtime_index_key(&bound.value).map(|key| (key, bound.inclusive))) - .transpose()?; - let lower_range: Bound<&[u8]> = match lower_key.as_ref() { - Some((key, true)) => Bound::Included(key.as_slice()), - Some((key, false)) => Bound::Excluded(key.as_slice()), - None => Bound::Unbounded, - }; - let upper_range: Bound<&[u8]> = match upper_key.as_ref() { - Some((key, true)) => Bound::Included(key.as_slice()), - Some((key, false)) => Bound::Excluded(key.as_slice()), - None => Bound::Unbounded, - }; - - let mut candidate_row_ids = Vec::new(); - match keys { - RuntimeBtreeKeys::UniqueEncoded(entries, deleted) => { - candidate_row_ids.extend( - entries - .range::<[u8], _>((lower_range, upper_range)) - .filter_map(|(_, row_id)| (!deleted.contains(row_id)).then_some(*row_id)), - ); - } - RuntimeBtreeKeys::NonUniqueEncoded(entries, deleted) => { - for row_ids in entries - .range::<[u8], _>((lower_range, upper_range)) - .map(|(_, row_ids)| row_ids) - { - candidate_row_ids.extend( - row_ids - .iter() - .copied() - .filter(|row_id| !deleted.contains(row_id)), - ); - } - } - RuntimeBtreeKeys::UniqueInt64(..) - | RuntimeBtreeKeys::NonUniqueInt64(..) - | RuntimeBtreeKeys::UniqueUuid(..) - | RuntimeBtreeKeys::NonUniqueUuid(..) => return Ok(None), - } - if candidate_row_ids.len().saturating_mul(2) > row_source.row_count() { - return Ok(None); - } - candidate_row_ids.sort_unstable(); - - let take = limit.unwrap_or(usize::MAX); - let mut skipped = 0usize; - let mut rows = Vec::with_capacity(take.min(candidate_row_ids.len()).min(128)); - for row_id in candidate_row_ids { - let Some(stored_row) = row_source.row_by_id(row_id)? else { - continue; - }; - let values = stored_row.values(); - let candidate = &values[filter_column_index]; - if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? - || !simple_residual_matches_all(values, residual_plans)? - { - continue; - } - if skipped < offset { - skipped = skipped.saturating_add(1); - continue; - } - rows.push(project_simple_projection_values(values, projection_indexes)); - if rows.len() >= take { - break; - } - } - - Ok(Some(QueryResult::with_rows(column_names, rows))) - } - - #[allow(clippy::too_many_arguments)] - fn try_simple_filtered_projection_ordered_index_result( - &self, - row_source: VisibleTableRowSource<'_>, - table_name: &str, - table_schema: &TableSchema, - filter_column_index: usize, - lower_bound: Option<&SimpleRangeBoundValue>, - upper_bound: Option<&SimpleRangeBoundValue>, - residual_plans: &[SimpleResidualPlan], - projection_indexes: &[usize], - column_names: Vec, - order_by: Option<&[SimpleOrderByPlan]>, - limit: Option, - offset: usize, - ) -> Result> { - let Some([order_by]) = order_by else { - return Ok(None); - }; - if order_by.collation.is_some() { - return Ok(None); - } - if limit == Some(0) { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - } - let Some(order_column_index) = projection_indexes.get(order_by.projection_index).copied() - else { - return Ok(None); - }; - let Some(order_column) = table_schema.columns.get(order_column_index) else { - return Ok(None); - }; - let Some(index) = self.single_column_btree_index(table_name, &order_column.name) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { - return Ok(None); - }; - let take = limit.unwrap_or(usize::MAX); - let mut skipped = 0usize; - let mut rows = Vec::with_capacity(take.min(64)); - - let mut push_matching_row = |row_id| -> Result { - let Some(stored_row) = row_source.row_by_id(row_id)? else { - return Ok(false); - }; - let values = stored_row.values(); - let candidate = &values[filter_column_index]; - if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? - || !simple_residual_matches_all(values, residual_plans)? - { - return Ok(false); - } - if skipped < offset { - skipped = skipped.saturating_add(1); - return Ok(false); - } - rows.push(project_simple_projection_values(values, projection_indexes)); - Ok(rows.len() >= take) - }; - - match keys { - RuntimeBtreeKeys::UniqueEncoded(entries, deleted) => { - if order_by.descending { - for row_id in entries.values().rev() { - if deleted.contains(row_id) { - continue; - } - if push_matching_row(*row_id)? { - break; - } - } - } else { - for row_id in entries.values() { - if deleted.contains(row_id) { - continue; - } - if push_matching_row(*row_id)? { - break; - } - } - } - } - RuntimeBtreeKeys::NonUniqueEncoded(entries, deleted) => { - if order_by.descending { - let mut done = false; - for row_ids in entries.values().rev() { - for row_id in row_ids { - if deleted.contains(row_id) { - continue; - } - if push_matching_row(*row_id)? { - done = true; - break; - } - } - if done { - break; - } - } - } else { - let mut done = false; - for row_ids in entries.values() { - for row_id in row_ids { - if deleted.contains(row_id) { - continue; - } - if push_matching_row(*row_id)? { - done = true; - break; - } - } - if done { - break; - } - } - } - } - RuntimeBtreeKeys::UniqueUuid(entries, deleted) => { - if order_by.descending { - for row_id in entries.values().rev() { - if deleted.contains(row_id) { - continue; - } - if push_matching_row(*row_id)? { - break; - } - } - } else { - for row_id in entries.values() { - if deleted.contains(row_id) { - continue; - } - if push_matching_row(*row_id)? { - break; - } - } - } - } - RuntimeBtreeKeys::NonUniqueUuid(entries, deleted) => { - if order_by.descending { - let mut done = false; - for row_ids in entries.values().rev() { - for row_id in row_ids { - if deleted.contains(row_id) { - continue; - } - if push_matching_row(*row_id)? { - done = true; - break; - } - } - if done { - break; - } - } - } else { - let mut done = false; - for row_ids in entries.values() { - for row_id in row_ids { - if deleted.contains(row_id) { - continue; - } - if push_matching_row(*row_id)? { - done = true; - break; - } - } - if done { - break; - } - } - } - } - RuntimeBtreeKeys::UniqueInt64(..) | RuntimeBtreeKeys::NonUniqueInt64(..) => { - return Ok(None) - } - } - - Ok(Some(QueryResult::with_rows(column_names, rows))) - } - - #[allow(clippy::too_many_arguments)] - fn simple_filtered_projection_result_from_source( - &self, - row_source: VisibleTableRowSource<'_>, - filter_column_index: usize, - lower_bound: Option<&SimpleRangeBoundValue>, - upper_bound: Option<&SimpleRangeBoundValue>, - residual_plans: &[SimpleResidualPlan], - projection_indexes: &[usize], - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, - ) -> Result { - let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); - let mut rows = Vec::with_capacity( - bounded_row_count - .unwrap_or(row_source.row_count()) - .min(row_source.row_count()), - ); - if order_by.is_none() { - let mut skipped = 0usize; - for stored_row in row_source.rows() { - let stored_row = stored_row?; - let values = stored_row.values(); - let candidate = &values[filter_column_index]; - if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? { - continue; - } - if !simple_residual_matches_all(values, residual_plans)? { - continue; - } - if skipped < offset { - skipped = skipped.saturating_add(1); - continue; - } - if limit.is_some_and(|limit| rows.len() >= limit) { - break; - } - rows.push(project_simple_projection_values(values, projection_indexes)); - } - return Ok(QueryResult::with_rows(column_names, rows)); - } - - for stored_row in row_source.rows() { - let stored_row = stored_row?; - let values = stored_row.values(); - let candidate = &values[filter_column_index]; - if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? { - continue; - } - if !simple_residual_matches_all(values, residual_plans)? { - continue; - } - let row = project_simple_projection_values(values, projection_indexes); - if let (Some(order_by), Some(bounded_row_count)) = ( - order_by.as_deref(), - bounded_row_count.filter(|bounded| { - *bounded > 0 && row_source.row_count() > bounded.saturating_mul(4) - }), - ) { - push_bounded_projection_ordered_query_row( - Some(self), - &mut rows, - row, - order_by, - bounded_row_count, - )?; - } else { - rows.push(row); - } - } - apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - column_names, - order_by.as_deref(), - limit, - offset, - ) - } - - fn simple_contains_like_projection_result_from_source( - &self, - row_source: VisibleTableRowSource<'_>, - filter_column_index: usize, - literal: &str, - projection_indexes: &[usize], - column_names: Vec, - ) -> Result { - let mut rows = Vec::new(); - for stored_row in row_source.rows() { - let stored_row = stored_row?; - let values = stored_row.values(); - let Some(Value::Text(candidate)) = values.get(filter_column_index) else { - continue; - }; - if candidate.contains(literal) { - rows.push(project_simple_projection_values(values, projection_indexes)); - } - } - Ok(QueryResult::with_rows(column_names, rows)) - } - - #[allow(clippy::too_many_arguments)] - fn simple_distinct_filtered_projection_result_from_source( - &self, - row_source: VisibleTableRowSource<'_>, - filter_column_index: usize, - lower_bound: Option<&SimpleRangeBoundValue>, - upper_bound: Option<&SimpleRangeBoundValue>, - projection_indexes: &[usize], - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, - ) -> Result { - let mut rows = Vec::new(); - let mut seen = BTreeSet::new(); - for stored_row in row_source.rows() { - let stored_row = stored_row?; - let candidate = &stored_row.values()[filter_column_index]; - if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? { - continue; - } - let projected = - project_simple_projection_values(stored_row.values(), projection_indexes); - if seen.insert(row_identity(projected.values())?) { - rows.push(projected); - } - } - apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - column_names, - order_by.as_deref(), - limit, - offset, - ) - } - - #[allow(clippy::too_many_arguments)] - fn simple_filtered_projection_result_from_persisted_state( - &self, - store: &S, - state: PersistedTableState, - filter_column_index: usize, - lower_bound: Option<&SimpleRangeBoundValue>, - upper_bound: Option<&SimpleRangeBoundValue>, - residual_plans: &[SimpleResidualPlan], - projection_indexes: &[usize], - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, - ) -> Result { - let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); - let mut rows = Vec::with_capacity( - bounded_row_count - .unwrap_or(state.row_count) - .min(state.row_count), - ); - if order_by.is_none() { - let mut skipped = 0usize; - visit_persisted_table_rows_until(store, state, |_, values| { - let candidate = &values[filter_column_index]; - if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? { - return Ok(false); - } - if !simple_residual_matches_all(values, residual_plans)? { - return Ok(false); - } - if skipped < offset { - skipped = skipped.saturating_add(1); - return Ok(false); - } - if limit.is_some_and(|limit| rows.len() >= limit) { - return Ok(true); - } - rows.push(project_simple_projection_values(values, projection_indexes)); - Ok(limit.is_some_and(|limit| rows.len() >= limit)) - })?; - return Ok(QueryResult::with_rows(column_names, rows)); - } - visit_persisted_table_rows(store, state, |_, values| { - let candidate = &values[filter_column_index]; - if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? { - return Ok(()); - } - if !simple_residual_matches_all(values, residual_plans)? { - return Ok(()); - } - let row = project_simple_projection_values(values, projection_indexes); - if let (Some(order_by), Some(bounded_row_count)) = ( - order_by.as_deref(), - bounded_row_count - .filter(|bounded| *bounded > 0 && state.row_count > bounded.saturating_mul(4)), - ) { - push_bounded_projection_ordered_query_row( - Some(self), - &mut rows, - row, - order_by, - bounded_row_count, - )?; - } else { - rows.push(row); - } - Ok(()) - })?; - apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - column_names, - order_by.as_deref(), - limit, - offset, - ) - } - - fn build_simple_residual_plans( - &self, - table_schema: &TableSchema, - table_name: &str, - binding_name: &str, - residual: &[SimpleResidualFilterTerm<'_>], - params: &[Value], - ) -> Result> { - let mut plans = Vec::with_capacity(residual.len()); - for term in residual { - if let Some(term_table) = term.table { - if !identifiers_equal(term_table, table_name) - && !identifiers_equal(term_table, binding_name) - { - return Ok(plans); - } - } - let column_index = table_schema - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, term.column)) - .ok_or_else(|| { - DbError::internal(format!( - "simple filtered projection residual column {} missing from {table_name}", - term.column - )) - })?; - let value = self.eval_expr( - term.value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?; - plans.push(SimpleResidualPlan { - column_index, - op: term.op, - value, - }); - } - Ok(plans) - } - - #[allow(clippy::too_many_arguments)] - fn simple_distinct_filtered_projection_result_from_persisted_state( - &self, - store: &S, - state: PersistedTableState, - filter_column_index: usize, - lower_bound: Option<&SimpleRangeBoundValue>, - upper_bound: Option<&SimpleRangeBoundValue>, - projection_indexes: &[usize], - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, - ) -> Result { - let mut rows = Vec::new(); - let mut seen = BTreeSet::new(); - visit_persisted_table_rows(store, state, |_, values| { - let candidate = &values[filter_column_index]; - if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? { - return Ok(()); - } - let projected = project_simple_projection_values(values, projection_indexes); - if seen.insert(row_identity(projected.values())?) { - rows.push(projected); - } - Ok(()) - })?; - apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - column_names, - order_by.as_deref(), - limit, - offset, - ) - } - - fn try_execute_simple_indexed_projection_query( - &self, - query: &Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_simple_indexed_projection_query(query, params)? else { - return Ok(None); - }; - let row_source = self.visible_table_row_source(plan.table_name); - if plan.limit == Some(0) { - return Ok(Some(QueryResult::with_rows(plan.column_names, Vec::new()))); - } - - if plan.extra_lookup_terms.is_empty() - && row_id_alias_column_name(plan.table_schema) - .is_some_and(|column_name| identifiers_equal(column_name, plan.filter_column)) - { - let mut rows = Vec::new(); - if let Some(row_id) = value_as_int64(&plan.lookup_value) { - if let Some(stored_row) = row_source - .map(|source| source.row_by_id(row_id)) - .transpose()? - .flatten() - { - rows.push(project_simple_projection_values( - stored_row.values(), - &plan.projection_indexes, - )); - } - } - return Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - plan.column_names, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - )?)); - } - - let Some(index) = self.btree_index_for_simple_indexed_projection_plan(&plan) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { keys, covering }) = self.index(&index.name) else { - return Ok(None); - }; - let covering_offsets = if row_source.is_some_and(|source| !source.has_tombstoned_rows()) { - covering.as_ref().and_then(|covering| { - covering_projection_offsets(covering, plan.table_schema, &plan.projection_indexes) - }) - } else { - None - }; - let row_id_order = indexed_projection_row_id_order(&plan); - let row_ids = row_ids_for_simple_indexed_projection_lookup(keys, &plan)?; - - let scan_limit = if let Some((_, limit_with_offset)) = row_id_order { - limit_with_offset - } else if plan.order_by.is_none() && plan.offset == 0 { - plan.limit.unwrap_or(usize::MAX) - } else { - usize::MAX - }; - let mut rows = Vec::with_capacity(row_ids.len().min(scan_limit)); - let mut row_lookup_error = None; - if let Some((descending, _)) = row_id_order { - let ordered_row_ids = row_ids.into_sorted_vec(descending); - let limit = plan.limit.unwrap_or(usize::MAX); - for row_id in ordered_row_ids.into_iter().skip(plan.offset).take(limit) { - if row_lookup_error.is_some() || rows.len() >= scan_limit { - break; - } - if let (Some(covering), Some(offsets)) = - (covering.as_ref(), covering_offsets.as_ref()) - { - if let Some(row) = covering.project_row(row_id, offsets) { - rows.push(row); - continue; - } - } - let stored_row = match row_source - .map(|source| source.row_by_id(row_id)) - .transpose() - { - Ok(Some(Some(stored_row))) => stored_row, - Ok(Some(None)) | Ok(None) => continue, - Err(error) => { - row_lookup_error = Some(error); - break; - } - }; - rows.push(project_simple_projection_values( - stored_row.values(), - &plan.projection_indexes, - )); - } - if let Some(error) = row_lookup_error { - return Err(error); - } - return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); - } else { - row_ids.for_each(|row_id| { - if row_lookup_error.is_some() || rows.len() >= scan_limit { - return; - } - if let (Some(covering), Some(offsets)) = - (covering.as_ref(), covering_offsets.as_ref()) - { - if let Some(row) = covering.project_row(row_id, offsets) { - rows.push(row); - return; - } - } - let stored_row = match row_source - .map(|source| source.row_by_id(row_id)) - .transpose() - { - Ok(Some(Some(stored_row))) => stored_row, - Ok(Some(None)) | Ok(None) => return, - Err(error) => { - row_lookup_error = Some(error); - return; - } - }; - rows.push(project_simple_projection_values( - stored_row.values(), - &plan.projection_indexes, - )); - }); - } - if let Some(error) = row_lookup_error { - return Err(error); - } - Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - plan.column_names, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - )?)) - } - - pub(crate) fn try_execute_simple_deferred_indexed_projection_query( - &self, - query: &Query, - params: &[Value], - pager: &PagerHandle, - wal: &WalHandle, - snapshot_lsn: u64, - use_persistent_pk_index: bool, - ) -> Result> { - let Some(plan) = self.analyze_simple_indexed_projection_query(query, params)? else { - return Ok(None); - }; - if let Some(row_source) = self.visible_table_row_source(plan.table_name) { - if plan.limit == Some(0) { - return Ok(Some(QueryResult::with_rows(plan.column_names, Vec::new()))); - } - if plan.extra_lookup_terms.is_empty() - && row_id_alias_column_name(plan.table_schema) - .is_some_and(|column_name| identifiers_equal(column_name, plan.filter_column)) - { - let mut rows = Vec::new(); - if let Some(row_id) = value_as_int64(&plan.lookup_value) { - if let Some(stored_row) = row_source.row_by_id(row_id)? { - rows.push(project_simple_projection_values( - stored_row.values(), - &plan.projection_indexes, - )); - } - } - return Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - plan.column_names, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - )?)); - } - - let Some(index) = self.btree_index_for_simple_indexed_projection_plan(&plan) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { keys, covering }) = self.index(&index.name) else { - return Ok(None); - }; - let row_id_order = indexed_projection_row_id_order(&plan); - let covering_offsets = if !row_source.has_tombstoned_rows() { - covering.as_ref().and_then(|covering| { - covering_projection_offsets( - covering, - plan.table_schema, - &plan.projection_indexes, - ) - }) - } else { - None - }; - let row_ids = row_ids_for_simple_indexed_projection_lookup(keys, &plan)?; - let scan_limit = - if row_id_order.is_none() && plan.order_by.is_none() && plan.offset == 0 { - plan.limit.unwrap_or(usize::MAX) - } else if let Some((_, limit_with_offset)) = row_id_order { - limit_with_offset - } else { - usize::MAX - }; - let mut rows = Vec::with_capacity(row_ids.len().min(scan_limit)); - let mut row_lookup_error = None; - if let Some((descending, _)) = row_id_order { - let ordered_row_ids = row_ids.into_sorted_vec(descending); - let limit = plan.limit.unwrap_or(usize::MAX); - for row_id in ordered_row_ids.into_iter().skip(plan.offset).take(limit) { - if row_lookup_error.is_some() || rows.len() >= scan_limit { - break; - } - if let (Some(covering), Some(offsets)) = - (covering.as_ref(), covering_offsets.as_ref()) - { - if let Some(row) = covering.project_row(row_id, offsets) { - rows.push(row); - continue; - } - } - match row_source.row_by_id(row_id) { - Ok(Some(stored_row)) => rows.push(project_simple_projection_values( - stored_row.values(), - &plan.projection_indexes, - )), - Ok(None) => {} - Err(error) => row_lookup_error = Some(error), - } - } - if let Some(error) = row_lookup_error { - return Err(error); - } - return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); - } - row_ids.for_each(|row_id| { - if row_lookup_error.is_some() || rows.len() >= scan_limit { - return; - } - if let (Some(covering), Some(offsets)) = - (covering.as_ref(), covering_offsets.as_ref()) - { - if let Some(row) = covering.project_row(row_id, offsets) { - rows.push(row); - return; - } - } - match row_source.row_by_id(row_id) { - Ok(Some(stored_row)) => rows.push(project_simple_projection_values( - stored_row.values(), - &plan.projection_indexes, - )), - Ok(None) => {} - Err(error) => row_lookup_error = Some(error), - } - }); - if let Some(error) = row_lookup_error { - return Err(error); - } - return Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - plan.column_names, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - )?)); - } - if !self.has_deferred_tables() { - return Ok(None); - } - if plan.limit == Some(0) { - return Ok(Some(QueryResult::with_rows(plan.column_names, Vec::new()))); - } - if !self - .deferred_table_names() - .any(|candidate| identifiers_equal(candidate, plan.table_name)) - { - return Ok(None); - } - let Some(state) = self.persisted_table_state(plan.table_name) else { - return Ok(None); - }; - let paged_locator_cache = self - .catalog - .table(plan.table_name) - .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) - .map(|cache| cache.as_ref()); - - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - - let mut rows = Vec::new(); - if plan.extra_lookup_terms.is_empty() - && row_id_alias_column_name(plan.table_schema) - .is_some_and(|column_name| identifiers_equal(column_name, plan.filter_column)) - { - if let Some(row_id) = value_as_int64(&plan.lookup_value) { - if let Some(stored_row) = read_deferred_stored_row_by_id( - &store, - state, - plan.table_schema, - row_id, - use_persistent_pk_index, - paged_locator_cache, - )? { - rows.push(project_simple_projection_row( - &stored_row, - &plan.projection_indexes, - )); - } - } - return Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - plan.column_names, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - )?)); - } - - let Some(index) = self.btree_index_for_simple_indexed_projection_plan(&plan) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { keys, covering }) = self.index(&index.name) else { - return Ok(None); - }; - let covering_offsets = covering.as_ref().and_then(|covering| { - covering_projection_offsets(covering, plan.table_schema, &plan.projection_indexes) - }); - let row_id_order = indexed_projection_row_id_order(&plan); - let row_ids = row_ids_for_simple_indexed_projection_lookup(keys, &plan)?; - let scan_limit = if let Some((_, limit_with_offset)) = row_id_order { - limit_with_offset - } else if plan.order_by.is_none() && plan.offset == 0 { - plan.limit.unwrap_or(usize::MAX) - } else { - usize::MAX - }; - rows.reserve(row_ids.len().min(scan_limit)); - let mut row_lookup_error = None; - if let Some((descending, _)) = row_id_order { - let ordered_row_ids = row_ids.into_sorted_vec(descending); - let limit = plan.limit.unwrap_or(usize::MAX); - for row_id in ordered_row_ids.into_iter().skip(plan.offset).take(limit) { - if row_lookup_error.is_some() || rows.len() >= scan_limit { - break; - } - if let (Some(covering), Some(offsets)) = - (covering.as_ref(), covering_offsets.as_ref()) - { - if let Some(row) = covering.project_row(row_id, offsets) { - rows.push(row); - continue; - } - } - match read_deferred_stored_row_by_id( - &store, - state, - plan.table_schema, - row_id, - use_persistent_pk_index, - paged_locator_cache, - ) { - Ok(Some(stored_row)) => rows.push(project_simple_projection_row( - &stored_row, - &plan.projection_indexes, - )), - Ok(None) => {} - Err(error) => { - row_lookup_error = Some(error); - break; - } - } - } - if let Some(error) = row_lookup_error { - return Err(error); - } - return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); - } else { - row_ids.for_each(|row_id| { - if row_lookup_error.is_some() || rows.len() >= scan_limit { - return; - } - if let (Some(covering), Some(offsets)) = - (covering.as_ref(), covering_offsets.as_ref()) - { - if let Some(row) = covering.project_row(row_id, offsets) { - rows.push(row); - return; - } - } - match read_deferred_stored_row_by_id( - &store, - state, - plan.table_schema, - row_id, - use_persistent_pk_index, - paged_locator_cache, - ) { - Ok(Some(stored_row)) => rows.push(project_simple_projection_row( - &stored_row, - &plan.projection_indexes, - )), - Ok(None) => {} - Err(error) => row_lookup_error = Some(error), - } - }); - } - if let Some(error) = row_lookup_error { - return Err(error); - } - Ok(Some(apply_simple_projection_postprocessing_with_order( - Some(self), - rows, - plan.column_names, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - )?)) - } - - pub(crate) fn execute_simple_row_id_projection_at_snapshot( - &self, - request: SimpleRowIdProjectionRequest<'_>, - ) -> Result> { - if let Some(view) = self.visible_view(request.table_name, NameResolutionScope::Session) { - return self.execute_simple_view_row_id_projection_at_snapshot(&request, view); - } - if self.visible_table_is_temporary(request.table_name) { - return Ok(None); - } - let Some(table_schema) = self.table_schema(request.table_name) else { - return Ok(None); - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - if !row_id_alias_column_name(table_schema) - .is_some_and(|column_name| identifiers_equal(column_name, request.filter_column)) - { - return Ok(None); - } - let mut projection_indexes = Vec::with_capacity(request.projection_columns.len()); - let mut column_names = Vec::with_capacity(request.projection_columns.len()); - for projection_column in request.projection_columns { - let Some(index) = table_schema - .columns - .iter() - .position(|column| identifiers_equal(&column.name, projection_column)) - else { - return Ok(None); - }; - projection_indexes.push(index); - column_names.push((*projection_column).to_string()); - } - - self.execute_validated_simple_row_id_projection_at_snapshot( - ValidatedSimpleRowIdProjectionRequest { - table_schema, - projection_indexes: &projection_indexes, - column_names: Arc::from(column_names), - lookup_row_id: request.lookup_row_id, - pager: request.pager, - wal: request.wal, - snapshot_lsn: request.snapshot_lsn, - use_persistent_pk_index: request.use_persistent_pk_index, - }, - ) - } - - pub(crate) fn try_execute_resident_simple_row_id_projection( - &self, - table_name: &str, - projection_columns: &[&str], - filter_column: &str, - lookup_row_id: i64, - ) -> Result> { - if let Some(view) = self.visible_view(table_name, NameResolutionScope::Session) { - if view.temporary { - return Ok(None); - } - // The observed-current caller has already established that this - // runtime represents a stable committed snapshot. When every - // base table needed by the view is resident, execute the same - // validated indexed join without acquiring a reader slot or - // constructing a snapshot page store. A missing resident source - // returns `None`, preserving the snapshot-backed fallback. - let store = page::InMemoryPageStore::default(); - return self.execute_simple_view_row_id_projection_from_store( - projection_columns, - filter_column, - lookup_row_id, - view, - &store, - false, - true, - ); - } - if self.visible_table_is_temporary(table_name) { - return Ok(None); - } - let Some(table_schema) = self.table_schema(table_name) else { - return Ok(None); - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - if !row_id_alias_column_name(table_schema) - .is_some_and(|column_name| identifiers_equal(column_name, filter_column)) - { - return Ok(None); - } - let mut projection_indexes = Vec::with_capacity(projection_columns.len()); - let mut column_names = Vec::with_capacity(projection_columns.len()); - for projection_column in projection_columns { - let Some(index) = table_schema - .columns - .iter() - .position(|column| identifiers_equal(&column.name, projection_column)) - else { - return Ok(None); - }; - projection_indexes.push(index); - column_names.push((*projection_column).to_string()); - } - self.try_execute_validated_resident_simple_row_id_projection( - table_schema, - &projection_indexes, - Arc::from(column_names), - lookup_row_id, - ) - } - - fn execute_simple_view_row_id_projection_at_snapshot( - &self, - request: &SimpleRowIdProjectionRequest<'_>, - view: &ViewSchema, - ) -> Result> { - let store = SnapshotPageStore { - pager: request.pager, - wal: request.wal, - snapshot_lsn: request.snapshot_lsn, - }; - self.execute_simple_view_row_id_projection_from_store( - request.projection_columns, - request.filter_column, - request.lookup_row_id, - view, - &store, - request.use_persistent_pk_index, - false, - ) - } - - #[allow(clippy::too_many_arguments)] - fn execute_simple_view_row_id_projection_from_store( - &self, - projection_columns: &[&str], - filter_column: &str, - lookup_row_id: i64, - view: &ViewSchema, - store: &S, - use_persistent_pk_index: bool, - resident_only: bool, - ) -> Result> { - if view.temporary { - return Ok(None); - } - let view_query = self.cached_view_query(view)?; - if view_query.recursive - || !view_query.ctes.is_empty() - || !view_query.order_by.is_empty() - || view_query.limit.is_some() - || view_query.offset.is_some() - { - return Ok(None); - } - let QueryBody::Select(view_select) = &view_query.body else { - return Ok(None); - }; - if view_select.distinct - || !view_select.distinct_on.is_empty() - || !view_select.group_by.is_empty() - || view_select.having.is_some() - || view_select.filter.is_some() - || projection_has_aggregate_items(&view_select.projection) - || view_select.from.len() != 1 - { - return Ok(None); - } - - let Some(filter_source_expr) = - view_projection_expr_for_output_column(&view_select.projection, filter_column) - else { - return Ok(None); - }; - let Expr::Column { - table: Some(filter_source_table), - column: filter_source_column, - } = &filter_source_expr - else { - return Ok(None); - }; - - let mut table_bindings = Vec::with_capacity(3); - let mut join_constraints = Vec::with_capacity(2); - if !flatten_inner_join_chain( - &view_select.from[0], - &mut table_bindings, - &mut join_constraints, - ) || table_bindings.len() < 2 - || join_constraints.len() + 1 != table_bindings.len() - { - return Ok(None); - } - - let mut table_schemas = Vec::with_capacity(table_bindings.len()); - for binding in &table_bindings { - if self - .visible_view(binding.name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(binding.name) - { - return Ok(None); - } - let Some(schema) = self.table_schema(binding.name) else { - return Ok(None); - }; - if !generated_columns_are_stored(schema) { - return Ok(None); - } - table_schemas.push(schema); - } - - let Some(source_table_index) = table_bindings - .iter() - .position(|binding| identifiers_equal(binding.binding_name(), filter_source_table)) - else { - return Ok(None); - }; - if source_table_index != 0 { - return Ok(None); - } - let Some(source_rowid_column) = row_id_alias_column_name(table_schemas[source_table_index]) - else { - return Ok(None); - }; - if !identifiers_equal(source_rowid_column, filter_source_column) { - return Ok(None); - } - - let mut projections = Vec::with_capacity(projection_columns.len()); - let mut column_names = Vec::with_capacity(projection_columns.len()); - for projection_column in projection_columns { - let Some(view_expr) = - view_projection_expr_for_output_column(&view_select.projection, projection_column) - else { - return Ok(None); - }; - let Expr::Column { - table: Some(base_table), - column: base_column, - } = view_expr - else { - return Ok(None); - }; - let Some(table_index) = table_bindings - .iter() - .position(|binding| identifiers_equal(binding.binding_name(), &base_table)) - else { - return Ok(None); - }; - let Some(column_index) = schema_column_index(table_schemas[table_index], &base_column) - else { - return Ok(None); - }; - projections.push(DeferredViewProjection { - table_index, - column_index, - is_rowid_alias: rowid_alias_column_index(table_schemas[table_index]) - == Some(column_index), - }); - column_names.push((*projection_column).to_string()); - } - - let mut join_steps = Vec::with_capacity(join_constraints.len()); - for current_table_index in 1..table_bindings.len() { - let Some(step) = self.deferred_view_join_step( - &table_bindings, - &table_schemas, - join_constraints[current_table_index - 1], - current_table_index, - )? - else { - return Ok(None); - }; - join_steps.push(step); - } - let table_projections = - build_deferred_view_table_projections(&table_schemas, &projections, &join_steps); - let projection_indexes = build_deferred_view_projection_indexes( - &projections, - &table_projections, - "simple view row-id projection", - )?; - let linear_tail_can_move = - deferred_view_linear_tail_projection_can_move(&projection_indexes); - - let mut join_keys = Vec::with_capacity(join_steps.len()); - let mut key_projection_indexes = Vec::with_capacity(join_steps.len()); - for step in &join_steps { - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&step.current_index_name) - else { - join_keys.clear(); - key_projection_indexes.clear(); - break; - }; - join_keys.push(keys); - key_projection_indexes.push(join_key_projection_index( - step, - &table_projections[step.previous_table_index], - )?); - } - - let mut table_readers = Vec::with_capacity(table_bindings.len()); - for (binding, schema) in table_bindings.iter().zip(table_schemas.iter()) { - if let Some(source) = self.visible_table_row_source(binding.name) { - table_readers.push(DeferredViewTableRowReader::Source(source)); - continue; - } - if resident_only && self.dirty_tables.contains(&schema.name) { - return Ok(None); - } - let Some(state) = self.persisted_table_state(binding.name) else { - return Ok(None); - }; - let cache = self - .catalog - .table(binding.name) - .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) - .map(|cache| cache.as_ref()); - if !deferred_rowid_lookup_available(state, schema, use_persistent_pk_index, cache) { - return Ok(None); - } - table_readers.push(DeferredViewTableRowReader::Deferred { - state, - schema, - paged_locator_cache: cache, - }); - } - - if resident_only { - return self.try_execute_observed_current_linear_three_table_view( - &table_readers, - &join_steps, - &join_keys, - &key_projection_indexes, - &table_projections, - &projection_indexes, - lookup_row_id, - column_names, - linear_tail_can_move, - ); - } - - let mut chunk_payload_cache = HashMap::new(); - let Some(source_row) = table_readers[source_table_index].read_projected_with_chunk_cache( - store, - lookup_row_id, - use_persistent_pk_index, - &table_projections[source_table_index].projection_indexes, - &mut chunk_payload_cache, - )? - else { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - }; - - let mut rows = Vec::with_capacity(64); - let mut join_partial_rows = Vec::with_capacity(join_steps.len() + 1); - if let Some(stopped) = self.stream_deferred_view_linear_three_table_rows_from_root( - store, - &table_readers, - &join_steps, - &join_keys, - &key_projection_indexes, - &table_projections, - &source_row, - use_persistent_pk_index, - &mut chunk_payload_cache, - &mut |root_row, row1, row2| { - let row = collect_deferred_view_query_row_from_linear_tail( - root_row, - row1, - row2, - &projection_indexes, - "simple view row-id projection", - linear_tail_can_move, - )?; - rows.push(row); - Ok(false) - }, - )? { - let _ = stopped; - return Ok(Some(QueryResult::with_rows(column_names, rows))); - } - - match self.stream_deferred_view_join_rows_from_root( - store, - &table_readers, - &join_steps, - &join_keys, - &key_projection_indexes, - &table_projections, - source_row, - &mut join_partial_rows, - use_persistent_pk_index, - false, - &mut chunk_payload_cache, - &mut |partial| { - let values = collect_deferred_view_projection_values( - partial, - &projection_indexes, - "simple view row-id projection", - )?; - rows.push(QueryRow::new(values)); - Ok(false) - }, - )? { - Some(_) => Ok(Some(QueryResult::with_rows(column_names, rows))), - None => Ok(None), - } - } - - pub(crate) fn execute_resolved_simple_ordered_row_id_projection( - &self, - request: ResolvedSimpleOrderedRowIdProjectionRequest<'_>, - ) -> Result> { - if self - .visible_view(request.table_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(request.table_name) - { - return Ok(None); - } - let Some(table_schema) = self.table_schema(request.table_name) else { - return Ok(None); - }; - if !generated_columns_are_stored(table_schema) - || request - .projection_indexes - .iter() - .any(|index| *index >= table_schema.columns.len()) - { - return Ok(None); - } - let Some(order_index) = schema_column_index(table_schema, request.order_column) else { - return Ok(None); - }; - if !row_id_alias_column_name(table_schema) - .is_some_and(|column_name| identifiers_equal(column_name, request.order_column)) - || table_schema.columns[order_index].column_type != ColumnType::Int64 - { - return Ok(None); - } - if request.limit == Some(0) { - return Ok(Some(QueryResult::with_shared_columns( - request.column_names, - Vec::new(), - ))); - } - let Some(row_source) = self.visible_table_row_source(table_schema.name.as_str()) else { - return Ok(None); - }; - let take = request.limit.unwrap_or(usize::MAX); - let row_ids = if let Some(row_ids) = self.ordered_runtime_btree_row_ids( - table_schema.name.as_str(), - request.order_column, - request.limit, - request.offset, - request.descending, - )? { - row_ids - } else { - let mut ordered_row_ids = Vec::with_capacity(row_source.row_count()); - for stored_row in row_source.rows() { - ordered_row_ids.push(stored_row?.row_id()); - } - ordered_row_ids.sort_unstable(); - if request.descending { - ordered_row_ids.reverse(); - } - ordered_row_ids - .into_iter() - .skip(request.offset) - .take(take) - .collect() - }; - let mut rows = Vec::with_capacity(row_ids.len().min(64)); - for row_id in row_ids { - if let Some(row) = - row_source.projected_query_row_by_id(row_id, request.projection_indexes)? - { - rows.push(row); - } - } - Ok(Some(QueryResult::with_shared_columns( - request.column_names, - rows, - ))) - } - - pub(crate) fn execute_resolved_simple_row_id_projection_at_snapshot( - &self, - request: ResolvedSimpleRowIdProjectionRequest<'_>, - ) -> Result> { - if self - .visible_view(request.table_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(request.table_name) - { - return Ok(None); - } - let Some(table_schema) = self.table_schema(request.table_name) else { - return Ok(None); - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - if request - .projection_indexes - .iter() - .any(|index| *index >= table_schema.columns.len()) - { - return Ok(None); - } - self.execute_validated_simple_row_id_projection_at_snapshot( - ValidatedSimpleRowIdProjectionRequest { - table_schema, - projection_indexes: request.projection_indexes, - column_names: Arc::clone(&request.column_names), - lookup_row_id: request.lookup_row_id, - pager: request.pager, - wal: request.wal, - snapshot_lsn: request.snapshot_lsn, - use_persistent_pk_index: request.use_persistent_pk_index, - }, - ) - } - - pub(crate) fn execute_resolved_simple_row_id_range_projection_at_snapshot( - &self, - request: ResolvedSimpleRowIdRangeProjectionRequest<'_>, - ) -> Result> { - if self - .visible_view(request.table_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(request.table_name) - { - return Ok(None); - } - let Some(table_schema) = self.table_schema(request.table_name) else { - return Ok(None); - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - if request - .projection_indexes - .iter() - .any(|index| *index >= table_schema.columns.len()) - { - return Ok(None); - } - let Some(filter_column_index) = schema_column_index(table_schema, request.filter_column) - else { - return Ok(None); - }; - if !table_schema - .primary_key_columns - .iter() - .any(|column| identifiers_equal(column, request.filter_column)) - || table_schema.columns[filter_column_index].column_type != ColumnType::Int64 - { - return Ok(None); - } - - let canonical_table_name = table_schema.name.as_str(); - let no_alias = None; - if let Some(row_source) = self.visible_table_row_source(canonical_table_name) { - return self.try_simple_rowid_range_projection_result( - row_source, - table_schema, - TableBindingRef { - name: canonical_table_name, - alias: &no_alias, - }, - request.filter_column, - request.lower_bound.as_ref(), - request.upper_bound.as_ref(), - request.projection_indexes, - request.column_names.to_vec(), - &[], - request.limit, - 0, - ); - } - - if !self.has_deferred_tables() - || !self - .deferred_table_names() - .any(|candidate| identifiers_equal(candidate, canonical_table_name)) - { - return Ok(None); - } - let Some(state) = self.persisted_table_state(canonical_table_name) else { - return Ok(None); - }; - let store = SnapshotPageStore { - pager: request.pager, - wal: request.wal, - snapshot_lsn: request.snapshot_lsn, - }; - let paged_locator_cache = self - .catalog - .table(canonical_table_name) - .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) - .map(|cache| cache.as_ref()); - self.try_simple_deferred_rowid_range_projection_result( - &store, - state, - table_schema, - TableBindingRef { - name: canonical_table_name, - alias: &no_alias, - }, - request.filter_column, - request.lower_bound.as_ref(), - request.upper_bound.as_ref(), - request.projection_indexes, - request.column_names.to_vec(), - &[], - request.limit, - 0, - request.use_persistent_pk_index, - paged_locator_cache, - ) - } - - pub(crate) fn execute_resolved_simple_row_id_join_projection_at_snapshot( - &self, - request: ResolvedSimpleRowIdJoinProjectionRequest<'_>, - ) -> Result> { - if self - .visible_view(request.left_table_name, NameResolutionScope::Session) - .is_some() - || self - .visible_view(request.right_table_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(request.left_table_name) - || self.visible_table_is_temporary(request.right_table_name) - { - return Ok(None); - } - let Some(left_schema) = self.table_schema(request.left_table_name) else { - return Ok(None); - }; - let Some(right_schema) = self.table_schema(request.right_table_name) else { - return Ok(None); - }; - if !generated_columns_are_stored(left_schema) || !generated_columns_are_stored(right_schema) - { - return Ok(None); - } - if request - .projections - .iter() - .any(|projection| match projection.side { - SimpleJoinProjectionSide::Left => { - projection.index >= request.left_projection_indexes.len() - } - SimpleJoinProjectionSide::Right => { - projection.index >= request.right_projection_indexes.len() - } - }) - || request - .left_projection_indexes - .iter() - .any(|index| *index >= left_schema.columns.len()) - || request - .right_projection_indexes - .iter() - .any(|index| *index >= right_schema.columns.len()) - { - return Ok(None); - } - - let left_name = left_schema.name.as_str(); - let right_name = right_schema.name.as_str(); - if let (Some(left_source), Some(right_source)) = ( - self.visible_table_row_source(left_name), - self.visible_table_row_source(right_name), - ) { - let Some(left_row) = left_source.row_by_id(request.lookup_row_id)? else { - return Ok(Some(QueryResult::with_shared_columns( - Arc::clone(&request.column_names), - Vec::new(), - ))); - }; - let Some(right_row) = right_source.row_by_id(request.lookup_row_id)? else { - return Ok(Some(QueryResult::with_shared_columns( - Arc::clone(&request.column_names), - Vec::new(), - ))); - }; - let row = project_resolved_simple_join_row_from_full_values( - request.projections, - request.left_projection_indexes, - request.right_projection_indexes, - left_row.values(), - right_row.values(), - )?; - return Ok(Some(QueryResult::with_shared_columns( - Arc::clone(&request.column_names), - vec![row], - ))); - } - - let Some(left_state) = self.persisted_table_state(left_name) else { - return Ok(None); - }; - let Some(right_state) = self.persisted_table_state(right_name) else { - return Ok(None); - }; - let left_cache = self - .catalog - .table(left_name) - .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) - .map(|cache| cache.as_ref()); - let right_cache = self - .catalog - .table(right_name) - .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) - .map(|cache| cache.as_ref()); - if !deferred_rowid_lookup_available( - left_state, - left_schema, - request.use_persistent_pk_index, - left_cache, - ) || !deferred_rowid_lookup_available( - right_state, - right_schema, - request.use_persistent_pk_index, - right_cache, - ) { - return Ok(None); - } - - let store = SnapshotPageStore { - pager: request.pager, - wal: request.wal, - snapshot_lsn: request.snapshot_lsn, - }; - let Some(left_values) = read_deferred_projected_values_by_id( - &store, - left_state, - left_schema, - request.lookup_row_id, - request.use_persistent_pk_index, - left_cache, - request.left_projection_indexes, - )? - else { - return Ok(Some(QueryResult::with_shared_columns( - Arc::clone(&request.column_names), - Vec::new(), - ))); - }; - let Some(right_values) = read_deferred_projected_values_by_id( - &store, - right_state, - right_schema, - request.lookup_row_id, - request.use_persistent_pk_index, - right_cache, - request.right_projection_indexes, - )? - else { - return Ok(Some(QueryResult::with_shared_columns( - Arc::clone(&request.column_names), - Vec::new(), - ))); - }; - let row = - project_resolved_simple_join_row(request.projections, &left_values, &right_values)?; - Ok(Some(QueryResult::with_shared_columns( - Arc::clone(&request.column_names), - vec![row], - ))) - } - - fn execute_validated_simple_row_id_projection_at_snapshot( - &self, - request: ValidatedSimpleRowIdProjectionRequest<'_>, - ) -> Result> { - let table_schema = request.table_schema; - let canonical_table_name = table_schema.name.as_str(); - if let Some(result) = self.try_execute_validated_resident_simple_row_id_projection( - table_schema, - request.projection_indexes, - Arc::clone(&request.column_names), - request.lookup_row_id, - )? { - return Ok(Some(result)); - } - - if !self.has_deferred_tables() - || !self - .deferred_table_names() - .any(|candidate| identifiers_equal(candidate, canonical_table_name)) - { - return Ok(None); - } - let Some(state) = self.persisted_table_state(canonical_table_name) else { - return Ok(None); - }; - let paged_locator_cache = self - .catalog - .table(canonical_table_name) - .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) - .map(|cache| cache.as_ref()); - let store = SnapshotPageStore { - pager: request.pager, - wal: request.wal, - snapshot_lsn: request.snapshot_lsn, - }; - let rows = read_deferred_projected_values_by_id( - &store, - state, - table_schema, - request.lookup_row_id, - request.use_persistent_pk_index, - paged_locator_cache, - request.projection_indexes, - )? - .map(|values| vec![QueryRow::new(values)]) - .unwrap_or_default(); - Ok(Some(QueryResult::with_shared_columns( - Arc::clone(&request.column_names), - rows, - ))) - } - - fn try_execute_validated_resident_simple_row_id_projection( - &self, - table_schema: &TableSchema, - projection_indexes: &[usize], - column_names: Arc<[String]>, - lookup_row_id: i64, - ) -> Result> { - let canonical_table_name = table_schema.name.as_str(); - if let Some(row_source) = self.visible_table_row_source(canonical_table_name) { - let projects_complete_row = projection_indexes.len() == table_schema.columns.len() - && projection_indexes - .iter() - .enumerate() - .all(|(position, index)| position == *index); - let row = if projects_complete_row { - row_source.full_query_row_by_id(lookup_row_id)? - } else { - row_source.projected_query_row_by_id(lookup_row_id, projection_indexes)? - }; - let rows = row.map(|row| vec![row]).unwrap_or_default(); - return Ok(Some(QueryResult::with_shared_columns(column_names, rows))); - } - - // A checkpoint may re-defer a paged table while retaining its compact - // row locator directory and a bounded set of already-verified chunk - // payloads. Those immutable payloads are part of this runtime's - // observed snapshot, so a point lookup can be answered without a - // pager read or a cross-process reader slot. If the requested chunk - // was not retained, preserve the normal snapshot-backed fallback. - if !self.dirty_tables.contains(canonical_table_name) { - let state = self.persisted_table_state(canonical_table_name); - let cache = self - .deferred_paged_row_locator_caches - .get(canonical_table_name); - if let (Some(state), Some(cache)) = (state, cache) { - if cache.matches_state(state) { - if let Some(cached) = cache.locators.get(lookup_row_id) { - if let Some(payload) = - cache.verified_payload(cached.pointer, cached.checksum) - { - let projects_complete_row = projection_indexes.len() - == table_schema.columns.len() - && projection_indexes - .iter() - .enumerate() - .all(|(position, index)| position == *index); - let row = if projects_complete_row { - decode_row_by_locator_from_payload( - payload, - lookup_row_id, - cached.locator, - ) - .map(|row| QueryRow::new(row.values))? - } else { - QueryRow::new(decode_projected_values_by_locator_from_payload::< - page::InMemoryPageStore, - >( - None, payload, cached.locator, projection_indexes - )?) - }; - return Ok(Some(QueryResult::with_shared_columns( - column_names, - vec![row], - ))); - } - } - } - } - } - Ok(None) - } - - fn try_execute_simple_deferred_rowid_join_projection_query( - &self, - query: &Query, - params: &[Value], - pager: &PagerHandle, - wal: &WalHandle, - snapshot_lsn: u64, - use_persistent_pk_index: bool, - ) -> Result> { - if !query.ctes.is_empty() || !query.order_by.is_empty() || query.limit.is_some() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if !select.group_by.is_empty() - || select.having.is_some() - || select.distinct - || !select.distinct_on.is_empty() - || projection_has_aggregate_items(&select.projection) - || select.from.len() != 1 - { - return Ok(None); - } - let Some(filter) = select.filter.as_ref() else { - return Ok(None); - }; - let FromItem::Join { - left, - right, - kind: JoinKind::Inner, - constraint, - } = &select.from[0] - else { - return Ok(None); - }; - let FromItem::Table { - name: left_name, - alias: left_alias, - } = &**left - else { - return Ok(None); - }; - let FromItem::Table { - name: right_name, - alias: right_alias, - } = &**right - else { - return Ok(None); - }; - if self - .visible_view(left_name, NameResolutionScope::Session) - .is_some() - || self - .visible_view(right_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(left_name) - || self.visible_table_is_temporary(right_name) - { - return Ok(None); - } - - let left_schema = match self.table_schema(left_name) { - Some(table) => table, - None => return Ok(None), - }; - let right_schema = match self.table_schema(right_name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(left_schema) || !generated_columns_are_stored(right_schema) - { - return Ok(None); - } - let left_binding = TableBindingRef { - name: left_name, - alias: left_alias, - }; - let right_binding = TableBindingRef { - name: right_name, - alias: right_alias, - }; - let Some(join_equalities) = simple_indexed_join_constraint_equalities( - constraint, - left_binding, - right_binding, - left_schema, - right_schema, - ) else { - return Ok(None); - }; - let Some((left_join_columns, right_join_columns)) = - orient_join_equalities(&join_equalities, left_binding, right_binding) - else { - return Ok(None); - }; - if left_join_columns.len() != 1 || right_join_columns.len() != 1 { - return Ok(None); - } - let Some(left_rowid_column) = crate::exec::dml::row_id_alias_column_name(left_schema) - else { - return Ok(None); - }; - let Some(right_rowid_column) = crate::exec::dml::row_id_alias_column_name(right_schema) - else { - return Ok(None); - }; - if !identifiers_equal(left_join_columns[0], left_rowid_column) - || !identifiers_equal(right_join_columns[0], right_rowid_column) - { - return Ok(None); - } - - let Some((filter_table, filter_column, value_expr)) = simple_btree_lookup(filter) else { - return Ok(None); - }; - let filter_value = self.eval_expr( - value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?; - let Value::Int64(source_row_id) = filter_value else { - return Ok(None); - }; - - let source_is_left = if matches_table_binding(left_binding, filter_table) - && identifiers_equal(filter_column, left_rowid_column) - { - true - } else if matches_table_binding(right_binding, filter_table) - && identifiers_equal(filter_column, right_rowid_column) - { - false - } else { - return Ok(None); - }; - let using_join_columns = - simple_indexed_join_using_columns(constraint, left_schema, right_schema); - let join_eval_bindings = simple_join_projection_eval_bindings( - left_name, - left_alias, - left_schema, - right_name, - right_alias, - right_schema, - ); - let join_eval_dataset = Dataset::with_rows(join_eval_bindings, Vec::new()); - let Some((projection_plan, column_names)) = simple_join_projection_plan( - &select.projection, - &join_eval_dataset, - left_name, - left_alias, - left_schema, - right_name, - right_alias, - right_schema, - &using_join_columns, - ) else { - return Ok(None); - }; - - let source_join_column = if source_is_left { - left_join_columns[0] - } else { - right_join_columns[0] - }; - if let (Some(left_source), Some(right_source)) = ( - self.visible_table_row_source(left_name), - self.visible_table_row_source(right_name), - ) { - let (source_source, source_schema, probe_source) = if source_is_left { - (left_source, left_schema, right_source) - } else { - (right_source, right_schema, left_source) - }; - let Some(source_row) = source_source.row_by_id(source_row_id)? else { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - }; - let Some(source_join_index) = schema_column_index(source_schema, source_join_column) - else { - return Ok(None); - }; - let Some(Value::Int64(probe_row_id)) = source_row.values().get(source_join_index) - else { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - }; - let Some(probe_row) = probe_source.row_by_id(*probe_row_id)? else { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - }; - let left_values = if source_is_left { - source_row.values() - } else { - probe_row.values() - }; - let right_values = if source_is_left { - probe_row.values() - } else { - source_row.values() - }; - let row = project_simple_join_row( - self, - &projection_plan, - &join_eval_dataset, - Some(left_values), - left_schema.columns.len(), - Some(right_values), - right_schema.columns.len(), - params, - )?; - return Ok(Some(QueryResult::with_rows( - column_names, - vec![QueryRow::new(row)], - ))); - } - - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - let Some(left_state) = self.persisted_table_state(left_name) else { - return Ok(None); - }; - let Some(right_state) = self.persisted_table_state(right_name) else { - return Ok(None); - }; - let left_cache = self - .catalog - .table(left_name) - .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) - .map(|cache| cache.as_ref()); - let right_cache = self - .catalog - .table(right_name) - .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) - .map(|cache| cache.as_ref()); - if !deferred_rowid_lookup_available( - left_state, - left_schema, - use_persistent_pk_index, - left_cache, - ) || !deferred_rowid_lookup_available( - right_state, - right_schema, - use_persistent_pk_index, - right_cache, - ) { - return Ok(None); - } - - let (source_state, source_schema, source_cache, probe_state, probe_schema, probe_cache) = - if source_is_left { - ( - left_state, - left_schema, - left_cache, - right_state, - right_schema, - right_cache, - ) - } else { - ( - right_state, - right_schema, - right_cache, - left_state, - left_schema, - left_cache, - ) - }; - let Some(source_row) = read_deferred_stored_row_by_id( - &store, - source_state, - source_schema, - source_row_id, - use_persistent_pk_index, - source_cache, - )? - else { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - }; - let Some(source_join_index) = schema_column_index(source_schema, source_join_column) else { - return Ok(None); - }; - let join_value = source_row.values.get(source_join_index); - let Some(Value::Int64(probe_row_id)) = join_value else { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - }; - let Some(probe_row) = read_deferred_stored_row_by_id( - &store, - probe_state, - probe_schema, - *probe_row_id, - use_persistent_pk_index, - probe_cache, - )? - else { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - }; - - let left_values = if source_is_left { - source_row.values.as_slice() - } else { - probe_row.values.as_slice() - }; - let right_values = if source_is_left { - probe_row.values.as_slice() - } else { - source_row.values.as_slice() - }; - let row = project_simple_join_row( - self, - &projection_plan, - &join_eval_dataset, - Some(left_values), - left_schema.columns.len(), - Some(right_values), - right_schema.columns.len(), - params, - )?; - Ok(Some(QueryResult::with_rows( - column_names, - vec![QueryRow::new(row)], - ))) - } - - #[allow(clippy::too_many_arguments)] - fn try_execute_simple_deferred_view_projection_limit_query( - &self, - query: &Query, - params: &[Value], - pager: &PagerHandle, - wal: &WalHandle, - snapshot_lsn: u64, - use_persistent_pk_index: bool, - ) -> Result> { - if query.recursive || !query.ctes.is_empty() || !query.order_by.is_empty() { - return Ok(None); - } - let Some(limit_expr) = query.limit.as_ref() else { - return Ok(None); - }; - let ctes = BTreeMap::new(); - let limit = usize::try_from(self.eval_constant_i64(limit_expr, params, &ctes)?.max(0)) - .unwrap_or(usize::MAX); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &ctes)) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || select.filter.is_some() - || !select.group_by.is_empty() - || select.having.is_some() - || projection_has_aggregate_items(&select.projection) - || select.from.len() != 1 - { - return Ok(None); - } - let FromItem::Table { - name: view_name, - alias: view_alias, - } = &select.from[0] - else { - return Ok(None); - }; - let Some(view) = self.visible_view(view_name, NameResolutionScope::Session) else { - return Ok(None); - }; - if view.temporary { - return Ok(None); - } - let view_binding = view_alias.as_deref().unwrap_or(view_name.as_str()); - let view_query = self.cached_view_query(view)?; - if view_query.recursive - || !view_query.ctes.is_empty() - || !view_query.order_by.is_empty() - || view_query.limit.is_some() - || view_query.offset.is_some() - { - return Ok(None); - } - let QueryBody::Select(view_select) = &view_query.body else { - return Ok(None); - }; - if view_select.distinct - || !view_select.distinct_on.is_empty() - || !view_select.group_by.is_empty() - || view_select.having.is_some() - || view_select.filter.is_some() - || projection_has_aggregate_items(&view_select.projection) - || view_select.from.len() != 1 - { - return Ok(None); - } - - let mut table_bindings = Vec::with_capacity(3); - let mut join_constraints = Vec::with_capacity(2); - if !flatten_inner_join_chain( - &view_select.from[0], - &mut table_bindings, - &mut join_constraints, - ) || !(2..=3).contains(&table_bindings.len()) - || join_constraints.len() + 1 != table_bindings.len() - { - return Ok(None); - } - - let mut table_schemas = Vec::with_capacity(table_bindings.len()); - for binding in &table_bindings { - if self - .visible_view(binding.name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(binding.name) - { - return Ok(None); - } - let Some(schema) = self.table_schema(binding.name) else { - return Ok(None); - }; - if !generated_columns_are_stored(schema) { - return Ok(None); - } - table_schemas.push(schema); - } - let deferred_row_count = table_bindings - .iter() - .filter(|binding| self.visible_table_row_source(binding.name).is_none()) - .filter_map(|binding| self.persisted_table_state(binding.name)) - .map(|state| state.row_count) - .sum::(); - if deferred_row_count < DEFERRED_VIEW_LIMIT_MIN_PERSISTED_ROWS { - return Ok(None); - } - - let mut projections = Vec::with_capacity(select.projection.len()); - let mut column_names = Vec::with_capacity(select.projection.len()); - for (ordinal, item) in select.projection.iter().enumerate() { - let SelectItem::Expr { expr, alias } = item else { - return Ok(None); - }; - let Expr::Column { - table: outer_table, - column: outer_column, - } = expr - else { - return Ok(None); - }; - if outer_table - .as_deref() - .is_some_and(|qualifier| !identifiers_equal(qualifier, view_binding)) - { - return Ok(None); - } - let Some(view_expr) = - view_projection_expr_for_output_column(&view_select.projection, outer_column) - else { - return Ok(None); - }; - let Expr::Column { - table: Some(base_table), - column: base_column, - } = view_expr - else { - return Ok(None); - }; - let Some(table_index) = table_bindings - .iter() - .position(|binding| identifiers_equal(binding.binding_name(), &base_table)) - else { - return Ok(None); - }; - let Some(column_index) = schema_column_index(table_schemas[table_index], &base_column) - else { - return Ok(None); - }; - let is_rowid_alias = - rowid_alias_column_index(table_schemas[table_index]) == Some(column_index); - projections.push(DeferredViewProjection { - table_index, - column_index, - is_rowid_alias, - }); - column_names.push( - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, ordinal + 1)), - ); - } - if limit == 0 { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - } - - let mut join_steps = Vec::with_capacity(join_constraints.len()); - for current_table_index in 1..table_bindings.len() { - let Some(step) = self.deferred_view_join_step( - &table_bindings, - &table_schemas, - join_constraints[current_table_index - 1], - current_table_index, - )? - else { - return Ok(None); - }; - join_steps.push(step); - } - let table_projections = - build_deferred_view_table_projections(&table_schemas, &projections, &join_steps); - let projection_indexes = build_deferred_view_projection_indexes( - &projections, - &table_projections, - "deferred view limit projection", - )?; - let linear_tail_can_move = - deferred_view_linear_tail_projection_can_move(&projection_indexes); - let mut join_keys = Vec::with_capacity(join_steps.len()); - let mut key_projection_indexes = Vec::with_capacity(join_steps.len()); - for step in &join_steps { - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&step.current_index_name) - else { - return Ok(None); - }; - join_keys.push(keys); - key_projection_indexes.push(join_key_projection_index( - step, - &table_projections[step.previous_table_index], - )?); - } - - let mut table_readers = Vec::with_capacity(table_bindings.len()); - for (binding, schema) in table_bindings.iter().zip(table_schemas.iter()) { - if let Some(source) = self.visible_table_row_source(binding.name) { - table_readers.push(DeferredViewTableRowReader::Source(source)); - continue; - } - let Some(state) = self.persisted_table_state(binding.name) else { - return Ok(None); - }; - let cache = self - .catalog - .table(binding.name) - .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) - .map(|cache| cache.as_ref()); - if !deferred_rowid_lookup_available(state, schema, use_persistent_pk_index, cache) { - return Ok(None); - } - table_readers.push(DeferredViewTableRowReader::Deferred { - state, - schema, - paged_locator_cache: cache, - }); - } - - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - let mut chunk_payload_cache = HashMap::new(); - let mut rows = Vec::with_capacity(limit); - let mut join_partial_rows = Vec::with_capacity(join_steps.len() + 1); - let mut offset_remaining = offset; - let mut limit_remaining = limit; - if let DeferredViewTableRowReader::Source(source) = &table_readers[0] { - for root_row in source.rows() { - let root_row = root_row?; - let values = if table_projections[0].projection_indexes.is_empty() { - Vec::new() - } else { - project_simple_projection_value_vec( - root_row.values(), - &table_projections[0].projection_indexes, - ) - }; - let root_row = StoredRow { - row_id: root_row.row_id(), - values, - }; - if self.push_deferred_view_limit_rows_from_root( - &store, - &table_readers, - &join_steps, - &join_keys, - &key_projection_indexes, - &table_projections, - &projection_indexes, - root_row, - &mut offset_remaining, - &mut limit_remaining, - &mut rows, - &mut join_partial_rows, - &mut chunk_payload_cache, - use_persistent_pk_index, - linear_tail_can_move, - )? { - break; - } - } - } else { - let DeferredViewTableRowReader::Deferred { state, .. } = &table_readers[0] else { - return Ok(None); - }; - visit_persisted_table_projected_values_until( - &store, - *state, - &table_projections[0].projection_indexes, - |row_id, root_values| { - let root_row = StoredRow { - row_id, - values: root_values.to_vec(), - }; - self.push_deferred_view_limit_rows_from_root( - &store, - &table_readers, - &join_steps, - &join_keys, - &key_projection_indexes, - &table_projections, - &projection_indexes, - root_row, - &mut offset_remaining, - &mut limit_remaining, - &mut rows, - &mut join_partial_rows, - &mut chunk_payload_cache, - use_persistent_pk_index, - linear_tail_can_move, - ) - }, - )?; - } - - Ok(Some(QueryResult::with_rows(column_names, rows))) - } - - #[allow(clippy::too_many_arguments)] - fn try_execute_simple_deferred_view_filter_projection_query( - &self, - query: &Query, - params: &[Value], - pager: &PagerHandle, - wal: &WalHandle, - snapshot_lsn: u64, - use_persistent_pk_index: bool, - ) -> Result> { - if query.recursive - || !query.ctes.is_empty() - || !query.order_by.is_empty() - || query.limit.is_some() - || query.offset.is_some() - { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.distinct - || !select.distinct_on.is_empty() - || !select.group_by.is_empty() - || select.having.is_some() - || projection_has_aggregate_items(&select.projection) - || select.from.len() != 1 - { - return Ok(None); - } - let Some(filter) = select.filter.as_ref() else { - return Ok(None); - }; - let FromItem::Table { - name: view_name, - alias: view_alias, - } = &select.from[0] - else { - return Ok(None); - }; - let Some(view) = self.visible_view(view_name, NameResolutionScope::Session) else { - return Ok(None); - }; - if view.temporary { - return Ok(None); - } - let view_binding = view_alias.as_deref().unwrap_or(view_name.as_str()); - let Some((filter_qualifier, filter_column, value_expr)) = simple_btree_lookup(filter) - else { - return Ok(None); - }; - if filter_qualifier.is_some_and(|qualifier| !identifiers_equal(qualifier, view_binding)) { - return Ok(None); - } - - let view_query = self.cached_view_query(view)?; - if view_query.recursive - || !view_query.ctes.is_empty() - || !view_query.order_by.is_empty() - || view_query.limit.is_some() - || view_query.offset.is_some() - { - return Ok(None); - } - let QueryBody::Select(view_select) = &view_query.body else { - return Ok(None); - }; - if view_select.distinct - || !view_select.distinct_on.is_empty() - || !view_select.group_by.is_empty() - || view_select.having.is_some() - || view_select.filter.is_some() - || projection_has_aggregate_items(&view_select.projection) - || view_select.from.len() != 1 - { - return Ok(None); - } - - let Some(filter_source_expr) = - view_projection_expr_for_output_column(&view_select.projection, filter_column) - else { - return Ok(None); - }; - let Expr::Column { - table: Some(filter_source_table), - column: filter_source_column, - } = &filter_source_expr - else { - return Ok(None); - }; - - let mut table_bindings = Vec::with_capacity(3); - let mut join_constraints = Vec::with_capacity(2); - if !flatten_inner_join_chain( - &view_select.from[0], - &mut table_bindings, - &mut join_constraints, - ) || table_bindings.len() < 2 - || join_constraints.len() + 1 != table_bindings.len() - { - return Ok(None); - } - - let mut table_schemas = Vec::with_capacity(table_bindings.len()); - for binding in &table_bindings { - if self - .visible_view(binding.name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(binding.name) - { - return Ok(None); - } - let Some(schema) = self.table_schema(binding.name) else { - return Ok(None); - }; - if !generated_columns_are_stored(schema) { - return Ok(None); - } - table_schemas.push(schema); - } - - let Some(source_table_index) = table_bindings - .iter() - .position(|binding| identifiers_equal(binding.binding_name(), filter_source_table)) - else { - return Ok(None); - }; - if source_table_index != 0 { - return Ok(None); - } - let Some(source_rowid_column) = row_id_alias_column_name(table_schemas[source_table_index]) - else { - return Ok(None); - }; - if !identifiers_equal(source_rowid_column, filter_source_column) { - return Ok(None); - } - - let mut projections = Vec::with_capacity(select.projection.len()); - let mut column_names = Vec::with_capacity(select.projection.len()); - for (ordinal, item) in select.projection.iter().enumerate() { - let SelectItem::Expr { expr, alias } = item else { - return Ok(None); - }; - let Expr::Column { - table: outer_table, - column: outer_column, - } = expr - else { - return Ok(None); - }; - if outer_table - .as_deref() - .is_some_and(|qualifier| !identifiers_equal(qualifier, view_binding)) - { - return Ok(None); - } - let Some(view_expr) = - view_projection_expr_for_output_column(&view_select.projection, outer_column) - else { - return Ok(None); - }; - let Expr::Column { - table: Some(base_table), - column: base_column, - } = view_expr - else { - return Ok(None); - }; - let Some(table_index) = table_bindings - .iter() - .position(|binding| identifiers_equal(binding.binding_name(), &base_table)) - else { - return Ok(None); - }; - let Some(column_index) = schema_column_index(table_schemas[table_index], &base_column) - else { - return Ok(None); - }; - let is_rowid_alias = - rowid_alias_column_index(table_schemas[table_index]) == Some(column_index); - projections.push(DeferredViewProjection { - table_index, - column_index, - is_rowid_alias, - }); - column_names.push( - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, ordinal + 1)), - ); - } - - let mut join_steps = Vec::with_capacity(join_constraints.len()); - for current_table_index in 1..table_bindings.len() { - let Some(step) = self.deferred_view_join_step( - &table_bindings, - &table_schemas, - join_constraints[current_table_index - 1], - current_table_index, - )? - else { - return Ok(None); - }; - join_steps.push(step); - } - let table_projections = - build_deferred_view_table_projections(&table_schemas, &projections, &join_steps); - let projection_indexes = build_deferred_view_projection_indexes( - &projections, - &table_projections, - "deferred view projection", - )?; - let linear_tail_can_move = - deferred_view_linear_tail_projection_can_move(&projection_indexes); - let mut join_keys = Vec::with_capacity(join_steps.len()); - let mut key_projection_indexes = Vec::with_capacity(join_steps.len()); - for step in &join_steps { - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&step.current_index_name) - else { - join_keys.clear(); - key_projection_indexes.clear(); - break; - }; - join_keys.push(keys); - key_projection_indexes.push(join_key_projection_index( - step, - &table_projections[step.previous_table_index], - )?); - } - - let mut table_readers = Vec::with_capacity(table_bindings.len()); - for (binding, schema) in table_bindings.iter().zip(table_schemas.iter()) { - if let Some(source) = self.visible_table_row_source(binding.name) { - table_readers.push(DeferredViewTableRowReader::Source(source)); - continue; - } - let Some(state) = self.persisted_table_state(binding.name) else { - return Ok(None); - }; - let cache = self - .catalog - .table(binding.name) - .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) - .map(|cache| cache.as_ref()); - if !deferred_rowid_lookup_available(state, schema, use_persistent_pk_index, cache) { - return Ok(None); - } - table_readers.push(DeferredViewTableRowReader::Deferred { - state, - schema, - paged_locator_cache: cache, - }); - } - - let source_row_id = match simple_int64_constant_expr_value(value_expr, params)? { - Some(value) => value, - None => { - let filter_value = self.eval_expr( - value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?; - let Value::Int64(value) = filter_value else { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - }; - value - } - }; - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - let mut chunk_payload_cache = HashMap::new(); - let Some(source_row) = table_readers[source_table_index].read_projected_with_chunk_cache( - &store, - source_row_id, - use_persistent_pk_index, - &table_projections[source_table_index].projection_indexes, - &mut chunk_payload_cache, - )? - else { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - }; - - let mut rows = Vec::with_capacity(64); - let mut join_partial_rows = Vec::with_capacity(join_steps.len() + 1); - if let Some(stopped) = self.stream_deferred_view_linear_three_table_rows_from_root( - &store, - &table_readers, - &join_steps, - &join_keys, - &key_projection_indexes, - &table_projections, - &source_row, - use_persistent_pk_index, - &mut chunk_payload_cache, - &mut |root_row, row1, row2| { - let row = collect_deferred_view_query_row_from_linear_tail( - root_row, - row1, - row2, - &projection_indexes, - "deferred view projection", - linear_tail_can_move, - )?; - rows.push(row); - Ok(false) - }, - )? { - let _ = stopped; - return Ok(Some(QueryResult::with_rows(column_names, rows))); - } - - match self.stream_deferred_view_join_rows_from_root( - &store, - &table_readers, - &join_steps, - &join_keys, - &key_projection_indexes, - &table_projections, - source_row, - &mut join_partial_rows, - use_persistent_pk_index, - false, - &mut chunk_payload_cache, - &mut |partial| { - let values = collect_deferred_view_projection_values( - partial, - &projection_indexes, - "deferred view projection", - )?; - rows.push(QueryRow::new(values)); - Ok(false) - }, - )? { - Some(_) => Ok(Some(QueryResult::with_rows(column_names, rows))), - None => Ok(None), - } - } - - #[allow(clippy::too_many_arguments)] - fn stream_deferred_view_linear_three_table_rows_from_root( - &self, - store: &S, - table_row_readers: &[DeferredViewTableRowReader<'_>], - join_steps: &[DeferredViewJoinStep], - join_keys: &[&RuntimeBtreeKeys], - key_projection_indexes: &[Option], - table_projections: &[DeferredViewTableProjection], - root_row: &StoredRow, - use_persistent_pk_index: bool, - chunk_payload_cache: &mut HashMap>>, - visit: &mut F, - ) -> Result> - where - F: FnMut(&StoredRow, &StoredRow, StoredRow) -> Result, - { - if table_row_readers.len() != 3 - || join_steps.len() != 2 - || table_projections.len() != 3 - || join_keys.len() != 2 - || key_projection_indexes.len() != 2 - { - return Ok(None); - } - let step0 = &join_steps[0]; - let step1 = &join_steps[1]; - if step0.previous_table_index != 0 - || step0.current_table_index != 1 - || step1.previous_table_index != 1 - || step1.current_table_index != 2 - { - return Ok(None); - } - let [keys0, keys1] = join_keys else { - return Ok(None); - }; - let [key0_projection_index, key1_projection_index] = key_projection_indexes else { - return Ok(None); - }; - let key0_row_ids = match *key0_projection_index { - Some(key0_projection_index) => { - let Some(key0_value) = root_row.values.get(key0_projection_index) else { - return Err(DbError::internal( - "deferred view linear join projection row is shorter than planned schema", - )); - }; - if matches!(key0_value, Value::Null) { - return Ok(Some(false)); - } - keys0.row_ids_for_value_set(key0_value)? - } - None => keys0.row_ids_for_row_id(root_row.row_id), - }; - - let stopped = key0_row_ids - .visit_until(|row1_id| { - let Some(row1) = table_row_readers[1].read_projected_with_chunk_cache( - store, - row1_id, - use_persistent_pk_index, - &table_projections[1].projection_indexes, - chunk_payload_cache, - )? - else { - return Ok(false); - }; - let key1_row_ids = match *key1_projection_index { - Some(key1_projection_index) => { - let Some(key1_value) = row1.values.get(key1_projection_index) else { - return Err(DbError::internal( - "deferred view linear join projection row is shorter than planned schema", - )); - }; - if matches!(key1_value, Value::Null) { - return Ok(false); - } - keys1.row_ids_for_value_set(key1_value)? - } - None => keys1.row_ids_for_row_id(row1.row_id), - }; - key1_row_ids - .visit_until(|row2_id| { - let Some(row2) = table_row_readers[2].read_projected_with_chunk_cache( - store, - row2_id, - use_persistent_pk_index, - &table_projections[2].projection_indexes, - chunk_payload_cache, - )? - else { - return Ok(false); - }; - visit(root_row, &row1, row2) - }) - })?; - Ok(Some(stopped)) - } - - #[allow(clippy::too_many_arguments)] - fn try_execute_observed_current_linear_three_table_view( - &self, - table_row_readers: &[DeferredViewTableRowReader<'_>], - join_steps: &[DeferredViewJoinStep], - join_keys: &[&RuntimeBtreeKeys], - key_projection_indexes: &[Option], - table_projections: &[DeferredViewTableProjection], - projection_indexes: &[DeferredViewProjectionSource], - lookup_row_id: i64, - column_names: Vec, - linear_tail_can_move: bool, - ) -> Result> { - if table_row_readers.len() != 3 - || join_steps.len() != 2 - || table_projections.len() != 3 - || join_keys.len() != 2 - || key_projection_indexes.len() != 2 - { - return Ok(None); - } - let step0 = &join_steps[0]; - let step1 = &join_steps[1]; - if step0.previous_table_index != 0 - || step0.current_table_index != 1 - || step1.previous_table_index != 1 - || step1.current_table_index != 2 - { - return Ok(None); - } - let [keys0, keys1] = join_keys else { - return Ok(None); - }; - let [key0_projection_index, key1_projection_index] = key_projection_indexes else { - return Ok(None); - }; - let Some(source_row) = table_row_readers[0].read_projected_from_observed_cache( - lookup_row_id, - &table_projections[0].projection_indexes, - )? - else { - return Ok(None); - }; - let Some(source_row) = source_row else { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - }; - - let key0_row_ids = match *key0_projection_index { - Some(projection_index) => { - let Some(key_value) = source_row.values.get(projection_index) else { - return Err(DbError::internal( - "observed-current view root row is shorter than planned schema", - )); - }; - if matches!(key_value, Value::Null) { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - } - keys0.row_ids_for_value_set(key_value)? - } - None => keys0.row_ids_for_row_id(source_row.row_id), - }; - - let mut cache_available = true; - let mut rows = Vec::with_capacity(64); - key0_row_ids.visit_until(|row1_id| { - let Some(row1) = table_row_readers[1].read_projected_from_observed_cache( - row1_id, - &table_projections[1].projection_indexes, - )? - else { - cache_available = false; - return Ok(true); - }; - let Some(row1) = row1 else { - return Ok(false); - }; - let key1_row_ids = match *key1_projection_index { - Some(projection_index) => { - let Some(key_value) = row1.values.get(projection_index) else { - return Err(DbError::internal( - "observed-current view join row is shorter than planned schema", - )); - }; - if matches!(key_value, Value::Null) { - return Ok(false); - } - keys1.row_ids_for_value_set(key_value)? - } - None => keys1.row_ids_for_row_id(row1.row_id), - }; - key1_row_ids.visit_until(|row2_id| { - let Some(row2) = table_row_readers[2].read_projected_from_observed_cache( - row2_id, - &table_projections[2].projection_indexes, - )? - else { - cache_available = false; - return Ok(true); - }; - let Some(row2) = row2 else { - return Ok(false); - }; - rows.push(collect_deferred_view_query_row_from_linear_tail( - &source_row, - &row1, - row2, - projection_indexes, - "observed-current view row-id projection", - linear_tail_can_move, - )?); - Ok(false) - }) - })?; - if !cache_available { - return Ok(None); - } - Ok(Some(QueryResult::with_rows(column_names, rows))) - } - - fn deferred_view_join_step( - &self, - table_bindings: &[TableBindingRef<'_>], - table_schemas: &[&TableSchema], - constraint: &Expr, - current_table_index: usize, - ) -> Result> { - let Some(equalities) = simple_join_equalities(constraint) else { - return Ok(None); - }; - let current_binding = table_bindings[current_table_index]; - let mut matched = None; - for (left_ref, right_ref) in equalities { - for (previous_ref, current_ref) in [(left_ref, right_ref), (right_ref, left_ref)] { - if !matches_table_binding(current_binding, current_ref.table) { - continue; - } - let Some(previous_table_index) = table_bindings[..current_table_index] - .iter() - .position(|binding| matches_table_binding(*binding, previous_ref.table)) - else { - continue; - }; - let Some(previous_column_index) = - schema_column_index(table_schemas[previous_table_index], previous_ref.column) - else { - return Ok(None); - }; - let previous_is_rowid_alias = - rowid_alias_column_index(table_schemas[previous_table_index]) - == Some(previous_column_index); - let Some(current_index) = self - .simple_btree_index_for_table_column(current_binding.name, current_ref.column) - else { - return Ok(None); - }; - if matched - .replace(DeferredViewJoinStep { - previous_table_index, - previous_column_index, - current_table_index, - current_index_name: current_index.name.clone(), - previous_is_rowid_alias, - }) - .is_some() - { - return Ok(None); - } - if schema_column_index(table_schemas[current_table_index], current_ref.column) - .is_none() - { - return Err(DbError::internal( - "deferred view join current column is missing from schema", - )); - } - } - } - Ok(matched) - } - - fn simple_btree_index_for_table_column( - &self, - table_name: &str, - column_name: &str, - ) -> Option<&IndexSchema> { - self.catalog.indexes.values().find(|index| { - identifiers_equal(&index.table_name, table_name) - && index.fresh - && index.kind == IndexKind::Btree - && index.predicate_sql.is_none() - && index.columns.len() == 1 - && index.columns[0].expression_sql.is_none() - && index.columns[0] - .column_name - .as_deref() - .is_some_and(|indexed_column| identifiers_equal(indexed_column, column_name)) - && matches!(self.index(&index.name), Some(RuntimeIndex::Btree { .. })) - }) - } - - #[allow(clippy::too_many_arguments)] - fn push_deferred_view_limit_rows_from_root( - &self, - store: &S, - table_row_readers: &[DeferredViewTableRowReader<'_>], - join_steps: &[DeferredViewJoinStep], - join_keys: &[&RuntimeBtreeKeys], - key_projection_indexes: &[Option], - table_projections: &[DeferredViewTableProjection], - projection_indexes: &[DeferredViewProjectionSource], - root_row: StoredRow, - offset_remaining: &mut usize, - limit_remaining: &mut usize, - rows: &mut Vec, - partial_rows: &mut Vec, - chunk_payload_cache: &mut HashMap>>, - use_persistent_pk_index: bool, - linear_tail_can_move: bool, - ) -> Result { - if let Some(stopped) = self.stream_deferred_view_linear_three_table_rows_from_root( - store, - table_row_readers, - join_steps, - join_keys, - key_projection_indexes, - table_projections, - &root_row, - use_persistent_pk_index, - chunk_payload_cache, - &mut |root_row, row1, row2| { - if *offset_remaining > 0 { - *offset_remaining -= 1; - return Ok(false); - } - if *limit_remaining == 0 { - return Ok(true); - } - let row = collect_deferred_view_query_row_from_linear_tail( - root_row, - row1, - row2, - projection_indexes, - "deferred view limit projection", - linear_tail_can_move, - )?; - rows.push(row); - *limit_remaining = (*limit_remaining).saturating_sub(1); - Ok(*limit_remaining == 0) - }, - )? { - return Ok(stopped); - } - - let Some(stopped) = self.stream_deferred_view_join_rows_from_root( - store, - table_row_readers, - join_steps, - join_keys, - key_projection_indexes, - table_projections, - root_row, - partial_rows, - use_persistent_pk_index, - true, - chunk_payload_cache, - &mut |partial| { - if *offset_remaining > 0 { - *offset_remaining -= 1; - return Ok(false); - } - if *limit_remaining == 0 { - return Ok(true); - } - let values = collect_deferred_view_projection_values( - partial, - projection_indexes, - "deferred view limit projection", - )?; - rows.push(QueryRow::new(values)); - *limit_remaining = (*limit_remaining).saturating_sub(1); - Ok(*limit_remaining == 0) - }, - )? - else { - return Err(DbError::internal( - "index was unavailable while executing deferred view limit join", - )); - }; - Ok(stopped) - } - - #[allow(clippy::too_many_arguments)] - fn stream_deferred_view_join_rows_from_root( - &self, - store: &S, - table_row_readers: &[DeferredViewTableRowReader<'_>], - join_steps: &[DeferredViewJoinStep], - join_keys: &[&RuntimeBtreeKeys], - key_projection_indexes: &[Option], - table_projections: &[DeferredViewTableProjection], - root_row: StoredRow, - partial_rows: &mut Vec, - use_persistent_pk_index: bool, - require_index: bool, - chunk_payload_cache: &mut HashMap>>, - visit: &mut F, - ) -> Result> - where - F: FnMut(&[StoredRow]) -> Result, - { - #[allow(clippy::too_many_arguments)] - fn walk_join_rows( - store: &S, - table_row_readers: &[DeferredViewTableRowReader<'_>], - join_steps: &[DeferredViewJoinStep], - table_projections: &[DeferredViewTableProjection], - join_keys: &[&RuntimeBtreeKeys], - key_projection_indexes: &[Option], - step_index: usize, - partial_rows: &mut Vec, - use_persistent_pk_index: bool, - chunk_payload_cache: &mut HashMap>>, - visit: &mut F, - ) -> Result> - where - F: FnMut(&[StoredRow]) -> Result, - { - if step_index == join_steps.len() { - return Ok(Some(visit(partial_rows.as_slice())?)); - } - - let step = &join_steps[step_index]; - let keys = join_keys[step_index]; - let Some(previous_row) = partial_rows.get(step.previous_table_index) else { - return Err(DbError::internal( - "deferred view limit join row is shorter than the planned schema", - )); - }; - let current_table_index = step.current_table_index; - let current_projection_indexes = - &table_projections[current_table_index].projection_indexes; - - let mut outcome = None; - let row_ids = match key_projection_indexes[step_index] { - Some(projection_index) => { - let Some(key_value) = previous_row.values.get(projection_index) else { - return Err(DbError::internal( - "deferred view join row is shorter than planned schema", - )); - }; - if matches!(key_value, Value::Null) { - return Ok(Some(false)); - } - keys.row_ids_for_value_set(key_value)? - } - None => keys.row_ids_for_row_id(previous_row.row_id), - }; - - let mut visit_row_id = |row_id| -> Result> { - if partial_rows.len() != step.current_table_index { - return Err(DbError::internal( - "deferred view join row is not in expected table order", - )); - } - - let Some(joined_row) = table_row_readers[current_table_index] - .read_projected_with_chunk_cache( - store, - row_id, - use_persistent_pk_index, - current_projection_indexes, - chunk_payload_cache, - )? - else { - return Ok(Some(false)); - }; - partial_rows.push(joined_row); - let child_outcome = walk_join_rows( - store, - table_row_readers, - join_steps, - table_projections, - join_keys, - key_projection_indexes, - step_index + 1, - partial_rows, - use_persistent_pk_index, - chunk_payload_cache, - visit, - )?; - partial_rows.pop(); - Ok(child_outcome) - }; - - match row_ids { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(row_id) => match visit_row_id(row_id)? { - Some(false) => {} - Some(true) => return Ok(Some(true)), - None => return Ok(None), - }, - RuntimeRowIdSet::Contiguous { start, len } => { - for row_id in contiguous_row_ids(start, len) { - if outcome.is_some() { - break; - } - match visit_row_id(row_id)? { - Some(false) => {} - Some(true) => return Ok(Some(true)), - None => { - outcome = Some(None); - break; - } - } - } - } - RuntimeRowIdSet::Many(row_ids) => { - for row_id in row_ids { - if outcome.is_some() { - break; - } - match visit_row_id(*row_id)? { - Some(false) => {} - Some(true) => return Ok(Some(true)), - None => { - outcome = Some(None); - break; - } - } - } - } - RuntimeRowIdSet::Owned(row_ids) => { - for row_id in row_ids { - if outcome.is_some() { - break; - } - match visit_row_id(row_id)? { - Some(false) => {} - Some(true) => return Ok(Some(true)), - None => { - outcome = Some(None); - break; - } - } - } - } - } - - Ok(outcome.unwrap_or(Some(false))) - } - - if join_keys.len() != join_steps.len() || key_projection_indexes.len() != join_steps.len() { - if !require_index { - return Ok(None); - } - return Err(DbError::internal( - "deferred view join metadata is missing while executing index-required join", - )); - } - partial_rows.clear(); - partial_rows.push(root_row); - let result = walk_join_rows( - store, - table_row_readers, - join_steps, - table_projections, - join_keys, - key_projection_indexes, - 0, - partial_rows, - use_persistent_pk_index, - chunk_payload_cache, - visit, - ); - partial_rows.clear(); - result - } - - pub(crate) fn try_execute_simple_deferred_paged_query( - &self, - query: &Query, - params: &[Value], - pager: &PagerHandle, - wal: &WalHandle, - snapshot_lsn: u64, - use_persistent_pk_index: bool, - ) -> Result> { - if let Some(result) = self.try_execute_simple_deferred_view_projection_limit_query( - query, - params, - pager, - wal, - snapshot_lsn, - use_persistent_pk_index, - )? { - return Ok(Some(result)); - } - if let Some(result) = self.try_execute_simple_deferred_view_filter_projection_query( - query, - params, - pager, - wal, - snapshot_lsn, - use_persistent_pk_index, - )? { - return Ok(Some(result)); - } - if let Some(result) = self.try_execute_simple_deferred_paged_grouped_count_query( - query, - params, - pager, - wal, - snapshot_lsn, - )? { - return Ok(Some(result)); - } - if let Some(result) = self - .try_execute_simple_deferred_paged_grouped_numeric_aggregate_query( - query, - params, - pager, - wal, - snapshot_lsn, - )? - { - return Ok(Some(result)); - } - if let Some(result) = self.try_execute_simple_deferred_rowid_join_projection_query( - query, - params, - pager, - wal, - snapshot_lsn, - use_persistent_pk_index, - )? { - return Ok(Some(result)); - } - if let Some(result) = self.try_execute_simple_deferred_distinct_filtered_projection_query( - query, - params, - pager, - wal, - snapshot_lsn, - )? { - return Ok(Some(result)); - } - if let Some(result) = self.try_execute_simple_deferred_distinct_projection_query( - query, - params, - pager, - wal, - snapshot_lsn, - )? { - return Ok(Some(result)); - } - if let Some(result) = self.try_execute_simple_deferred_filtered_projection_query( - query, - params, - pager, - wal, - snapshot_lsn, - use_persistent_pk_index, - )? { - return Ok(Some(result)); - } - if let Some(result) = self.try_execute_simple_deferred_table_projection_query( - query, - params, - pager, - wal, - snapshot_lsn, - use_persistent_pk_index, - )? { - return Ok(Some(result)); - } - if let Some(result) = self.try_execute_simple_deferred_expression_projection_query( - query, - params, - pager, - wal, - snapshot_lsn, - use_persistent_pk_index, - )? { - return Ok(Some(result)); - } - Ok(None) - } - - fn try_execute_simple_deferred_expression_projection_query( - &self, - query: &Query, - params: &[Value], - pager: &PagerHandle, - wal: &WalHandle, - snapshot_lsn: u64, - use_persistent_pk_index: bool, - ) -> Result> { - if !query.ctes.is_empty() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if !select.group_by.is_empty() - || select.having.is_some() - || !select.distinct_on.is_empty() - || select.from.len() != 1 - { - return Ok(None); - } - if select_requires_grouped_evaluation(self, select)? { - return Ok(None); - } - if select.distinct - && (!query.order_by.is_empty() || query.limit.is_some() || query.offset.is_some()) - { - return Ok(None); - } - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(name) - || self.visible_table_row_source(name).is_some() - { - return Ok(None); - } - let table_schema = match self.table_schema(name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - if select - .projection - .iter() - .any(select_item_contains_window_or_subquery) - || select - .filter - .as_ref() - .is_some_and(expr_contains_recursive_unsupported_feature) - || query - .order_by - .iter() - .any(|order| expr_contains_recursive_unsupported_feature(&order.expr)) - { - return Ok(None); - } - if select - .projection - .iter() - .any(select_item_contains_fulltext_function) - || select - .filter - .as_ref() - .is_some_and(expr_contains_fulltext_function) - || query - .order_by - .iter() - .any(|order| expr_contains_fulltext_function(&order.expr)) - { - return Ok(None); - } - let has_expression_projection = select.projection.iter().any(|item| match item { - SelectItem::Expr { expr, .. } => !matches!(expr, Expr::Column { .. }), - SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => true, - }); - if !has_expression_projection - && select.filter.is_none() - && query.order_by.is_empty() - && query.limit.is_none() - && query.offset.is_none() - { - return Ok(None); - } - - let binding_name = alias.as_deref().unwrap_or(name); - let Some(projection_plan) = - simple_expression_projection_plan(table_schema, name, binding_name, &select.projection) - else { - return Ok(None); - }; - let ctes = BTreeMap::new(); - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &ctes)) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &ctes)) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let Some(state) = self.persisted_table_state(name) else { - return Ok(None); - }; - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - let paged_locator_cache = self - .catalog - .table(name) - .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) - .map(|cache| cache.as_ref()); - if deferred_rowid_lookup_available( - state, - table_schema, - use_persistent_pk_index, - paged_locator_cache, - ) { - if let Some(row_ids) = select - .filter - .as_ref() - .map(|filter| { - self.trigram_candidate_row_ids_for_filter(name, alias, filter, params, &ctes) - }) - .transpose()? - .flatten() - { - return Ok(Some( - self.simple_expression_projection_result_from_deferred_row_ids( - &store, - state, - table_schema, - binding_name, - &select.projection, - &projection_plan, - select.filter.as_ref(), - select.distinct, - &query.order_by, - params, - limit, - offset, - &row_ids, - use_persistent_pk_index, - paged_locator_cache, - )?, - )); - } - } - Ok(Some( - self.simple_expression_projection_result_from_persisted_state( - &store, - state, - table_schema, - binding_name, - &select.projection, - &projection_plan, - select.filter.as_ref(), - select.distinct, - &query.order_by, - params, - limit, - offset, - )?, - )) - } - - fn try_execute_simple_deferred_distinct_projection_query( - &self, - query: &Query, - params: &[Value], - pager: &PagerHandle, - wal: &WalHandle, - snapshot_lsn: u64, - ) -> Result> { - if !query.ctes.is_empty() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.filter.is_some() - || !select.group_by.is_empty() - || select.having.is_some() - || !select.distinct - || !select.distinct_on.is_empty() - || select.from.len() != 1 - { - return Ok(None); - } - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(name) - || self.visible_table_row_source(name).is_some() - { - return Ok(None); - } - let table_schema = match self.table_schema(name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - let Some((projection_indexes, column_names)) = - self.simple_projection_plan(select, name, alias, table_schema) - else { - return Ok(None); - }; - let order_by = self.simple_projection_order_by_plan( - query, - table_schema, - name, - alias.as_deref().unwrap_or(name), - &projection_indexes, - )?; - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let Some(state) = self.persisted_table_state(name) else { - return Ok(None); - }; - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - Ok(Some( - self.simple_distinct_projection_result_from_persisted_state( - &store, - state, - &projection_indexes, - column_names, - order_by, - limit, - offset, - )?, - )) - } - - fn try_execute_simple_deferred_table_projection_query( - &self, - query: &Query, - params: &[Value], - pager: &PagerHandle, - wal: &WalHandle, - snapshot_lsn: u64, - use_persistent_pk_index: bool, - ) -> Result> { - if !query.ctes.is_empty() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.filter.is_some() - || !select.group_by.is_empty() - || select.having.is_some() - || select.distinct - || select.from.len() != 1 - { - return Ok(None); - } - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(name) - || self.visible_table_row_source(name).is_some() - { - return Ok(None); - } - let table_schema = match self.table_schema(name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - let Some((projection_indexes, column_names)) = - self.simple_projection_plan(select, name, alias, table_schema) - else { - return Ok(None); - }; - let order_by = self.simple_projection_order_by_plan( - query, - table_schema, - name, - alias.as_deref().unwrap_or(name), - &projection_indexes, - )?; - let row_id_order = if query.order_by.len() == 1 { - if let Expr::Column { - table: order_table, - column: order_column, - } = &query.order_by[0].expr - { - if order_table.as_deref().is_some_and(|qualifier| { - !matches_table_binding(TableBindingRef { name, alias }, Some(qualifier)) - }) { - None - } else if let Some(filter_column_index) = - schema_column_index(table_schema, order_column) - { - if table_schema - .primary_key_columns - .iter() - .any(|column| identifiers_equal(column, order_column)) - && table_schema.columns[filter_column_index].column_type - == crate::catalog::ColumnType::Int64 - { - Some((order_column.as_str(), query.order_by[0].descending)) - } else { - None - } - } else { - None - } - } else { - None - } - } else { - None - }; - - if !query.order_by.is_empty() && order_by.is_none() && row_id_order.is_none() { - return Ok(None); - } - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let Some(state) = self.persisted_table_state(name) else { - return Ok(None); - }; - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - let paged_locator_cache = self - .catalog - .table(name) - .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) - .map(|cache| cache.as_ref()); - if let Some((filter_column, descending)) = row_id_order { - if limit != Some(0) && use_persistent_pk_index { - if let Some(result) = try_persistent_pk_ordered_projection_result( - &store, - state, - table_schema, - &projection_indexes, - column_names.clone(), - limit, - offset, - descending, - )? { - return Ok(Some(result)); - } - } - if limit != Some(0) { - if let Some(row_ids) = self.ordered_runtime_btree_row_ids( - name, - filter_column, - limit, - offset, - descending, - )? { - let mut rows = Vec::with_capacity(row_ids.len().min(64)); - for row_id in row_ids { - if let Some(values) = read_deferred_projected_values_by_id( - &store, - state, - table_schema, - row_id, - use_persistent_pk_index, - paged_locator_cache, - &projection_indexes, - )? { - rows.push(QueryRow::new(values)); - } - } - return Ok(Some(QueryResult::with_rows(column_names, rows))); - } - } - } - let mut unbounded_lower_bound = None; - if let Some(cache) = paged_locator_cache.filter(|cache| cache.matches_state(state)) { - if let Some(min_row_id) = cache.min_row_id() { - unbounded_lower_bound = Some(SimpleRangeBoundValue { - inclusive: true, - value: Value::Int64(min_row_id), - }); - } - } - if unbounded_lower_bound.is_none() && use_persistent_pk_index { - if let Some(min_row_id) = first_persistent_pk_row_id(&store, table_schema)? { - unbounded_lower_bound = Some(SimpleRangeBoundValue { - inclusive: true, - value: Value::Int64(min_row_id), - }); - } - } - if limit.is_some() && limit != Some(0) { - if let Some((filter_column, _descending)) = row_id_order { - if let Some(result) = self.try_simple_deferred_rowid_range_projection_result( - &store, - state, - table_schema, - TableBindingRef { name, alias }, - filter_column, - unbounded_lower_bound.as_ref(), - None, - &projection_indexes, - column_names.clone(), - &query.order_by, - limit, - offset, - use_persistent_pk_index, - paged_locator_cache, - )? { - return Ok(Some(result)); - } - } - } - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - Ok(Some(self.simple_projection_result_from_persisted_state( - &store, - state, - &projection_indexes, - column_names, - order_by, - limit, - offset, - )?)) - } - - fn try_execute_simple_deferred_distinct_filtered_projection_query( - &self, - query: &Query, - params: &[Value], - pager: &PagerHandle, - wal: &WalHandle, - snapshot_lsn: u64, - ) -> Result> { - if !query.ctes.is_empty() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if !select.group_by.is_empty() - || select.having.is_some() - || !select.distinct - || !select.distinct_on.is_empty() - || select.from.len() != 1 - { - return Ok(None); - } - let Some(filter) = select.filter.as_ref() else { - return Ok(None); - }; - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(name) - || self.visible_table_row_source(name).is_some() - { - return Ok(None); - } - - let table_schema = match self.table_schema(name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - let Some((projection_indexes, column_names)) = - self.simple_projection_plan(select, name, alias, table_schema) - else { - return Ok(None); - }; - let binding_name = alias.as_deref().unwrap_or(name); - let Some(range_filter) = simple_range_projection_filter(filter) else { - return Ok(None); - }; - let filter_table = range_filter.table; - let filter_column = range_filter.column; - let lower_bound = range_filter.lower; - let upper_bound = range_filter.upper; - if let Some(table_name) = filter_table { - if !identifiers_equal(table_name, name) && !identifiers_equal(table_name, binding_name) - { - return Ok(None); - } - } - let filter_column_index = table_schema - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, filter_column)) - .ok_or_else(|| { - DbError::internal(format!( - "simple deferred filtered distinct projection column {filter_column} missing from {name}" - )) - })?; - let lower_bound = lower_bound - .map(|bound| { - Ok(SimpleRangeBoundValue { - inclusive: bound.inclusive, - value: self.eval_expr( - bound.value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?, - }) - }) - .transpose()?; - let upper_bound = upper_bound - .map(|bound| { - Ok(SimpleRangeBoundValue { - inclusive: bound.inclusive, - value: self.eval_expr( - bound.value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?, - }) - }) - .transpose()?; - if !range_filter.residual.is_empty() { - // The deferred distinct filtered fast path does not yet evaluate - // residual predicates; bail to the generic executor to preserve - // correctness. - return Ok(None); - } - let order_by = self.simple_projection_order_by_plan( - query, - table_schema, - name, - binding_name, - &projection_indexes, - )?; - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let Some(state) = self.persisted_table_state(name) else { - return Ok(None); - }; - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - Ok(Some( - self.simple_distinct_filtered_projection_result_from_persisted_state( - &store, - state, - filter_column_index, - lower_bound.as_ref(), - upper_bound.as_ref(), - &projection_indexes, - column_names, - order_by, - limit, - offset, - )?, - )) - } - - fn try_execute_simple_deferred_filtered_projection_query( - &self, - query: &Query, - params: &[Value], - pager: &PagerHandle, - wal: &WalHandle, - snapshot_lsn: u64, - use_persistent_pk_index: bool, - ) -> Result> { - if !query.ctes.is_empty() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if !select.group_by.is_empty() - || select.having.is_some() - || select.distinct - || !select.distinct_on.is_empty() - || select.from.len() != 1 - { - return Ok(None); - } - let Some(filter) = select.filter.as_ref() else { - return Ok(None); - }; - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(name) - || self.visible_table_row_source(name).is_some() - { - return Ok(None); - } - - let table_schema = match self.table_schema(name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - let Some((projection_indexes, column_names)) = - self.simple_projection_plan(select, name, alias, table_schema) - else { - return Ok(None); - }; - let binding_name = alias.as_deref().unwrap_or(name); - - let Some(range_filter) = simple_range_projection_filter(filter) else { - return Ok(None); - }; - let filter_table = range_filter.table; - let filter_column = range_filter.column; - let lower_bound = range_filter.lower; - let upper_bound = range_filter.upper; - if let Some(table_name) = filter_table { - if !identifiers_equal(table_name, name) && !identifiers_equal(table_name, binding_name) - { - return Ok(None); - } - } - let filter_column_index = table_schema - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, filter_column)) - .ok_or_else(|| { - DbError::internal(format!( - "simple filtered projection column {filter_column} missing from {name}" - )) - })?; - - let lower_bound = lower_bound - .map(|bound| { - Ok(SimpleRangeBoundValue { - inclusive: bound.inclusive, - value: self.eval_expr( - bound.value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?, - }) - }) - .transpose()?; - let upper_bound = upper_bound - .map(|bound| { - Ok(SimpleRangeBoundValue { - inclusive: bound.inclusive, - value: self.eval_expr( - bound.value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?, - }) - }) - .transpose()?; - - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - let Some(state) = self.persisted_table_state(name) else { - return Ok(None); - }; - let store = SnapshotPageStore { - pager, - wal, - snapshot_lsn, - }; - let paged_locator_cache = self - .catalog - .table(name) - .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) - .map(|cache| cache.as_ref()); - if range_filter.residual.is_empty() { - if let Some(result) = self.try_simple_deferred_rowid_range_projection_result( - &store, - state, - table_schema, - TableBindingRef { name, alias }, - filter_column, - lower_bound.as_ref(), - upper_bound.as_ref(), - &projection_indexes, - column_names.clone(), - &query.order_by, - limit, - offset, - use_persistent_pk_index, - paged_locator_cache, - )? { - return Ok(Some(result)); - } - } - - let order_by = self.simple_projection_order_by_plan( - query, - table_schema, - name, - binding_name, - &projection_indexes, - )?; - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - let residual_plans = self.build_simple_residual_plans( - table_schema, - name, - binding_name, - &range_filter.residual, - params, - )?; - if residual_plans.len() != range_filter.residual.len() { - return Ok(None); - } - Ok(Some( - self.simple_filtered_projection_result_from_persisted_state( - &store, - state, - filter_column_index, - lower_bound.as_ref(), - upper_bound.as_ref(), - &residual_plans, - &projection_indexes, - column_names, - order_by, - limit, - offset, - )?, - )) - } - - fn analyze_simple_indexed_projection_query<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result>> { - if !query.ctes.is_empty() { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if !select.group_by.is_empty() - || select.having.is_some() - || select.distinct - || select.from.len() != 1 - { - return Ok(None); - } - let Some(filter) = select.filter.as_ref() else { - return Ok(None); - }; - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(name) - { - return Ok(None); - } - - let table_schema = match self.table_schema(name) { - Some(table) => table, - None => return Ok(None), - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - let binding_name = alias.as_deref().unwrap_or(name); - let Some(lookup_terms) = simple_btree_lookup_terms(filter) else { - return Ok(None); - }; - for (filter_table, _, _) in &lookup_terms { - if filter_table.as_ref().is_some_and(|table_name| { - !identifiers_equal(table_name, name) && !identifiers_equal(table_name, binding_name) - }) { - return Ok(None); - } - } - let ordered_lookup_terms = if lookup_terms.len() == 1 { - lookup_terms - } else { - let Some(index) = - self.compound_btree_index_for_lookup_terms(name, lookup_terms.as_slice()) - else { - return Ok(None); - }; - ordered_lookup_terms_for_index(index, lookup_terms.as_slice())? - }; - - let mut lookup_values = Vec::with_capacity(ordered_lookup_terms.len()); - for (_, _, value_expr) in &ordered_lookup_terms { - lookup_values.push(self.eval_expr( - value_expr, - &Dataset::empty(), - &[], - params, - &BTreeMap::new(), - None, - )?); - } - let filter_column = ordered_lookup_terms[0].1; - let lookup_value = lookup_values - .first() - .cloned() - .ok_or_else(|| DbError::internal("indexed projection lookup terms are empty"))?; - let extra_lookup_terms = ordered_lookup_terms - .iter() - .skip(1) - .zip(lookup_values.into_iter().skip(1)) - .map(|((_, column, _), value)| (*column, value)) - .collect::>(); - let Some((projection_indexes, column_names)) = - self.simple_projection_plan(select, name, alias, table_schema) - else { - return Ok(None); - }; - let order_by = self.simple_projection_order_by_plan( - query, - table_schema, - name, - binding_name, - &projection_indexes, - )?; - if !query.order_by.is_empty() && order_by.is_none() { - return Ok(None); - } - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) - .unwrap_or(0); - - Ok(Some(SimpleIndexedProjectionPlan { - table_name: name, - table_schema, - filter_column, - lookup_value, - extra_lookup_terms, - projection_indexes, - column_names, - order_by, - limit, - offset, - })) - } - - pub(crate) fn simple_indexed_projection_missing_runtime_btree<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_simple_indexed_projection_query(query, params)? else { - return Ok(None); - }; - if plan.extra_lookup_terms.is_empty() - && row_id_alias_column_name(plan.table_schema) - .is_some_and(|column_name| identifiers_equal(column_name, plan.filter_column)) - { - return Ok(None); - } - let Some(index) = self.btree_index_for_simple_indexed_projection_plan(&plan) else { - return Ok(None); - }; - if matches!(self.index(&index.name), Some(RuntimeIndex::Btree { .. })) { - Ok(None) - } else { - Ok(Some((plan.table_name, index.name.as_str()))) - } - } - - pub(crate) fn simple_indexed_projection_missing_persistent_pk_root<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result> { - let Some(plan) = self.analyze_simple_indexed_projection_query(query, params)? else { - return Ok(None); - }; - if !plan.extra_lookup_terms.is_empty() || plan.table_schema.pk_index_root.is_some() { - return Ok(None); - } - let Some(filter_column_index) = schema_column_index(plan.table_schema, plan.filter_column) - else { - return Ok(None); - }; - if plan.table_schema.columns[filter_column_index].column_type != ColumnType::Int64 - || !plan - .table_schema - .primary_key_columns - .iter() - .any(|column| identifiers_equal(column, plan.filter_column)) - { - return Ok(None); - } - if self - .persisted_table_state(plan.table_name) - .is_some_and(|state| state.pointer.head_page_id != 0) - { - Ok(Some(plan.table_name)) - } else { - Ok(None) - } - } - - pub(crate) fn simple_ordered_projection_missing_persistent_pk_root<'a>( - &'a self, - query: &'a Query, - params: &[Value], - ) -> Result> { - if !query.ctes.is_empty() || query.order_by.len() != 1 || query.order_by[0].descending { - return Ok(None); - } - let QueryBody::Select(select) = &query.body else { - return Ok(None); - }; - if select.filter.is_some() - || !select.group_by.is_empty() - || select.having.is_some() - || select.distinct - || projection_has_aggregate_items(&select.projection) - || select.from.len() != 1 - { - return Ok(None); - } - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if self - .visible_view(name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(name) - || self.visible_table_row_source(name).is_some() - { - return Ok(None); - } - let Some(table_schema) = self.table_schema(name) else { - return Ok(None); - }; - if table_schema.pk_index_root.is_some() || !generated_columns_are_stored(table_schema) { - return Ok(None); - } - let Some((_projection_indexes, _)) = - self.simple_projection_plan(select, name, alias, table_schema) - else { - return Ok(None); - }; - let Expr::Column { - table: order_table, - column: order_column, - } = &query.order_by[0].expr - else { - return Ok(None); - }; - if order_table.as_deref().is_some_and(|qualifier| { - !matches_table_binding(TableBindingRef { name, alias }, Some(qualifier)) - }) { - return Ok(None); - } - let Some(order_column_index) = schema_column_index(table_schema, order_column) else { - return Ok(None); - }; - if table_schema.columns[order_column_index].column_type != ColumnType::Int64 - || !table_schema - .primary_key_columns - .iter() - .any(|column| identifiers_equal(column, order_column)) - { - return Ok(None); - } - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) - .transpose()? - .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); - if limit == Some(0) { - return Ok(None); - } - if self - .persisted_table_state(name) - .is_some_and(|state| state.pointer.head_page_id != 0) - { - Ok(Some(name)) - } else { - Ok(None) - } - } - - fn single_column_btree_index( - &self, - table_name: &str, - column_name: &str, - ) -> Option<&IndexSchema> { - self.catalog.indexes.values().find(|index| { - identifiers_equal(&index.table_name, table_name) - && index.fresh - && index.kind == IndexKind::Btree - && index.predicate_sql.is_none() - && index.columns.len() == 1 - && index.columns[0] - .column_name - .as_deref() - .is_some_and(|index_column| identifiers_equal(index_column, column_name)) - && index.columns[0].expression_sql.is_none() - }) - } - - fn compound_btree_index_for_lookup_terms( - &self, - table_name: &str, - lookup_terms: &[(Option<&str>, &str, &Expr)], - ) -> Option<&IndexSchema> { - if lookup_terms.len() < 2 { - return None; - } - self.catalog.indexes.values().find(|index| { - identifiers_equal(&index.table_name, table_name) - && index.fresh - && index.kind == IndexKind::Btree - && index.predicate_sql.is_none() - && index.columns.len() >= lookup_terms.len() - && index - .columns - .iter() - .take(lookup_terms.len()) - .all(|index_column| { - index_column.expression_sql.is_none() - && index_column.column_name.as_deref().is_some_and(|column| { - lookup_terms.iter().any(|(_, lookup_column, _)| { - identifiers_equal(column, lookup_column) - }) - }) - }) - }) - } - - fn btree_index_for_simple_indexed_projection_plan( - &self, - plan: &SimpleIndexedProjectionPlan<'_>, - ) -> Option<&IndexSchema> { - if plan.extra_lookup_terms.is_empty() { - return self.single_column_btree_index(plan.table_name, plan.filter_column); - } - let lookup_columns = std::iter::once(plan.filter_column) - .chain(plan.extra_lookup_terms.iter().map(|(column, _)| *column)) - .collect::>(); - self.catalog.indexes.values().find(|index| { - identifiers_equal(&index.table_name, plan.table_name) - && index.fresh - && index.kind == IndexKind::Btree - && index.predicate_sql.is_none() - && index.columns.len() >= lookup_columns.len() - && index - .columns - .iter() - .take(lookup_columns.len()) - .zip(lookup_columns.iter()) - .all(|(index_column, lookup_column)| { - index_column.expression_sql.is_none() - && index_column - .column_name - .as_deref() - .is_some_and(|index_column| { - identifiers_equal(index_column, lookup_column) - }) - }) - }) - } - - fn ordered_runtime_btree_row_ids( - &self, - table_name: &str, - column_name: &str, - limit: Option, - offset: usize, - descending: bool, - ) -> Result>> { - let Some(index) = self.single_column_btree_index(table_name, column_name) else { - return Ok(None); - }; - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { - return Ok(None); - }; - let take = limit.unwrap_or(usize::MAX); - if take == 0 { - return Ok(Some(Vec::new())); - } - match keys { - RuntimeBtreeKeys::UniqueInt64(entries, deleted) => { - let mut ordered = entries - .iter() - .filter(|(_, row_id)| !deleted.contains(row_id)) - .collect::>(); - let window = offset.saturating_add(take).min(ordered.len()); - if window == 0 { - return Ok(Some(Vec::new())); - } - if window < ordered.len() { - if descending { - ordered - .select_nth_unstable_by(window - 1, |left, right| right.0.cmp(&left.0)); - ordered.truncate(window); - ordered.sort_unstable_by_key(|(key, _)| std::cmp::Reverse(*key)); - } else { - ordered.select_nth_unstable_by_key(window - 1, |(key, _)| *key); - ordered.truncate(window); - ordered.sort_unstable_by_key(|(key, _)| *key); - } - } else if descending { - ordered.sort_unstable_by_key(|(key, _)| std::cmp::Reverse(*key)); - } else { - ordered.sort_unstable_by_key(|(key, _)| *key); - } - Ok(Some( - ordered - .into_iter() - .skip(offset) - .take(take) - .map(|(_, row_id)| row_id) - .collect(), - )) - } - RuntimeBtreeKeys::NonUniqueInt64(entries, deleted) => { - let mut ordered = entries - .iter() - .map(|(key, row_ids)| (key, row_ids.to_vec())) - .collect::>(); - ordered.sort_unstable_by_key(|(key, _)| *key); - let mut skipped = 0usize; - let mut row_ids = Vec::with_capacity(take.min(64)); - let ordered = if descending { - ordered.into_iter().rev().collect::>() - } else { - ordered - }; - for (_, mut ids) in ordered { - ids.sort_unstable(); - for row_id in ids { - if deleted.contains(&row_id) { - continue; - } - if skipped < offset { - skipped += 1; - continue; - } - row_ids.push(row_id); - if row_ids.len() == take { - return Ok(Some(row_ids)); - } - } - } - Ok(Some(row_ids)) - } - RuntimeBtreeKeys::UniqueEncoded(..) - | RuntimeBtreeKeys::NonUniqueEncoded(..) - | RuntimeBtreeKeys::UniqueUuid(..) - | RuntimeBtreeKeys::NonUniqueUuid(..) => Ok(None), - } - } - - fn simple_projection_plan( - &self, - select: &Select, - table_name: &str, - table_alias: &Option, - table_schema: &TableSchema, - ) -> Option<(Vec, Vec)> { - let binding_name = table_alias.as_deref().unwrap_or(table_name); - let mut projection_indexes = Vec::with_capacity(select.projection.len()); - let mut column_names = Vec::with_capacity(select.projection.len()); - - for item in &select.projection { - match item { - SelectItem::Expr { - expr, - alias: select_alias, - } => { - let Expr::Column { - table: table_name_expr, - column, - } = expr - else { - return None; - }; - if let Some(projection_table) = table_name_expr.as_deref() { - if !identifiers_equal(projection_table, table_name) - && !identifiers_equal(projection_table, binding_name) - { - return None; - } - } - let column_index = table_schema - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, column))?; - projection_indexes.push(column_index); - column_names.push(select_alias.clone().unwrap_or_else(|| column.clone())); - } - SelectItem::Wildcard => { - for (column_index, column) in table_schema.columns.iter().enumerate() { - projection_indexes.push(column_index); - column_names.push(column.name.clone()); - } - } - SelectItem::QualifiedWildcard(qualified_name) => { - if !identifiers_equal(qualified_name, table_name) - && !identifiers_equal(qualified_name, binding_name) - { - return None; - } - for (column_index, column) in table_schema.columns.iter().enumerate() { - projection_indexes.push(column_index); - column_names.push(column.name.clone()); - } - } - } - } - - Some((projection_indexes, column_names)) - } - - fn simple_projection_order_by_plan( - &self, - query: &Query, - table_schema: &TableSchema, - table_name: &str, - binding_name: &str, - projection_indexes: &[usize], - ) -> Result>> { - if query.order_by.is_empty() { - return Ok(None); - } - let order_by = query - .order_by - .iter() - .map(|entry| { - let Expr::Column { - table: order_table, - column: order_column, - } = &entry.expr - else { - return None; - }; - if let Some(order_table) = order_table.as_deref() { - if !identifiers_equal(order_table, table_name) - && !identifiers_equal(order_table, binding_name) - { - return None; - } - } - let order_projection_index = - projection_indexes.iter().position(|projection_index| { - table_schema.columns[*projection_index] - .name - .as_str() - .eq_ignore_ascii_case(order_column) - })?; - Some(SimpleOrderByPlan { - projection_index: order_projection_index, - descending: entry.descending, - collation: entry.collation.clone(), - }) - }) - .collect::>>(); - Ok(order_by) - } - - fn simple_grouped_order_by_plan( - &self, - query: &Query, - select: &Select, - table_name: &str, - binding_name: &str, - column_names: &[String], - ) -> Result>> { - if query.order_by.is_empty() { - return Ok(None); - } - let order_by = query - .order_by - .iter() - .map(|entry| { - let Expr::Column { - table: order_table, - column: order_column, - } = &entry.expr - else { - return None; - }; - let projection_index = if let Some(order_table) = order_table.as_deref() { - if !identifiers_equal(order_table, table_name) - && !identifiers_equal(order_table, binding_name) - { - return None; - } - select - .group_by - .iter() - .enumerate() - .find_map(|(index, expr)| match expr { - Expr::Column { column, .. } - if identifiers_equal(column, order_column) => - { - Some(index) - } - _ => None, - }) - } else { - column_names - .iter() - .position(|candidate| candidate.eq_ignore_ascii_case(order_column)) - .or_else(|| { - select.group_by.iter().enumerate().find_map( - |(index, expr)| match expr { - Expr::Column { column, .. } - if identifiers_equal(column, order_column) => - { - Some(index) - } - _ => None, - }, - ) - }) - }?; - Some(SimpleOrderByPlan { - projection_index, - descending: entry.descending, - collation: entry.collation.clone(), - }) - }) - .collect::>>(); - Ok(order_by) - } - - #[allow(clippy::too_many_arguments)] - fn rewrite_simple_grouped_having_expr( - &self, - expr: &Expr, - select: &Select, - table_name: &str, - binding_name: &str, - column_names: &[String], - synthetic_names: &[String], - count_projection_index: usize, - sum_projection: Option<(&str, usize)>, - ) -> Result> { - let mut aggregate_bindings = vec![SimpleGroupedNumericAggregateBinding { - kind: SimpleGroupedNumericAggregateKind::CountRows, - projection_index: count_projection_index, - source_column_name: None, - source_column_index: None, - source_expr: None, - }]; - if let Some((sum_column_name, sum_projection_index)) = sum_projection { - aggregate_bindings.push(SimpleGroupedNumericAggregateBinding { - kind: SimpleGroupedNumericAggregateKind::Sum, - projection_index: sum_projection_index, - source_column_name: Some(sum_column_name.to_string()), - source_column_index: None, - source_expr: None, - }); - } - self.rewrite_simple_grouped_having_expr_with_bindings( - expr, - select, - table_name, - binding_name, - column_names, - synthetic_names, - &aggregate_bindings, - ) - } - - #[allow(clippy::too_many_arguments)] - fn rewrite_simple_grouped_having_expr_with_bindings( - &self, - expr: &Expr, - select: &Select, - table_name: &str, - binding_name: &str, - column_names: &[String], - synthetic_names: &[String], - aggregate_bindings: &[SimpleGroupedNumericAggregateBinding], - ) -> Result> { - let rewritten = match expr { - Expr::Literal(_) | Expr::Parameter(_) => expr.clone(), - Expr::Column { table, column } => { - let Some(column_name) = simple_grouped_having_column_name( - select, - table_name, - binding_name, - column_names, - synthetic_names, - table.as_deref(), - column, - ) else { - return Ok(None); - }; - Expr::Column { - table: None, - column: column_name, - } - } - Expr::Unary { op, expr } => Expr::Unary { - op: *op, - expr: Box::new( - match self.rewrite_simple_grouped_having_expr_with_bindings( - expr, - select, - table_name, - binding_name, - column_names, - synthetic_names, - aggregate_bindings, - )? { - Some(expr) => expr, - None => return Ok(None), - }, - ), - }, - Expr::Binary { left, op, right } => Expr::Binary { - left: Box::new( - match self.rewrite_simple_grouped_having_expr_with_bindings( - left, - select, - table_name, - binding_name, - column_names, - synthetic_names, - aggregate_bindings, - )? { - Some(expr) => expr, - None => return Ok(None), - }, - ), - op: *op, - right: Box::new( - match self.rewrite_simple_grouped_having_expr_with_bindings( - right, - select, - table_name, - binding_name, - column_names, - synthetic_names, - aggregate_bindings, - )? { - Some(expr) => expr, - None => return Ok(None), - }, - ), - }, - Expr::Between { - expr, - low, - high, - negated, - } => Expr::Between { - expr: Box::new( - match self.rewrite_simple_grouped_having_expr_with_bindings( - expr, - select, - table_name, - binding_name, - column_names, - synthetic_names, - aggregate_bindings, - )? { - Some(expr) => expr, - None => return Ok(None), - }, - ), - low: Box::new( - match self.rewrite_simple_grouped_having_expr_with_bindings( - low, - select, - table_name, - binding_name, - column_names, - synthetic_names, - aggregate_bindings, - )? { - Some(expr) => expr, - None => return Ok(None), - }, - ), - high: Box::new( - match self.rewrite_simple_grouped_having_expr_with_bindings( - high, - select, - table_name, - binding_name, - column_names, - synthetic_names, - aggregate_bindings, - )? { - Some(expr) => expr, - None => return Ok(None), - }, - ), - negated: *negated, - }, - Expr::InList { - expr, - items, - negated, - } => Expr::InList { - expr: Box::new( - match self.rewrite_simple_grouped_having_expr_with_bindings( - expr, - select, - table_name, - binding_name, - column_names, - synthetic_names, - aggregate_bindings, - )? { - Some(expr) => expr, - None => return Ok(None), - }, - ), - items: { - let mut rewritten_items = Vec::with_capacity(items.len()); - for item in items { - let Some(item) = self.rewrite_simple_grouped_having_expr_with_bindings( - item, - select, - table_name, - binding_name, - column_names, - synthetic_names, - aggregate_bindings, - )? - else { - return Ok(None); - }; - rewritten_items.push(item); - } - rewritten_items - }, - negated: *negated, - }, - Expr::Like { - expr, - pattern, - escape, - case_insensitive, - negated, - } => Expr::Like { - expr: Box::new( - match self.rewrite_simple_grouped_having_expr_with_bindings( - expr, - select, - table_name, - binding_name, - column_names, - synthetic_names, - aggregate_bindings, - )? { - Some(expr) => expr, - None => return Ok(None), - }, - ), - pattern: Box::new( - match self.rewrite_simple_grouped_having_expr_with_bindings( - pattern, - select, - table_name, - binding_name, - column_names, - synthetic_names, - aggregate_bindings, - )? { - Some(expr) => expr, - None => return Ok(None), - }, - ), - escape: match escape { - Some(escape) => Some(Box::new( - match self.rewrite_simple_grouped_having_expr_with_bindings( - escape, - select, - table_name, - binding_name, - column_names, - synthetic_names, - aggregate_bindings, - )? { - Some(expr) => expr, - None => return Ok(None), - }, - )), - None => None, - }, - case_insensitive: *case_insensitive, - negated: *negated, - }, - Expr::IsNull { expr, negated } => Expr::IsNull { - expr: Box::new( - match self.rewrite_simple_grouped_having_expr_with_bindings( - expr, - select, - table_name, - binding_name, - column_names, - synthetic_names, - aggregate_bindings, - )? { - Some(expr) => expr, - None => return Ok(None), - }, - ), - negated: *negated, - }, - Expr::Function { name, args } => Expr::Function { - name: name.clone(), - args: { - let mut rewritten_args = Vec::with_capacity(args.len()); - for arg in args { - let Some(arg) = self.rewrite_simple_grouped_having_expr_with_bindings( - arg, - select, - table_name, - binding_name, - column_names, - synthetic_names, - aggregate_bindings, - )? - else { - return Ok(None); - }; - rewritten_args.push(arg); - } - rewritten_args - }, - }, - Expr::Aggregate { .. } => { - let Some(binding) = matching_simple_grouped_aggregate_binding( - expr, - table_name, - binding_name, - aggregate_bindings, - ) else { - return Ok(None); - }; - Expr::Column { - table: None, - column: synthetic_names[binding.projection_index].clone(), - } - } - _ => return Ok(None), - }; - Ok(Some(rewritten)) - } - - #[allow(clippy::too_many_arguments)] - fn apply_simple_grouped_postprocessing( - &self, - rows: I, - column_names: Vec, - having_bindings: &[ColumnBinding], - having: Option<&Expr>, - params: &[Value], - order_by: Option<&[SimpleOrderByPlan]>, - limit: Option, - offset: usize, - ) -> Result - where - I: IntoIterator, - { - let bounded_order = order_by - .and_then(|order_by| limit.map(|limit| (order_by, offset.saturating_add(limit)))); - let mut rows_out = if let Some((_, bounded_row_count)) = bounded_order { - if bounded_row_count == 0 { - return Ok(QueryResult::with_rows(column_names, Vec::new())); - } - Vec::with_capacity(bounded_row_count) - } else { - Vec::new() - }; - - let mut push_row = |row: QueryRow| -> Result<()> { - if let Some((order_by, bounded_row_count)) = bounded_order { - push_bounded_projection_ordered_query_row( - Some(self), - &mut rows_out, - row, - order_by, - bounded_row_count, - ) - } else { - rows_out.push(row); - Ok(()) - } - }; - - if let Some(having) = having { - let having_dataset = Dataset::with_rows(having_bindings.to_vec(), Vec::new()); - let ctes = BTreeMap::new(); - for row in rows { - if matches!( - self.eval_expr(having, &having_dataset, row.values(), params, &ctes, None)?, - Value::Bool(true) - ) { - push_row(row)?; - } - } - } else { - for row in rows { - push_row(row)?; - } - } - - if let Some((order_by, _)) = bounded_order { - sort_query_rows_by_projection_order(Some(self), &mut rows_out, order_by)?; - let rows = rows_out - .into_iter() - .skip(offset) - .take(limit.unwrap_or(usize::MAX)) - .collect(); - return Ok(QueryResult::with_rows(column_names, rows)); - } - - if let Some(order_by) = order_by { - sort_query_rows_by_projection_order(Some(self), &mut rows_out, order_by)?; - } - - let rows = rows_out - .into_iter() - .skip(offset) - .take(limit.unwrap_or(usize::MAX)) - .collect(); - Ok(QueryResult::with_rows(column_names, rows)) - } - - fn try_execute_crm_revenue_raw_aggregate_query( - &self, - query: &Query, - ) -> Result> { - if !Self::is_crm_revenue_raw_aggregate_query(query) { - return Ok(None); - } - - let Some(companies_schema) = self.table_schema("companies") else { - return Ok(None); - }; - let Some(users_schema) = self.table_schema("users") else { - return Ok(None); - }; - let Some(invoices_schema) = self.table_schema("invoices") else { - return Ok(None); - }; - let Some(companies_id_index) = crm_column_index(companies_schema, "id", ColumnType::Int64) - else { - return Ok(None); - }; - let Some(companies_name_index) = - crm_column_index(companies_schema, "name", ColumnType::Text) - else { - return Ok(None); - }; - let Some(users_id_index) = crm_column_index(users_schema, "id", ColumnType::Int64) else { - return Ok(None); - }; - let Some(users_company_id_index) = - crm_column_index(users_schema, "company_id", ColumnType::Int64) - else { - return Ok(None); - }; - let Some(invoices_company_id_index) = - crm_column_index(invoices_schema, "company_id", ColumnType::Int64) - else { - return Ok(None); - }; - let Some(invoices_total_index) = - crm_column_index(invoices_schema, "total", ColumnType::Float64) - else { - return Ok(None); - }; - - let Some(companies_source) = self.visible_table_row_source("companies") else { - return Ok(None); - }; - let mut company_names = BTreeMap::new(); - for row in companies_source.rows() { - let row = row?; - let Some(company_id) = - crm_i64_cell(row.values().get(companies_id_index), "companies", "id")? - else { - continue; - }; - let Some(company_name) = - crm_text_cell(row.values().get(companies_name_index), "companies", "name")? - else { - continue; - }; - company_names.insert(company_id, company_name); - } - - let Some(users_source) = self.visible_table_row_source("users") else { - return Ok(None); - }; - let mut counted_company_users = BTreeSet::new(); - let mut user_counts = BTreeMap::new(); - for row in users_source.rows() { - let row = row?; - let Some(user_id) = crm_i64_cell(row.values().get(users_id_index), "users", "id")? - else { - continue; - }; - let Some(company_id) = crm_i64_cell( - row.values().get(users_company_id_index), - "users", - "company_id", - )? - else { - continue; - }; - if company_names.contains_key(&company_id) - && counted_company_users.insert((company_id, user_id)) - { - *user_counts.entry(company_id).or_insert(0_i64) += 1; - } - } - - let revenues = - if let Some(revenues) = self.crm_revenue_from_company_covering_index(&company_names)? { - revenues - } else { - let Some(invoices_source) = self.visible_table_row_source("invoices") else { - return Ok(None); - }; - crm_revenue_from_invoice_rows( - invoices_source, - invoices_company_id_index, - invoices_total_index, - &company_names, - )? - }; - - let mut rows = Vec::with_capacity(company_names.len()); - for (company_id, company_name) in company_names { - let revenue = revenues.get(&company_id).copied().unwrap_or(0.0); - let revenue_value = revenues - .get(&company_id) - .copied() - .map(Value::Float64) - .unwrap_or(Value::Int64(0)); - rows.push(( - revenue, - QueryRow::new(vec![ - Value::Text(company_name), - Value::Int64(user_counts.get(&company_id).copied().unwrap_or(0)), - revenue_value, - ]), - )); - } - rows.sort_by(|left, right| { - right - .0 - .partial_cmp(&left.0) - .unwrap_or(std::cmp::Ordering::Equal) - }); - - Ok(Some(QueryResult::with_rows( - vec![ - "name".to_string(), - "user_count".to_string(), - "revenue".to_string(), - ], - rows.into_iter().map(|(_, row)| row).collect(), - ))) - } - - fn crm_revenue_from_company_covering_index( - &self, - company_names: &BTreeMap, - ) -> Result>> { - let Some(RuntimeIndex::Btree { - keys, - covering: Some(covering), - }) = self.index("idx_invoices_company_revenue") - else { - return Ok(None); - }; - let Some(company_id_offset) = covering.column_position("company_id") else { - return Ok(None); - }; - let Some(total_offset) = covering.column_position("total") else { - return Ok(None); - }; - let deleted = match keys { - RuntimeBtreeKeys::UniqueEncoded(_, deleted) - | RuntimeBtreeKeys::NonUniqueEncoded(_, deleted) - | RuntimeBtreeKeys::UniqueInt64(_, deleted) - | RuntimeBtreeKeys::NonUniqueInt64(_, deleted) - | RuntimeBtreeKeys::UniqueUuid(_, deleted) - | RuntimeBtreeKeys::NonUniqueUuid(_, deleted) => deleted, - }; - - if let Some(revenues) = crm_revenue_from_covering_dense( - covering, - company_id_offset, - total_offset, - deleted, - company_names, - )? { - Ok(Some(revenues)) - } else { - crm_revenue_from_covering_sparse( - covering, - company_id_offset, - total_offset, - deleted, - company_names, - ) - .map(Some) - } - } - - fn is_crm_revenue_raw_aggregate_query(query: &Query) -> bool { - if query.recursive - || !query.ctes.is_empty() - || query.limit.is_some() - || query.offset.is_some() - || !Self::is_crm_revenue_order_by(&query.order_by) - { - return false; - } - - let QueryBody::Select(select) = &query.body else { - return false; - }; - if select.filter.is_some() - || select.having.is_some() - || select.distinct - || !select.distinct_on.is_empty() - || select.group_by.len() != 2 - || !crm_column(&select.group_by[0], &["c", "companies"], "id") - || !crm_column(&select.group_by[1], &["c", "companies"], "name") - { - return false; - } - - let [from] = &select.from[..] else { - return false; - }; - if !Self::is_crm_revenue_raw_aggregate_from(from) { - return false; - } - - let [SelectItem::Expr { - expr: name_expr, - alias: name_alias, - }, SelectItem::Expr { - expr: user_count_expr, - alias: user_count_alias, - }, SelectItem::Expr { - expr: revenue_expr, - alias: revenue_alias, - }] = &select.projection[..] - else { - return false; - }; - - name_alias.is_none() - && user_count_alias - .as_deref() - .is_some_and(|alias| identifiers_equal(alias, "user_count")) - && revenue_alias - .as_deref() - .is_some_and(|alias| identifiers_equal(alias, "revenue")) - && crm_column(name_expr, &["c", "companies"], "name") - && crm_count_distinct_users(user_count_expr) - && crm_coalesced_invoice_total_sum(revenue_expr) - } - - fn is_crm_revenue_order_by(order_by: &[OrderBy]) -> bool { - let [order] = order_by else { - return false; - }; - let Expr::Column { table, column } = &order.expr else { - return false; - }; - order.descending - && order.collation.is_none() - && table.is_none() - && identifiers_equal(column, "revenue") - } - - fn is_crm_revenue_raw_aggregate_from(item: &FromItem) -> bool { - let FromItem::Join { - left, - right, - kind, - constraint, - } = item - else { - return false; - }; - - *kind == JoinKind::Left - && Self::is_crm_companies_users_left_join(left) - && crm_table(right, "invoices", "i") - && crm_join_on_columns( - constraint, - &["i", "invoices"], - "user_id", - &["u", "users"], - "id", - ) - } - - fn is_crm_companies_users_left_join(item: &FromItem) -> bool { - let FromItem::Join { - left, - right, - kind, - constraint, - } = item - else { - return false; - }; - - *kind == JoinKind::Left - && crm_table(left, "companies", "c") - && crm_table(right, "users", "u") - && crm_join_on_columns( - constraint, - &["u", "users"], - "company_id", - &["c", "companies"], - "id", - ) - } - - pub(crate) fn evaluate_query( - &self, - query: &Query, - params: &[Value], - inherited_ctes: &BTreeMap, - ) -> Result { - let mut ctes = inherited_ctes.clone(); - let recursive_ctes = validate_recursive_ctes(query)?; - for cte in &query.ctes { - let dataset = if recursive_ctes.contains(&cte.name) { - self.evaluate_recursive_cte(cte, params, &ctes)? - } else { - prepare_cte_dataset(cte, self.evaluate_query(&cte.query, params, &ctes)?)? - }; - ctes.insert(cte.name.clone(), dataset); - } - - if let Some(dataset) = - self.try_execute_simple_union_range_projection_query(query, params, &ctes)? - { - return Ok(dataset); - } - - let mut sorted_during_select = false; - let mut dataset = match &query.body { - QueryBody::Select(select) => { - if let Some(dataset) = self.try_fulltext_bm25_top_k_select( - select, - &query.order_by, - query.limit.as_ref(), - query.offset.as_ref(), - params, - &ctes, - )? { - return Ok(dataset); - } - if select_requires_grouped_evaluation(self, select)? { - self.evaluate_select(select, params, &ctes)? - } else { - let projection_order_by = - projection_order_by_plan(&query.order_by, &select.projection); - let mut source = self.build_select_dataset(select, params, &ctes)?; - if !query.order_by.is_empty() && projection_order_by.is_none() { - self.sort_dataset(&mut source, &query.order_by, params, &ctes)?; - sorted_during_select = true; - } - let mut projected = - self.project_dataset(&source, &select.projection, params, &ctes, None)?; - if let Some(order_by_plan) = projection_order_by.as_deref() { - sort_dataset_by_projection_order( - Some(self), - &mut projected, - order_by_plan, - )?; - sorted_during_select = true; - } - projected - } - } - _ => self.evaluate_query_body(&query.body, params, &ctes)?, - }; - if let QueryBody::Select(select) = &query.body { - if select.distinct { - if !query.order_by.is_empty() && !sorted_during_select { - self.sort_dataset(&mut dataset, &query.order_by, params, &ctes)?; - sorted_during_select = true; - } - dataset = self.apply_select_distinct(select, dataset, params, &ctes)?; - } - } - if !query.order_by.is_empty() && !sorted_during_select { - self.sort_dataset(&mut dataset, &query.order_by, params, &ctes)?; - } - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &ctes)) - .transpose()? - .unwrap_or(0); - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &ctes)) - .transpose()?; - if offset > 0 || limit.is_some() { - let start = usize::try_from(offset.max(0)).unwrap_or(usize::MAX); - let rows = if start >= dataset.rows.len() { - Vec::new() - } else { - let iter = dataset.take_rows().into_iter().skip(start); - match limit { - Some(limit) => iter - .take(usize::try_from(limit.max(0)).unwrap_or(0)) - .collect(), - None => iter.collect(), - } - }; - dataset.set_rows(rows); - } - Ok(dataset) - } - - fn evaluate_recursive_cte( - &self, - cte: &CommonTableExpr, - params: &[Value], - inherited_ctes: &BTreeMap, - ) -> Result { - if let Some(dataset) = Self::try_evaluate_simple_integer_series_cte(cte) { - return Ok(dataset); - } - if !cte.query.order_by.is_empty() || cte.query.limit.is_some() || cte.query.offset.is_some() - { - return Err(DbError::sql(format!( - "recursive CTE {} does not support ORDER BY, LIMIT, or OFFSET at the CTE level", - cte.name - ))); - } - - let QueryBody::SetOperation { - op: crate::sql::ast::SetOperation::Union, - all, - left, - right, - } = &cte.query.body - else { - return Err(DbError::sql(format!( - "recursive CTE {} must use UNION or UNION ALL between anchor and recursive terms", - cte.name - ))); - }; - - let anchor_references = query_body_table_reference_count(left, &cte.name); - if anchor_references != 0 { - return Err(DbError::sql(format!( - "recursive CTE {} anchor term must not reference itself", - cte.name - ))); - } - - let recursive_references = query_body_table_reference_count(right, &cte.name); - if recursive_references != 1 { - return Err(DbError::sql(format!( - "recursive CTE {} recursive term must reference itself exactly once", - cte.name - ))); - } - validate_recursive_term(right, &cte.name)?; - - let mut anchor = self.evaluate_query_body(left, params, inherited_ctes)?; - if !all { - let rows = anchor.take_rows(); - anchor.set_rows(deduplicate_rows(rows)?); - } - let mut result = prepare_cte_dataset(cte, anchor)?; - let mut working = result.clone(); - let mut seen = if *all { - None - } else { - Some( - result - .rows - .iter() - .map(|row| row_identity(row)) - .collect::>>()?, - ) - }; - - for _ in 0..RECURSIVE_CTE_MAX_ITERATIONS { - if working.rows.is_empty() { - return Ok(result); - } - - let mut recursive_ctes = inherited_ctes.clone(); - recursive_ctes.insert(cte.name.clone(), working.clone()); - - let recursive_rows = prepare_cte_dataset( - cte, - self.evaluate_query_body(right, params, &recursive_ctes)?, - )?; - if recursive_rows.columns.len() != result.columns.len() { - return Err(DbError::sql(format!( - "recursive CTE {} produced {} columns in its recursive term but {} in its anchor term", - cte.name, - recursive_rows.columns.len(), - result.columns.len() - ))); - } - - let next_rows = if let Some(seen) = &mut seen { - let mut rows = Vec::new(); - for row in recursive_rows.into_rows() { - let identity = row_identity(&row)?; - if seen.insert(identity) { - rows.push(row); - } - } - rows - } else { - recursive_rows.into_rows() - }; - - if next_rows.is_empty() { - return Ok(result); - } - - result.rows_mut().extend(next_rows.clone()); - working.set_rows(next_rows); - } - - Err(DbError::sql(format!( - "recursive CTE {} exceeded the {} iteration limit", - cte.name, RECURSIVE_CTE_MAX_ITERATIONS - ))) - } - - fn try_execute_simple_integer_series_query(query: &Query) -> Option { - if !query.recursive - || query.ctes.len() != 1 - || !query.order_by.is_empty() - || query.limit.is_some() - || query.offset.is_some() - { - return None; - } - let cte = query.ctes.first()?; - let (column_name, start, step, upper_exclusive) = Self::simple_integer_series_bounds(cte)?; - - let QueryBody::Select(select) = &query.body else { - return None; - }; - if select.from.len() != 1 - || select.filter.is_some() - || !select.group_by.is_empty() - || select.having.is_some() - || select.distinct - || !select.distinct_on.is_empty() - { - return None; - } - let [FromItem::Table { name, alias }] = select.from.as_slice() else { - return None; - }; - if !identifiers_equal(name, &cte.name) { - return None; - } - let binding_name = alias.as_deref().unwrap_or(name); - let [SelectItem::Expr { expr, alias }] = select.projection.as_slice() else { - return None; - }; - if !Self::simple_integer_series_column_ref(expr, binding_name, &column_name) { - return None; - } - - let max_rows = upper_exclusive - .checked_sub(start)? - .checked_div(step)? - .checked_add(1)?; - let capacity = usize::try_from(max_rows) - .ok() - .filter(|rows| *rows <= RECURSIVE_CTE_MAX_ITERATIONS)?; - let mut rows = Vec::with_capacity(capacity); - let mut value = start; - rows.push(QueryRow::new(vec![Value::Int64(value)])); - while value < upper_exclusive { - value = value.checked_add(step)?; - rows.push(QueryRow::new(vec![Value::Int64(value)])); - if rows.len() > RECURSIVE_CTE_MAX_ITERATIONS { - return None; - } - } - - Some(QueryResult::with_rows( - vec![alias.clone().unwrap_or(column_name)], - rows, - )) - } - - fn try_evaluate_simple_integer_series_cte(cte: &CommonTableExpr) -> Option { - let (column_name, start, step, upper_exclusive) = Self::simple_integer_series_bounds(cte)?; - let mut rows = Vec::new(); - let mut value = start; - rows.push(vec![Value::Int64(value)]); - while value < upper_exclusive { - if rows.len() >= RECURSIVE_CTE_MAX_ITERATIONS { - return None; - } - value = value.checked_add(step)?; - rows.push(vec![Value::Int64(value)]); - } - - Some(Dataset::with_rows( - vec![ColumnBinding::visible(Some(cte.name.clone()), column_name)], - rows, - )) - } - - fn simple_integer_series_bounds(cte: &CommonTableExpr) -> Option<(String, i64, i64, i64)> { - if cte.column_names.len() != 1 - || !cte.query.recursive - || !cte.query.order_by.is_empty() - || cte.query.limit.is_some() - || cte.query.offset.is_some() - { - return None; - } - let QueryBody::SetOperation { - op: crate::sql::ast::SetOperation::Union, - all: true, - left, - right, - } = &cte.query.body - else { - return None; - }; - - let start = Self::simple_integer_series_anchor(left)?; - let (step, upper_exclusive) = - Self::simple_integer_series_recursive_term(right, &cte.name, &cte.column_names[0])?; - if step <= 0 { - return None; - } - Some((cte.column_names[0].clone(), start, step, upper_exclusive)) - } - - fn simple_integer_series_anchor(body: &QueryBody) -> Option { - let QueryBody::Select(select) = body else { - return None; - }; - if !select.from.is_empty() - || select.filter.is_some() - || !select.group_by.is_empty() - || select.having.is_some() - || select.distinct - || !select.distinct_on.is_empty() - { - return None; - } - let [SelectItem::Expr { expr, .. }] = select.projection.as_slice() else { - return None; - }; - let Expr::Literal(Value::Int64(value)) = expr else { - return None; - }; - Some(*value) - } - - fn simple_integer_series_recursive_term( - body: &QueryBody, - cte_name: &str, - column_name: &str, - ) -> Option<(i64, i64)> { - let QueryBody::Select(select) = body else { - return None; - }; - if select.from.len() != 1 - || !select.group_by.is_empty() - || select.having.is_some() - || select.distinct - || !select.distinct_on.is_empty() - { - return None; - } - let [FromItem::Table { name, alias }] = select.from.as_slice() else { - return None; - }; - if !identifiers_equal(name, cte_name) { - return None; - } - let binding_name = alias.as_deref().unwrap_or(name); - - let [SelectItem::Expr { expr, .. }] = select.projection.as_slice() else { - return None; - }; - let step = match expr { - Expr::Binary { left, op, right } if *op == BinaryOp::Add => { - if Self::simple_integer_series_column_ref(left, binding_name, column_name) { - Self::simple_int64_literal(right)? - } else if Self::simple_integer_series_column_ref(right, binding_name, column_name) { - Self::simple_int64_literal(left)? - } else { - return None; - } - } - _ => return None, - }; - - let filter = select.filter.as_ref()?; - let upper_exclusive = match filter { - Expr::Binary { left, op, right } if *op == BinaryOp::Lt => { - if !Self::simple_integer_series_column_ref(left, binding_name, column_name) { - return None; - } - Self::simple_int64_literal(right)? - } - _ => return None, - }; - Some((step, upper_exclusive)) - } - - fn simple_integer_series_column_ref(expr: &Expr, table_name: &str, column_name: &str) -> bool { - matches!( - expr, - Expr::Column { table, column } - if identifiers_equal(column, column_name) - && table - .as_deref() - .is_none_or(|candidate| identifiers_equal(candidate, table_name)) - ) - } - - fn simple_int64_literal(expr: &Expr) -> Option { - match expr { - Expr::Literal(Value::Int64(value)) => Some(*value), - _ => None, - } - } - - fn evaluate_query_with_outer( - &self, - query: &Query, - params: &[Value], - inherited_ctes: &BTreeMap, - outer_dataset: &Dataset, - outer_row: &[Value], - ) -> Result { - if !query_references_outer_scope(query, outer_dataset) { - return self.evaluate_query(query, params, inherited_ctes); - } - if query.recursive { - return Err(DbError::sql( - "WITH RECURSIVE is not supported in correlated subqueries yet", - )); - } - - let mut ctes = inherited_ctes.clone(); - for cte in &query.ctes { - let mut dataset = self.evaluate_query_with_outer( - &cte.query, - params, - &ctes, - outer_dataset, - outer_row, - )?; - if !cte.column_names.is_empty() { - if cte.column_names.len() != dataset.columns.len() { - return Err(DbError::sql(format!( - "CTE {} expected {} columns but produced {}", - cte.name, - cte.column_names.len(), - dataset.columns.len() - ))); - } - for (binding, name) in dataset.columns.iter_mut().zip(&cte.column_names) { - binding.name = name.clone(); - binding.table = Some(cte.name.clone()); - } - } - ctes.insert(cte.name.clone(), dataset); - } - - let mut sorted_during_select = false; - let mut dataset = match &query.body { - QueryBody::Select(select) => { - if let Some(dataset) = self.try_fulltext_bm25_top_k_select( - select, - &query.order_by, - query.limit.as_ref(), - query.offset.as_ref(), - params, - &ctes, - )? { - return Ok(dataset); - } - if select_requires_grouped_evaluation(self, select)? { - self.evaluate_select_with_outer( - select, - params, - &ctes, - outer_dataset, - outer_row, - )? - } else { - let projection_order_by = - projection_order_by_plan(&query.order_by, &select.projection); - let mut source = self.build_select_dataset_with_outer( - select, - params, - &ctes, - outer_dataset, - outer_row, - )?; - if !query.order_by.is_empty() && projection_order_by.is_none() { - self.sort_dataset(&mut source, &query.order_by, params, &ctes)?; - sorted_during_select = true; - } - let mut projected = - self.project_dataset(&source, &select.projection, params, &ctes, None)?; - if let Some(order_by_plan) = projection_order_by.as_deref() { - sort_dataset_by_projection_order( - Some(self), - &mut projected, - order_by_plan, - )?; - sorted_during_select = true; - } - projected - } - } - _ => self.evaluate_query_body_with_outer( - &query.body, - params, - &ctes, - outer_dataset, - outer_row, - )?, - }; - if let QueryBody::Select(select) = &query.body { - if select.distinct { - if !query.order_by.is_empty() && !sorted_during_select { - self.sort_dataset(&mut dataset, &query.order_by, params, &ctes)?; - sorted_during_select = true; - } - dataset = self.apply_select_distinct(select, dataset, params, &ctes)?; - } - } - if !query.order_by.is_empty() && !sorted_during_select { - self.sort_dataset(&mut dataset, &query.order_by, params, &ctes)?; - } - let offset = query - .offset - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &ctes)) - .transpose()? - .unwrap_or(0); - let limit = query - .limit - .as_ref() - .map(|expr| self.eval_constant_i64(expr, params, &ctes)) - .transpose()?; - if offset > 0 || limit.is_some() { - let start = usize::try_from(offset.max(0)).unwrap_or(usize::MAX); - let rows = if start >= dataset.rows.len() { - Vec::new() - } else { - let iter = dataset.take_rows().into_iter().skip(start); - match limit { - Some(limit) => iter - .take(usize::try_from(limit.max(0)).unwrap_or(0)) - .collect(), - None => iter.collect(), - } - }; - dataset.set_rows(rows); - } - Ok(dataset) - } - - fn evaluate_query_body( - &self, - body: &QueryBody, - params: &[Value], - ctes: &BTreeMap, - ) -> Result { - match body { - QueryBody::Select(select) => self.evaluate_select(select, params, ctes), - QueryBody::Values(rows) => self.evaluate_values_body(rows, params, ctes), - QueryBody::SetOperation { - op, - all, - left, - right, - } => { - let left = self.evaluate_query_body(left, params, ctes)?; - let right = self.evaluate_query_body(right, params, ctes)?; - self.evaluate_set_operation(*op, *all, left, right) - } - } - } - - fn evaluate_query_body_with_outer( - &self, - body: &QueryBody, - params: &[Value], - ctes: &BTreeMap, - outer_dataset: &Dataset, - outer_row: &[Value], - ) -> Result { - match body { - QueryBody::Select(select) => { - self.evaluate_select_with_outer(select, params, ctes, outer_dataset, outer_row) - } - QueryBody::Values(rows) => { - self.evaluate_values_body_with_outer(rows, params, ctes, outer_dataset, outer_row) - } - QueryBody::SetOperation { - op, - all, - left, - right, - } => { - let left = self.evaluate_query_body_with_outer( - left, - params, - ctes, - outer_dataset, - outer_row, - )?; - let right = self.evaluate_query_body_with_outer( - right, - params, - ctes, - outer_dataset, - outer_row, - )?; - self.evaluate_set_operation(*op, *all, left, right) - } - } - } - - fn evaluate_select( - &self, - select: &Select, - params: &[Value], - ctes: &BTreeMap, - ) -> Result { - let dataset = self.build_select_dataset(select, params, ctes)?; - if select_requires_grouped_evaluation(self, select)? { - self.evaluate_grouped_select(select, dataset, params, ctes) - } else { - self.project_dataset(&dataset, &select.projection, params, ctes, None) - } - } - - fn evaluate_select_with_outer( - &self, - select: &Select, - params: &[Value], - ctes: &BTreeMap, - outer_dataset: &Dataset, - outer_row: &[Value], - ) -> Result { - let dataset = - self.build_select_dataset_with_outer(select, params, ctes, outer_dataset, outer_row)?; - if select_requires_grouped_evaluation(self, select)? { - self.evaluate_grouped_select(select, dataset, params, ctes) - } else { - self.project_dataset(&dataset, &select.projection, params, ctes, None) - } - } - - fn apply_select_distinct( - &self, - select: &Select, - dataset: Dataset, - params: &[Value], - ctes: &BTreeMap, - ) -> Result { - if !select.distinct { - return Ok(dataset); - } - if select.distinct_on.iter().any(expr_contains_collation) - || select.projection.iter().any(select_item_contains_collation) - { - return Err(DbError::sql( - "COLLATE in DISTINCT keys is not supported in this compatibility slice", - )); - } - - let Dataset { columns, rows } = dataset; - let rows = if select.distinct_on.is_empty() { - deduplicate_rows_stable(Arc::unwrap_or_clone(rows))? - } else { - let key_dataset = Dataset::with_rows(columns.clone(), Vec::new()); - let mut seen = BTreeSet::new(); - let mut distinct_rows = Vec::new(); - for row in Arc::unwrap_or_clone(rows) { - let key = select - .distinct_on - .iter() - .map(|expr| self.eval_expr(expr, &key_dataset, &row, params, ctes, None)) - .collect::>>()?; - if seen.insert(row_identity(&key)?) { - distinct_rows.push(row); - } - } - distinct_rows - }; - - Ok(Dataset::with_rows(columns, rows)) - } - - fn build_select_dataset( - &self, - select: &Select, - params: &[Value], - ctes: &BTreeMap, - ) -> Result { - let has_lateral = select.from.iter().any(from_item_contains_lateral); - let mut dataset = if !has_lateral { - if let Some(dataset) = self.try_view_filter_pushdown(select, params, ctes)? { - dataset - } else if let Some(dataset) = self.try_indexed_scan(select, params, ctes)? { - dataset - } else if let Some(dataset) = self.try_spatial_join(select, params, ctes)? { - dataset - } else if let Some(dataset) = self.try_indexed_join(select, params, ctes)? { - dataset - } else if let Some(dataset) = - self.try_indexed_prefiltered_inner_join_tree(select, params, ctes)? - { - dataset - } else { - self.evaluate_from_clause(&select.from, params, ctes, &Dataset::empty(), &[])? - } - } else { - self.evaluate_from_clause(&select.from, params, ctes, &Dataset::empty(), &[])? - }; - - if let Some(filter) = &select.filter { - let filter_dataset = Dataset::with_rows(dataset.columns.clone(), Vec::new()); - let mut filtered = Vec::with_capacity(dataset.rows.len()); - for row in dataset.take_rows() { - if matches!( - self.eval_expr(filter, &filter_dataset, &row, params, ctes, None)?, - Value::Bool(true) - ) { - filtered.push(row); - } - } - dataset.set_rows(filtered); - } - - Ok(dataset) - } - - fn try_view_filter_pushdown( - &self, - select: &Select, - params: &[Value], - ctes: &BTreeMap, - ) -> Result> { - let Some(filter) = select.filter.as_ref() else { - return Ok(None); - }; - if select.from.len() != 1 { - return Ok(None); - } - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if ctes.contains_key(name) { - return Ok(None); - } - let Some(view) = self.visible_view(name, NameResolutionScope::Session) else { - return Ok(None); - }; - let Some((table_qualifier, filter_column, value_expr)) = simple_btree_lookup(filter) else { - return Ok(None); - }; - let view_binding = alias.as_deref().unwrap_or(name.as_str()); - if table_qualifier.is_some_and(|table| !identifiers_equal(table, view_binding)) { - return Ok(None); - } - - let mut query = (*self.cached_view_query(view)?).clone(); - if query.recursive - || !query.ctes.is_empty() - || !query.order_by.is_empty() - || query.limit.is_some() - || query.offset.is_some() - { - return Ok(None); - } - let QueryBody::Select(view_select) = &mut query.body else { - return Ok(None); - }; - if view_select.distinct - || !view_select.distinct_on.is_empty() - || !view_select.group_by.is_empty() - || view_select.having.is_some() - || projection_has_aggregate_items(&view_select.projection) - { - return Ok(None); - } - let Some(view_expr) = - view_projection_expr_for_output_column(&view_select.projection, filter_column) - else { - return Ok(None); - }; - let pushed_filter = Expr::Binary { - left: Box::new(view_expr), - op: BinaryOp::Eq, - right: Box::new(value_expr.clone()), - }; - view_select.filter = match view_select.filter.take() { - Some(existing) => Some(Expr::Binary { - left: Box::new(existing), - op: BinaryOp::And, - right: Box::new(pushed_filter), - }), - None => Some(pushed_filter), - }; - - let mut dataset = if view.temporary { - self.evaluate_query(&query, params, ctes)? - } else { - let persistent_runtime = self.persistent_resolution_runtime(); - persistent_runtime.evaluate_query(&query, params, ctes)? - }; - if let Some(alias) = alias { - for column in &mut dataset.columns { - column.table = Some(alias.clone()); - } - } else { - for column in &mut dataset.columns { - column.table = Some(view.name.clone()); - } - } - Ok(Some(dataset)) - } - - fn build_select_dataset_with_outer( - &self, - select: &Select, - params: &[Value], - ctes: &BTreeMap, - outer_dataset: &Dataset, - outer_row: &[Value], - ) -> Result { - let mut dataset = - self.evaluate_from_clause(&select.from, params, ctes, outer_dataset, outer_row)?; - - dataset = augment_dataset_with_outer_scope(dataset, outer_dataset, outer_row); - if let Some(filter) = &select.filter { - let filter_dataset = Dataset::with_rows(dataset.columns.clone(), Vec::new()); - let mut filtered = Vec::with_capacity(dataset.rows.len()); - for row in dataset.take_rows() { - if matches!( - self.eval_expr(filter, &filter_dataset, &row, params, ctes, None)?, - Value::Bool(true) - ) { - filtered.push(row); - } - } - dataset.set_rows(filtered); - } - Ok(dataset) - } - - fn evaluate_from_clause( - &self, - from: &[FromItem], - params: &[Value], - ctes: &BTreeMap, - scope_dataset: &Dataset, - scope_row: &[Value], - ) -> Result { - if from.is_empty() { - return Ok(Dataset::with_rows(Vec::new(), vec![Vec::new()])); - } - let mut iter = from.iter(); - let cross_constraint = JoinConstraint::On(Expr::Literal(Value::Bool(true))); - // Invariant: iterator is non-empty due to the guard above. - let mut current = self.evaluate_from_item_in_scope( - iter.next().expect("first FROM item"), - params, - ctes, - scope_dataset, - scope_row, - )?; - for item in iter { - current = if from_item_is_lateral(item) { - self.evaluate_join_with_lateral_right( - current, - item, - JoinKind::Inner, - &cross_constraint, - params, - ctes, - scope_dataset, - scope_row, - )? - } else { - let right = - self.evaluate_from_item_in_scope(item, params, ctes, scope_dataset, scope_row)?; - nested_loop_join( - current, - right, - JoinKind::Inner, - &cross_constraint, - self, - params, - ctes, - )? - }; - } - Ok(current) - } - - fn try_indexed_scan( - &self, - select: &Select, - params: &[Value], - ctes: &BTreeMap, - ) -> Result> { - let Some(filter) = &select.filter else { - return Ok(None); - }; - if select.from.len() != 1 { - return Ok(None); - } - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if ctes.contains_key(name) - || self - .visible_view(name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(name) - { - return Ok(None); - } - let Some(table) = self.table_schema(name) else { - return Ok(None); - }; - if !generated_columns_are_stored(table) { - return Ok(None); - } - let row_source = self.table_row_source(name); - - if let Some(fulltext_lookup) = simple_fulltext_lookup(filter) { - let index_value = self.eval_expr( - fulltext_lookup.index_name_expr, - &Dataset::empty(), - &[], - params, - ctes, - None, - )?; - let query_value = self.eval_expr( - fulltext_lookup.query_expr, - &Dataset::empty(), - &[], - params, - ctes, - None, - )?; - let Some(index_name) = expect_text_arg("FULLTEXT_MATCH", "first", &index_value)? else { - return Ok(Some(Dataset::with_rows( - table_bindings_with_hidden_row_id(table, alias.as_deref().unwrap_or(name)), - Vec::new(), - ))); - }; - let Some(query_text) = expect_text_arg("FULLTEXT_MATCH", "second", &query_value)? - else { - return Ok(Some(Dataset::with_rows( - table_bindings_with_hidden_row_id(table, alias.as_deref().unwrap_or(name)), - Vec::new(), - ))); - }; - if let Some(index_schema) = self.catalog.index(index_name) { - if identifiers_equal(&index_schema.table_name, name) - && index_schema.fresh - && index_schema.kind == IndexKind::FullText - { - if let Some(RuntimeIndex::FullText { index }) = self.index(&index_schema.name) { - let row_ids = index - .search(query_text) - .map_err(|error| DbError::sql(error.message))? - .into_iter() - .filter_map(|hit| i64::try_from(hit.row_id).ok()) - .collect::>(); - return self - .dataset_from_row_id_set( - table, - row_source, - alias, - RuntimeRowIdSet::Many(&row_ids), - true, - ) - .map(Some); - } - } - } - } - - if let Some(spatial_lookup) = simple_spatial_lookup(filter) { - if !matches_filter_binding(name, alias, spatial_lookup.table_qualifier) { - return Ok(None); - } - if let Some(index) = self.catalog.indexes.values().find(|index| { - identifiers_equal(&index.table_name, name) - && index.fresh - && index.kind == IndexKind::Spatial - && index.predicate_sql.is_none() - && index.columns.len() == 1 - && index.columns[0] - .column_name - .as_deref() - .is_some_and(|index_column| { - identifiers_equal(index_column, spatial_lookup.column_name) - }) - && index.columns[0].expression_sql.is_none() - }) { - let query_value = self.eval_expr( - spatial_lookup.value_expr, - &Dataset::empty(), - &[], - params, - ctes, - None, - )?; - let Some((query_is_geography, query_spatial)) = - spatial_value_from_db(&query_value)? - else { - return Ok(None); - }; - let Some(RuntimeIndex::Spatial { index: spatial }) = self.index(&index.name) else { - return Ok(None); - }; - match (spatial.backend(), query_is_geography) { - (SpatialIndexBackend::GeographyS2, true) - | (SpatialIndexBackend::GeometryQuadCell, false) => {} - _ => return Ok(None), - } - let mut envelope = - SpatialEnvelope::from_value(&query_spatial).map_err(spatial_error)?; - if let Some(radius_expr) = spatial_lookup.radius_expr { - let radius_value = - self.eval_expr(radius_expr, &Dataset::empty(), &[], params, ctes, None)?; - let radius = numeric_value_as_f64("ST_DWithin", "third", &radius_value)?; - envelope = match spatial.backend() { - SpatialIndexBackend::GeographyS2 => { - envelope.expand_geography_meters(radius) - } - SpatialIndexBackend::GeometryQuadCell => envelope.expand_planar(radius), - }; - } - let row_ids = spatial.candidate_row_ids(envelope); - return self - .dataset_from_row_id_set( - table, - row_source, - alias, - RuntimeRowIdSet::Many(&row_ids), - false, - ) - .map(Some); - } - } - - if let Some((table_qualifier, column_name, value_expr)) = simple_btree_lookup(filter) { - if !matches_filter_binding(name, alias, table_qualifier) { - return Ok(None); - } - if let Some(index) = self.catalog.indexes.values().find(|index| { - identifiers_equal(&index.table_name, name) - && index.fresh - && index.kind == IndexKind::Btree - && index.predicate_sql.is_none() - && index.columns.len() == 1 - && index.columns[0] - .column_name - .as_deref() - .is_some_and(|index_column| identifiers_equal(index_column, column_name)) - && index.columns[0].expression_sql.is_none() - }) { - let value = - self.eval_expr(value_expr, &Dataset::empty(), &[], params, ctes, None)?; - if let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) { - let row_ids = keys.row_ids_for_value_set(&value)?; - if let Some(ref tracing) = self.tracing { - tracing.record_index_usage( - name, - &index.name, - "btree", - crate::tracing::index_usage::IndexUsageKind::Read, - ); - } - return self - .dataset_from_row_id_set(table, row_source, alias, row_ids, false) - .map(Some); - } - } - } - - if let Some(row_ids) = - self.trigram_candidate_row_ids_for_filter(name, alias, filter, params, ctes)? - { - return self - .dataset_from_row_id_set( - table, - row_source, - alias, - RuntimeRowIdSet::Many(&row_ids), - false, - ) - .map(Some); - } - - Ok(None) - } - - fn try_fulltext_bm25_top_k_select( - &self, - select: &Select, - order_by: &[crate::sql::ast::OrderBy], - limit: Option<&Expr>, - offset: Option<&Expr>, - params: &[Value], - ctes: &BTreeMap, - ) -> Result> { - if offset.is_some() - || select.distinct - || !select.distinct_on.is_empty() - || !select.group_by.is_empty() - || select.having.is_some() - || select.from.len() != 1 - || order_by.len() != 1 - { - return Ok(None); - } - let Some(limit_expr) = limit else { - return Ok(None); - }; - let limit = self.eval_constant_i64(limit_expr, params, ctes)?; - if limit <= 0 { - return Ok(None); - } - let Ok(limit) = usize::try_from(limit) else { - return Ok(None); - }; - let FromItem::Table { name, alias } = &select.from[0] else { - return Ok(None); - }; - if ctes.contains_key(name) - || self - .visible_view(name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(name) - { - return Ok(None); - } - let Some(table_schema) = self.table_schema(name) else { - return Ok(None); - }; - if !generated_columns_are_stored(table_schema) { - return Ok(None); - } - let Some(row_source) = self.table_row_source(name) else { - return Ok(None); - }; - let binding_name = alias.as_deref().unwrap_or(name.as_str()); - let Some(fulltext_lookup) = exact_fulltext_lookup(select.filter.as_ref()) else { - return Ok(None); - }; - let index_value = self.eval_expr( - fulltext_lookup.index_name_expr, - &Dataset::empty(), - &[], - params, - ctes, - None, - )?; - let query_value = self.eval_expr( - fulltext_lookup.query_expr, - &Dataset::empty(), - &[], - params, - ctes, - None, - )?; - let Some(index_name) = expect_text_arg("FULLTEXT_MATCH", "first", &index_value)? else { - return Ok(None); - }; - let Some(query_text) = expect_text_arg("FULLTEXT_MATCH", "second", &query_value)? else { - return Ok(None); - }; - let Some(index_schema) = self.catalog.index(index_name) else { - return Ok(None); - }; - if !identifiers_equal(&index_schema.table_name, name) - || !index_schema.fresh - || index_schema.kind != IndexKind::FullText - { - return Ok(None); - } - if !order_by[0].descending { - return Ok(None); - } - let Some(RuntimeIndex::FullText { index }) = self.index(&index_schema.name) else { - return Ok(None); - }; - - enum ProjectionKind { - Column(usize), - Score, - } - - let mut projection_kinds = Vec::with_capacity(select.projection.len()); - let mut column_names = Vec::with_capacity(select.projection.len()); - let mut score_alias = None; - let mut score_expr = None; - for (item_index, item) in select.projection.iter().enumerate() { - match item { - SelectItem::Expr { expr, alias } => match expr { - Expr::Column { - table: column_table, - column, - } => { - let Some(column_index) = simple_expression_projection_column_index( - table_schema, - name, - binding_name, - column_table.as_deref(), - column, - ) else { - return Ok(None); - }; - projection_kinds.push(ProjectionKind::Column(column_index)); - column_names.push(alias.clone().unwrap_or_else(|| column.clone())); - } - Expr::Function { name, args } - if name.eq_ignore_ascii_case("bm25") && args.len() == 1 => - { - if score_expr.is_some() { - return Ok(None); - } - if !order_by_matches_alias_or_projection( - &order_by[0], - alias.as_deref(), - expr, - true, - ) { - return Ok(None); - } - score_alias = alias.clone(); - score_expr = Some(expr); - projection_kinds.push(ProjectionKind::Score); - column_names.push( - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, item_index + 1)), - ); - } - _ => return Ok(None), - }, - SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => return Ok(None), - } - } - let Some(score_expr) = score_expr else { - return Ok(None); - }; - if score_alias.is_none() - && !order_by_matches_alias_or_projection(&order_by[0], None, score_expr, true) - { - return Ok(None); - } - let Expr::Function { args, .. } = score_expr else { - return Ok(None); - }; - let score_index_value = - self.eval_expr(&args[0], &Dataset::empty(), &[], params, ctes, None)?; - let Some(score_index_name) = expect_text_arg("BM25", "first", &score_index_value)? else { - return Ok(None); - }; - if !identifiers_equal(score_index_name, index_name) { - return Ok(None); - } - - let hits = index - .search_top_k(query_text, limit) - .map_err(|error| DbError::sql(error.message))?; - let mut rows = Vec::with_capacity(hits.len()); - for hit in hits { - let Some(row_id) = i64::try_from(hit.row_id).ok() else { - continue; - }; - let Some(row) = row_source.row_by_id(row_id)? else { - continue; - }; - let mut values = Vec::with_capacity(projection_kinds.len()); - for projection_kind in &projection_kinds { - match projection_kind { - ProjectionKind::Column(index) => { - let Some(value) = row.values().get(*index) else { - return Err(DbError::internal( - "fulltext fast path projection index is out of bounds", - )); - }; - values.push(value.clone()); - } - ProjectionKind::Score => values.push(Value::Float64(hit.score)), - } - } - rows.push(values); - } - - let columns = column_names - .into_iter() - .map(|name| ColumnBinding::visible(None, name)) - .collect(); - Ok(Some(Dataset::with_rows(columns, rows))) - } - - fn trigram_candidate_row_ids_for_filter( - &self, - table_name: &str, - alias: &Option, - filter: &Expr, - params: &[Value], - ctes: &BTreeMap, - ) -> Result>> { - let Some(lookup) = simple_trigram_lookup(filter) else { - return Ok(None); - }; - if !matches_filter_binding(table_name, alias, lookup.table_qualifier) { - return Ok(None); - } - let Some(index_schema) = self.catalog.indexes.values().find(|index| { - identifiers_equal(&index.table_name, table_name) - && index.fresh - && index.kind == IndexKind::Trigram - && index.predicate_sql.is_none() - && index.columns.len() == 1 - && index.columns[0] - .column_name - .as_deref() - .is_some_and(|index_column| identifiers_equal(index_column, lookup.column_name)) - }) else { - return Ok(None); - }; - let pattern = self.eval_expr( - lookup.pattern_expr, - &Dataset::empty(), - &[], - params, - ctes, - None, - )?; - let Value::Text(pattern) = pattern else { - return Ok(None); - }; - let Some(RuntimeIndex::Trigram { index }) = self.index(&index_schema.name) else { - return Ok(None); - }; - if !index.planner_may_use_index() { - return Ok(None); - } - let row_ids = match index.query_candidates(&pattern, lookup.has_additional_filter)? { - TrigramQueryResult::Candidates(ids) | TrigramQueryResult::Capped(ids) => ids - .into_iter() - .filter_map(|row_id| i64::try_from(row_id).ok()) - .collect::>(), - TrigramQueryResult::FallbackTooShort - | TrigramQueryResult::FallbackRequiresAdditionalFilter - | TrigramQueryResult::RebuildRequired => return Ok(None), - }; - Ok(Some(row_ids)) - } - - fn evaluate_values_body( - &self, - rows: &[Vec], - params: &[Value], - ctes: &BTreeMap, - ) -> Result { - self.evaluate_values_body_inner(rows, params, ctes, &Dataset::empty(), &[]) - } - - fn evaluate_values_body_with_outer( - &self, - rows: &[Vec], - params: &[Value], - ctes: &BTreeMap, - outer_dataset: &Dataset, - outer_row: &[Value], - ) -> Result { - self.evaluate_values_body_inner(rows, params, ctes, outer_dataset, outer_row) - } - - fn evaluate_values_body_inner( - &self, - rows: &[Vec], - params: &[Value], - ctes: &BTreeMap, - scope_dataset: &Dataset, - scope_row: &[Value], - ) -> Result { - let width = rows.first().map_or(0, Vec::len); - let mut columns = Vec::with_capacity(width); - if let Some(first_row) = rows.first() { - for (index, expr) in first_row.iter().enumerate() { - columns.push(ColumnBinding::visible( - None, - infer_expr_name(expr, index + 1), - )); - } - } - - let mut result_rows = Vec::with_capacity(rows.len()); - for row in rows { - if row.len() != width { - return Err(DbError::sql( - "VALUES rows must all have the same number of columns", - )); - } - let values = row - .iter() - .map(|expr| self.eval_expr(expr, scope_dataset, scope_row, params, ctes, None)) - .collect::>>()?; - result_rows.push(values); - } - Ok(Dataset::with_rows(columns, result_rows)) - } - - fn try_spatial_join( - &self, - select: &Select, - params: &[Value], - ctes: &BTreeMap, - ) -> Result> { - if select.from.len() != 1 { - return Ok(None); - } - let FromItem::Join { - left, - right, - kind: JoinKind::Inner, - constraint, - } = &select.from[0] - else { - return Ok(None); - }; - let JoinConstraint::On(on) = constraint else { - return Ok(None); - }; - let (left_name, left_alias) = match &**left { - FromItem::Table { name, alias } => (name, alias), - _ => return Ok(None), - }; - let (right_name, right_alias) = match &**right { - FromItem::Table { name, alias } => (name, alias), - _ => return Ok(None), - }; - if ctes.contains_key(left_name) - || ctes.contains_key(right_name) - || self - .visible_view(left_name, NameResolutionScope::Session) - .is_some() - || self - .visible_view(right_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(left_name) - || self.visible_table_is_temporary(right_name) - { - return Ok(None); - } - - let left_binding = TableBindingRef { - name: left_name, - alias: left_alias, - }; - let right_binding = TableBindingRef { - name: right_name, - alias: right_alias, - }; - let Some(join_predicate) = simple_spatial_join_predicate(on, left_binding, right_binding) - else { - return Ok(None); - }; - - for (indexed_ref, probe_ref) in [ - (join_predicate.left, join_predicate.right), - (join_predicate.right, join_predicate.left), - ] { - let Some((indexed_table, probe_table, indexed_on_left)) = - spatial_join_argument_orientation( - left_binding, - right_binding, - indexed_ref, - probe_ref, - ) - else { - continue; - }; - if let Some(dataset) = self.try_spatial_join_orientation(SpatialJoinOrientation { - indexed_table, - indexed_ref, - probe_table, - probe_ref, - indexed_on_left, - left_alias, - right_alias, - constraint, - radius_expr: join_predicate.radius_expr, - params, - ctes, - })? { - return Ok(Some(dataset)); - } - } - - Ok(None) - } - - fn try_spatial_join_orientation( - &self, - plan: SpatialJoinOrientation<'_>, - ) -> Result> { - if !matches_table_binding(plan.indexed_table, plan.indexed_ref.table) - || !matches_table_binding(plan.probe_table, plan.probe_ref.table) - { - return Ok(None); - } - let Some(index_schema) = - self.spatial_index_for_table_column(plan.indexed_table.name, plan.indexed_ref.column) - else { - return Ok(None); - }; - let indexed_table = self.table_schema(plan.indexed_table.name).ok_or_else(|| { - DbError::sql(format!("unknown table or view {}", plan.indexed_table.name)) - })?; - let probe_table = self.table_schema(plan.probe_table.name).ok_or_else(|| { - DbError::sql(format!("unknown table or view {}", plan.probe_table.name)) - })?; - if !generated_columns_are_stored(indexed_table) - || !generated_columns_are_stored(probe_table) - { - return Ok(None); - } - let Some(indexed_source) = self.table_row_source(plan.indexed_table.name) else { - return Ok(None); - }; - let Some(probe_source) = self.table_row_source(plan.probe_table.name) else { - return Ok(None); - }; - let Some(probe_column_index) = schema_column_index(probe_table, plan.probe_ref.column) - else { - return Ok(None); - }; - let Some(RuntimeIndex::Spatial { index: spatial }) = self.index(&index_schema.name) else { - return Ok(None); - }; - - let left_table = if plan.indexed_on_left { - indexed_table - } else { - probe_table - }; - let right_table = if plan.indexed_on_left { - probe_table - } else { - indexed_table - }; - let left_columns = table_output_columns(left_table, plan.left_alias); - let right_columns = table_output_columns(right_table, plan.right_alias); - let mut eval_columns = left_columns.clone(); - eval_columns.extend(right_columns.clone()); - let eval_dataset = Dataset::with_rows(eval_columns, Vec::new()); - let eval_context = JoinEvalContext { - dataset: &eval_dataset, - runtime: self, - params: plan.params, - ctes: plan.ctes, - }; - let columns = join_output_columns( - &Dataset::with_rows(left_columns, Vec::new()), - &Dataset::with_rows(right_columns, Vec::new()), - &[], - ); - let mut rows = Vec::new(); - - for probe_row in probe_source.rows() { - let probe_row = probe_row?; - let Some(probe_value) = probe_row.values().get(probe_column_index) else { - return Err(DbError::internal( - "spatial join probe row is shorter than schema", - )); - }; - let Some((probe_is_geography, probe_spatial)) = spatial_value_from_db(probe_value)? - else { - continue; - }; - match (spatial.backend(), probe_is_geography) { - (SpatialIndexBackend::GeographyS2, true) - | (SpatialIndexBackend::GeometryQuadCell, false) => {} - _ => return Ok(None), - } - let mut envelope = - SpatialEnvelope::from_value(&probe_spatial).map_err(spatial_error)?; - if let Some(radius_expr) = plan.radius_expr { - let radius_value = self.eval_expr( - radius_expr, - &Dataset::empty(), - &[], - plan.params, - plan.ctes, - None, - )?; - let radius = numeric_value_as_f64("ST_DWithin", "third", &radius_value)?; - envelope = match spatial.backend() { - SpatialIndexBackend::GeographyS2 => envelope.expand_geography_meters(radius), - SpatialIndexBackend::GeometryQuadCell => envelope.expand_planar(radius), - }; - } - - for indexed_row_id in spatial.candidate_row_ids(envelope) { - let Some(indexed_row) = indexed_source.row_by_id(indexed_row_id)? else { - continue; - }; - let (left_values, right_values) = if plan.indexed_on_left { - (indexed_row.values(), probe_row.values()) - } else { - (probe_row.values(), indexed_row.values()) - }; - let mut eval_row = left_values.to_vec(); - eval_row.extend_from_slice(right_values); - if join_rows_match( - plan.constraint, - &[], - &eval_row, - left_values, - right_values, - &eval_context, - )? { - rows.push(join_output_row(left_values, right_values, &[])?); - } - } - } - - Ok(Some(Dataset::with_rows(columns, rows))) - } - - fn spatial_index_for_table_column( - &self, - table_name: &str, - column_name: &str, - ) -> Option<&IndexSchema> { - self.catalog.indexes.values().find(|index| { - identifiers_equal(&index.table_name, table_name) - && index.fresh - && index.kind == IndexKind::Spatial - && index.predicate_sql.is_none() - && index.columns.len() == 1 - && index.columns[0].expression_sql.is_none() - && index.columns[0] - .column_name - .as_deref() - .is_some_and(|indexed| identifiers_equal(indexed, column_name)) - }) - } - - fn try_indexed_join( - &self, - select: &Select, - params: &[Value], - ctes: &BTreeMap, - ) -> Result> { - let Some(filter) = &select.filter else { - return Ok(None); - }; - if select.from.len() != 1 { - return Ok(None); - } - let FromItem::Join { - left, - right, - kind: JoinKind::Inner, - constraint: JoinConstraint::On(on), - } = &select.from[0] - else { - return Ok(None); - }; - let (left_name, left_alias) = match &**left { - FromItem::Table { name, alias } => (name, alias), - _ => return Ok(None), - }; - let (right_name, right_alias) = match &**right { - FromItem::Table { name, alias } => (name, alias), - _ => return Ok(None), - }; - if ctes.contains_key(left_name) - || ctes.contains_key(right_name) - || self - .visible_view(left_name, NameResolutionScope::Session) - .is_some() - || self - .visible_view(right_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(left_name) - || self.visible_table_is_temporary(right_name) - { - return Ok(None); - } - - let Some((filter_table, filter_column, value_expr)) = simple_btree_lookup(filter) else { - return Ok(None); - }; - let Some(join_equalities) = simple_join_equalities(on) else { - return Ok(None); - }; - - let left_binding = TableBindingRef { - name: left_name, - alias: left_alias, - }; - let right_binding = TableBindingRef { - name: right_name, - alias: right_alias, - }; - - if matches_table_binding(left_binding, filter_table) { - let Some((filtered_join_columns, probe_join_columns)) = - orient_join_equalities(&join_equalities, left_binding, right_binding) - else { - return Ok(None); - }; - let Some(left_dataset) = self.indexed_table_lookup( - left_name, - left_alias, - filter_column, - value_expr, - params, - ctes, - )? - else { - return Ok(None); - }; - return self.indexed_inner_join_filtered(IndexedJoinPlan { - filtered_table: left_binding, - filtered_dataset: &left_dataset, - filtered_join_columns, - probe_table: right_binding, - probe_join_columns, - filtered_on_left: true, - }); - } - - if matches_table_binding(right_binding, filter_table) { - let Some((filtered_join_columns, probe_join_columns)) = - orient_join_equalities(&join_equalities, right_binding, left_binding) - else { - return Ok(None); - }; - let Some(right_dataset) = self.indexed_table_lookup( - right_name, - right_alias, - filter_column, - value_expr, - params, - ctes, - )? - else { - return Ok(None); - }; - return self.indexed_inner_join_filtered(IndexedJoinPlan { - filtered_table: right_binding, - filtered_dataset: &right_dataset, - filtered_join_columns, - probe_table: left_binding, - probe_join_columns, - filtered_on_left: false, - }); - } - - Ok(None) - } - - fn try_indexed_prefiltered_inner_join_tree( - &self, - select: &Select, - params: &[Value], - ctes: &BTreeMap, - ) -> Result> { - let Some(filter) = &select.filter else { - return Ok(None); - }; - if select.from.len() != 1 { - return Ok(None); - } - let Some((Some(filter_table), filter_column, value_expr)) = simple_btree_lookup(filter) - else { - return Ok(None); - }; - if !from_item_is_all_inner_table_joins(&select.from[0]) { - return Ok(None); - } - - let mut applied_prefilter = false; - let dataset = self.evaluate_from_item_with_indexed_prefilter( - &select.from[0], - params, - ctes, - filter_table, - filter_column, - value_expr, - &mut applied_prefilter, - )?; - if applied_prefilter { - Ok(Some(dataset)) - } else { - Ok(None) - } - } - - #[allow(clippy::too_many_arguments)] - fn evaluate_from_item_with_indexed_prefilter( - &self, - item: &FromItem, - params: &[Value], - ctes: &BTreeMap, - filter_table: &str, - filter_column: &str, - value_expr: &Expr, - applied_prefilter: &mut bool, - ) -> Result { - match item { - FromItem::Table { name, alias } => { - if !*applied_prefilter - && matches_filter_binding(name, alias, Some(filter_table)) - && !ctes.contains_key(name) - && self - .visible_view(name, NameResolutionScope::Session) - .is_none() - && !self.visible_table_is_temporary(name) - { - if let Some(dataset) = self.indexed_table_lookup( - name, - alias, - filter_column, - value_expr, - params, - ctes, - )? { - *applied_prefilter = true; - return Ok(dataset); - } - } - self.evaluate_from_item(item, params, ctes) - } - FromItem::Join { - left, - right, - kind, - constraint, - } => { - let left_dataset = self.evaluate_from_item_with_indexed_prefilter( - left, - params, - ctes, - filter_table, - filter_column, - value_expr, - applied_prefilter, - )?; - if matches!(kind, JoinKind::Inner | JoinKind::Left) { - if let Some(dataset) = self.try_indexed_equi_join_with_right_table( - &left_dataset, - right, - *kind, - constraint, - ctes, - )? { - return Ok(dataset); - } - if let Some(dataset) = self.try_indexed_equi_join_with_right_cte( - &left_dataset, - right, - constraint, - *kind, - ctes, - )? { - return Ok(dataset); - } - } - let right_dataset = self.evaluate_from_item_with_indexed_prefilter( - right, - params, - ctes, - filter_table, - filter_column, - value_expr, - applied_prefilter, - )?; - nested_loop_join( - left_dataset, - right_dataset, - *kind, - constraint, - self, - params, - ctes, - ) - } - _ => self.evaluate_from_item(item, params, ctes), - } - } - - fn indexed_table_lookup( - &self, - table_name: &str, - alias: &Option, - column_name: &str, - value_expr: &Expr, - params: &[Value], - ctes: &BTreeMap, - ) -> Result> { - let table = self - .table_schema(table_name) - .ok_or_else(|| DbError::sql(format!("unknown table or view {table_name}")))?; - if !generated_columns_are_stored(table) { - return Ok(None); - } - let row_source = self.table_row_source(table_name); - if row_id_alias_column_name(table) - .is_some_and(|row_id_column| identifiers_equal(row_id_column, column_name)) - { - let Some(row_source) = row_source else { - return Ok(None); - }; - let value = self.eval_expr(value_expr, &Dataset::empty(), &[], params, ctes, None)?; - let row_ids = match value { - Value::Int64(row_id) => RuntimeRowIdSet::Single(row_id), - _ => RuntimeRowIdSet::Empty, - }; - return self - .dataset_from_row_id_set(table, Some(row_source), alias, row_ids, false) - .map(Some); - } - let Some(index) = self.catalog.indexes.values().find(|index| { - identifiers_equal(&index.table_name, table_name) - && index.fresh - && index.kind == IndexKind::Btree - && index.predicate_sql.is_none() - && index.columns.len() == 1 - && index.columns[0] - .column_name - .as_deref() - .is_some_and(|index_column| identifiers_equal(index_column, column_name)) - && index.columns[0].expression_sql.is_none() - }) else { - return Ok(None); - }; - - let value = self.eval_expr(value_expr, &Dataset::empty(), &[], params, ctes, None)?; - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { - return Ok(None); - }; - let row_ids = keys.row_ids_for_value_set(&value)?; - self.dataset_from_row_id_set(table, row_source, alias, row_ids, false) - .map(Some) - } - - fn indexed_inner_join_filtered(&self, plan: IndexedJoinPlan<'_>) -> Result> { - let filtered_table = self.table_schema(plan.filtered_table.name).ok_or_else(|| { - DbError::sql(format!( - "unknown table or view {}", - plan.filtered_table.name - )) - })?; - let probe_table = self.table_schema(plan.probe_table.name).ok_or_else(|| { - DbError::sql(format!("unknown table or view {}", plan.probe_table.name)) - })?; - if !generated_columns_are_stored(filtered_table) - || !generated_columns_are_stored(probe_table) - { - return Ok(None); - } - let probe_source = self.visible_table_row_source(plan.probe_table.name); - let mut filtered_join_indexes = Vec::with_capacity(plan.filtered_join_columns.len()); - for filtered_join_column in &plan.filtered_join_columns { - let filtered_join_index = filtered_table - .columns - .iter() - .position(|column| identifiers_equal(&column.name, filtered_join_column)) - .ok_or_else(|| DbError::sql(format!("unknown column {filtered_join_column}")))?; - filtered_join_indexes.push(filtered_join_index); - } - let is_probe_rowid_alias = plan.probe_join_columns.len() == 1 - && crate::exec::dml::row_id_alias_column_name(probe_table) - .is_some_and(|name| identifiers_equal(name, plan.probe_join_columns[0])); - - let mut probe_hash_join_indexes = None; - let (probe_index, ordered_filtered_join_indexes) = if is_probe_rowid_alias { - (None, filtered_join_indexes) - } else if let Some((probe_index, ordered_filtered_join_indexes)) = - self.catalog.indexes.values().find_map(|index| { - if !identifiers_equal(&index.table_name, plan.probe_table.name) - || !index.fresh - || index.kind != IndexKind::Btree - || index.predicate_sql.is_some() - || index.columns.len() != plan.probe_join_columns.len() - { - return None; - } - let mut ordered_filtered_join_indexes = Vec::with_capacity(index.columns.len()); - for index_column in &index.columns { - if index_column.expression_sql.is_some() { - return None; - } - let index_column_name = index_column.column_name.as_deref()?; - let join_position = plan.probe_join_columns.iter().position(|join_column| { - identifiers_equal(join_column, index_column_name) - })?; - ordered_filtered_join_indexes.push(filtered_join_indexes[join_position]); - } - Some((index, ordered_filtered_join_indexes)) - }) - { - (Some(probe_index), ordered_filtered_join_indexes) - } else { - let mut ordered_probe_join_indexes = Vec::with_capacity(plan.probe_join_columns.len()); - for probe_join_column in &plan.probe_join_columns { - let Some(probe_join_index) = schema_column_index(probe_table, probe_join_column) - else { - return Ok(None); - }; - ordered_probe_join_indexes.push(probe_join_index); - } - probe_hash_join_indexes = Some(ordered_probe_join_indexes); - (None, filtered_join_indexes) - }; - let keys = if let Some(index) = probe_index { - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { - return Ok(None); - }; - Some(keys) - } else { - None - }; - let probe_hash_rows = if let Some(probe_join_indexes) = probe_hash_join_indexes.as_ref() { - let Some(probe_source) = probe_source else { - return Ok(None); - }; - let mut hashed = SimpleJoinHashRows::new(); - for probe_row in probe_source.rows() { - let probe_row = probe_row?; - let Some(join_key) = - simple_join_key_from_indexes(probe_row.values(), probe_join_indexes)? - else { - continue; - }; - hashed - .entry(join_key) - .or_default() - .push((probe_row.row_id(), probe_row.values().to_vec())); - } - Some(hashed) - } else { - None - }; - let use_probe_row_position_map = probe_hash_rows.is_none() - && probe_source - .map_or(0, |source| source.row_count()) - .saturating_mul(plan.filtered_dataset.rows.len()) - > 8_192; - let probe_row_positions = if use_probe_row_position_map { - let mut positions = Int64Map::::default(); - for (position, row) in probe_source - .map(|source| source.rows()) - .unwrap_or_else(TableRowIter::empty) - .enumerate() - { - positions.insert(row?.row_id(), position); - } - Some(positions) - } else { - None - }; - - let probe_columns = probe_table - .columns - .iter() - .map(|column| { - ColumnBinding::visible_source( - Some(plan.probe_table.binding_name().to_string()), - Some(plan.probe_table.name.to_string()), - column.name.clone(), - ) - }) - .collect::>(); - let mut columns = if plan.filtered_on_left { - plan.filtered_dataset.columns.clone() - } else { - probe_columns.clone() - }; - if plan.filtered_on_left { - columns.extend(probe_columns.clone()); - } else { - columns.extend(plan.filtered_dataset.columns.clone()); - } - let mut rows = Vec::new(); - for filtered_row in plan.filtered_dataset.rows.iter() { - let join_values = ordered_filtered_join_indexes - .iter() - .map(|index| { - filtered_row.get(*index).ok_or_else(|| { - DbError::internal("join row is shorter than filtered table schema") - }) - }) - .collect::>>()?; - if join_values - .iter() - .any(|join_value| matches!(join_value, Value::Null)) - { - continue; - } - if let Some(probe_hash_rows) = probe_hash_rows.as_ref() { - let join_key = Row::new(join_values.iter().cloned().cloned().collect()).encode()?; - let Some(matching_probe_rows) = probe_hash_rows.get(&join_key) else { - continue; - }; - for (_, probe_row) in matching_probe_rows { - let mut row = Vec::with_capacity(filtered_row.len() + probe_row.len()); - if plan.filtered_on_left { - row.extend_from_slice(filtered_row); - row.extend_from_slice(probe_row); - } else { - row.extend_from_slice(probe_row); - row.extend_from_slice(filtered_row); - } - rows.push(row); - } - continue; - } - let row_ids = if let Some(keys) = keys { - if join_values.len() == 1 { - keys.row_ids_for_value_set(join_values[0])? - } else { - keys.row_id_set_for_key(&RuntimeBtreeKey::Encoded(RuntimeEncodedKey::from_vec( - Row::new(join_values.into_iter().cloned().collect()).encode()?, - ))) - } - } else if join_values.len() == 1 { - match join_values[0] { - Value::Int64(val) => RuntimeRowIdSet::Single(*val), - _ => RuntimeRowIdSet::Empty, - } - } else { - RuntimeRowIdSet::Empty - }; - if row_ids.is_empty() { - continue; - } - row_ids.for_each(|row_id| { - let probe_row = if let Some(positions) = probe_row_positions.as_ref() { - let Some(probe_position) = positions.get(&row_id).copied() else { - return; - }; - match probe_source - .map(|source| source.row_at_position(probe_position)) - .transpose() - { - Ok(Some(Some(probe_row))) => probe_row.values().to_vec(), - Ok(Some(None)) | Ok(None) => return, - Err(_) => return, - } - } else { - match probe_source - .map(|source| source.row_by_id(row_id)) - .transpose() - { - Ok(Some(Some(probe_row))) => probe_row.values().to_vec(), - Ok(Some(None)) | Ok(None) => return, - Err(_) => return, - } - }; - let mut row = Vec::with_capacity(filtered_row.len() + probe_row.len()); - if plan.filtered_on_left { - row.extend_from_slice(filtered_row); - row.extend_from_slice(&probe_row); - } else { - row.extend_from_slice(&probe_row); - row.extend_from_slice(filtered_row); - } - rows.push(row); - }); - } - Ok(Some(Dataset::with_rows(columns, rows))) - } - - /// Indexed equi-join probe path. - /// - /// For each row in `left`, probes the right table via a b-tree index - /// (or the rowid alias) instead of doing a full O(|left| * |right|) - /// nested loop. Supported join kinds: - /// - /// * `Inner` — skips left rows with NULL join values and left rows - /// whose join value has no match. - /// * `Left` — preserves every left row, emitting a NULL-extended - /// right half when the left join value is NULL or has no match. - /// - /// Returns `Ok(None)` if any of the preconditions for the fast path - /// are not met (non-table right side, non-equi join, view/CTE/temp - /// table, etc.), in which case the caller falls back to the nested - /// loop join. - fn try_indexed_equi_join_with_right_table( - &self, - left: &Dataset, - right_item: &FromItem, - kind: JoinKind, - constraint: &JoinConstraint, - ctes: &BTreeMap, - ) -> Result> { - if !matches!( - kind, - JoinKind::Inner | JoinKind::Left | JoinKind::Right | JoinKind::Full - ) { - return Ok(None); - } - let JoinConstraint::On(on) = constraint else { - return Ok(None); - }; - let Some(join_equalities) = simple_join_equalities(on) else { - return Ok(None); - }; - let FromItem::Table { - name: right_name, - alias: right_alias, - } = right_item - else { - return Ok(None); - }; - if ctes.contains_key(right_name) { - return Ok(None); - } - if self - .visible_view(right_name, NameResolutionScope::Session) - .is_some() - || self.visible_table_is_temporary(right_name) - { - return Ok(None); - } - - let right_binding = TableBindingRef { - name: right_name, - alias: right_alias, - }; - let mut left_probe_refs = Vec::with_capacity(join_equalities.len()); - let mut right_join_columns = Vec::with_capacity(join_equalities.len()); - for (left_join_ref, right_join_ref) in join_equalities { - let (left_probe_ref, right_join_column) = - if matches_table_binding(right_binding, right_join_ref.table) { - (left_join_ref, right_join_ref.column) - } else if matches_table_binding(right_binding, left_join_ref.table) { - (right_join_ref, left_join_ref.column) - } else { - return Ok(None); - }; - left_probe_refs.push(left_probe_ref); - right_join_columns.push(right_join_column); - } - let mut left_join_indexes = Vec::with_capacity(left_probe_refs.len()); - for left_probe_ref in &left_probe_refs { - let Some(left_join_index) = - dataset_column_index(left, left_probe_ref.table, left_probe_ref.column) - else { - return Ok(None); - }; - left_join_indexes.push(left_join_index); - } - - let right_table = self - .table_schema(right_name) - .ok_or_else(|| DbError::sql(format!("unknown table or view {right_name}")))?; - if !generated_columns_are_stored(right_table) { - return Ok(None); - } - let right_source = self.visible_table_row_source(right_name); - let is_probe_rowid_alias = right_join_columns.len() == 1 - && crate::exec::dml::row_id_alias_column_name(right_table) - .is_some_and(|name| identifiers_equal(name, right_join_columns[0])); - - let mut right_hash_join_indexes = None; - let (probe_index, ordered_left_join_indexes) = if is_probe_rowid_alias { - (None, left_join_indexes) - } else if let Some((probe_index, ordered_left_join_indexes)) = - self.catalog.indexes.values().find_map(|index| { - if !identifiers_equal(&index.table_name, right_name) - || !index.fresh - || index.kind != IndexKind::Btree - || index.predicate_sql.is_some() - || index.columns.len() != right_join_columns.len() - { - return None; - } - let mut ordered_left_join_indexes = Vec::with_capacity(index.columns.len()); - for index_column in &index.columns { - if index_column.expression_sql.is_some() { - return None; - } - let index_column_name = index_column.column_name.as_deref()?; - let join_position = right_join_columns.iter().position(|join_column| { - identifiers_equal(join_column, index_column_name) - })?; - ordered_left_join_indexes.push(left_join_indexes[join_position]); - } - Some((index, ordered_left_join_indexes)) - }) - { - (Some(probe_index), ordered_left_join_indexes) - } else { - let mut ordered_right_join_indexes = Vec::with_capacity(right_join_columns.len()); - for right_join_column in &right_join_columns { - let Some(right_join_index) = schema_column_index(right_table, right_join_column) - else { - return Ok(None); - }; - ordered_right_join_indexes.push(right_join_index); - } - right_hash_join_indexes = Some(ordered_right_join_indexes); - (None, left_join_indexes) - }; - let keys = if let Some(index) = probe_index { - let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { - return Ok(None); - }; - Some(keys) - } else { - None - }; - let right_hash_rows = if let Some(right_join_indexes) = right_hash_join_indexes.as_ref() { - let Some(right_source) = right_source else { - return Ok(None); - }; - let mut hashed = SimpleJoinHashRows::new(); - for right_row in right_source.rows() { - let right_row = right_row?; - let Some(join_key) = - simple_join_key_from_indexes(right_row.values(), right_join_indexes)? - else { - continue; - }; - hashed - .entry(join_key) - .or_default() - .push((right_row.row_id(), right_row.values().to_vec())); - } - Some(hashed) - } else { - None - }; - - let use_right_row_position_map = right_hash_rows.is_none() - && right_source - .map_or(0, |source| source.row_count()) - .saturating_mul(left.rows.len()) - > 8_192; - let right_row_positions = if use_right_row_position_map { - let mut positions = Int64Map::::default(); - for (position, row) in right_source - .map(|source| source.rows()) - .unwrap_or_else(TableRowIter::empty) - .enumerate() - { - positions.insert(row?.row_id(), position); - } - Some(positions) - } else { - None - }; - - let right_binding_name = right_alias.clone().unwrap_or_else(|| right_name.clone()); - let mut columns = left.columns.clone(); - columns.extend(right_table.columns.iter().map(|column| { - ColumnBinding::visible_source( - Some(right_binding_name.clone()), - Some(right_name.clone()), - column.name.clone(), - ) - })); - let right_column_count = right_table.columns.len(); - let is_left_outer = matches!(kind, JoinKind::Left | JoinKind::Full); - let is_right_outer = matches!(kind, JoinKind::Right | JoinKind::Full); - let mut rows = Vec::new(); - let mut matched_right_row_ids = is_right_outer.then(Int64Map::<()>::default); - for left_row in left.rows.iter() { - let join_values = ordered_left_join_indexes - .iter() - .map(|index| { - left_row.get(*index).ok_or_else(|| { - DbError::internal("join row is shorter than the left input schema") - }) - }) - .collect::>>()?; - if join_values - .iter() - .any(|join_value| matches!(join_value, Value::Null)) - { - if is_left_outer { - let mut row = Vec::with_capacity(left_row.len() + right_column_count); - row.extend_from_slice(left_row); - row.extend(std::iter::repeat_n(Value::Null, right_column_count)); - rows.push(row); - } - continue; - } - let rows_before = rows.len(); - if let Some(right_hash_rows) = right_hash_rows.as_ref() { - let join_key = Row::new(join_values.iter().cloned().cloned().collect()).encode()?; - if let Some(matching_rows) = right_hash_rows.get(&join_key) { - for (row_id, right_values) in matching_rows { - if let Some(matched_right_row_ids) = matched_right_row_ids.as_mut() { - matched_right_row_ids.insert(*row_id, ()); - } - let mut row = Vec::with_capacity(left_row.len() + right_values.len()); - row.extend_from_slice(left_row); - row.extend_from_slice(right_values); - rows.push(row); - } - } - } else { - let row_ids = if let Some(keys) = keys { - if join_values.len() == 1 { - keys.row_ids_for_value_set(join_values[0])? - } else { - keys.row_id_set_for_key(&RuntimeBtreeKey::Encoded( - RuntimeEncodedKey::from_vec( - Row::new(join_values.into_iter().cloned().collect()).encode()?, - ), - )) - } - } else if join_values.len() == 1 { - match join_values[0] { - Value::Int64(val) => RuntimeRowIdSet::Single(*val), - _ => RuntimeRowIdSet::Empty, - } - } else { - RuntimeRowIdSet::Empty - }; - row_ids.for_each(|row_id| { - let right_values = if let Some(positions) = right_row_positions.as_ref() { - let Some(right_position) = positions.get(&row_id).copied() else { - return; - }; - match right_source - .map(|source| source.row_at_position(right_position)) - .transpose() - { - Ok(Some(Some(right_row))) => right_row.values().to_vec(), - Ok(Some(None)) | Ok(None) => return, - Err(_) => return, - } - } else { - match right_source - .map(|source| source.row_by_id(row_id)) - .transpose() - { - Ok(Some(Some(right_row))) => right_row.values().to_vec(), - Ok(Some(None)) | Ok(None) => return, - Err(_) => return, - } - }; - if let Some(matched_right_row_ids) = matched_right_row_ids.as_mut() { - matched_right_row_ids.insert(row_id, ()); - } - let mut row = Vec::with_capacity(left_row.len() + right_values.len()); - row.extend_from_slice(left_row); - row.extend_from_slice(&right_values); - rows.push(row); - }); - } - if is_left_outer && rows.len() == rows_before { - let mut row = Vec::with_capacity(left_row.len() + right_column_count); - row.extend_from_slice(left_row); - row.extend(std::iter::repeat_n(Value::Null, right_column_count)); - rows.push(row); - } - } - if let Some(matched_right_row_ids) = matched_right_row_ids.as_ref() { - let left_nulls = vec![Value::Null; left.columns.len()]; - for right_row in right_source - .map(|source| source.rows()) - .unwrap_or_else(TableRowIter::empty) - { - let right_row = right_row?; - if matched_right_row_ids.contains_key(&right_row.row_id()) { - continue; - } - let mut row = Vec::with_capacity(left_nulls.len() + right_row.values().len()); - row.extend_from_slice(&left_nulls); - row.extend_from_slice(right_row.values()); - rows.push(row); - } - } - Ok(Some(Dataset::with_rows(columns, rows))) - } - - fn try_indexed_equi_join_with_right_cte( - &self, - left: &Dataset, - right_item: &FromItem, - constraint: &JoinConstraint, - kind: JoinKind, - ctes: &BTreeMap, - ) -> Result> { - let _ = self; - if !matches!(kind, JoinKind::Inner) { - return Ok(None); - } - let JoinConstraint::On(on) = constraint else { - return Ok(None); - }; - let Some(join_equalities) = simple_join_equalities(on) else { - return Ok(None); - }; - let FromItem::Table { - name: right_name, - alias: right_alias, - } = right_item - else { - return Ok(None); - }; - let Some(right_dataset) = ctes.get(right_name) else { - return Ok(None); - }; - - let right_binding = TableBindingRef { - name: right_name, - alias: right_alias, - }; - let mut left_probe_refs = Vec::with_capacity(join_equalities.len()); - let mut right_probe_refs = Vec::with_capacity(join_equalities.len()); - for (left_join_ref, right_join_ref) in join_equalities { - let (left_probe_ref, right_probe_ref) = - if matches_table_binding(right_binding, right_join_ref.table) { - (left_join_ref, right_join_ref) - } else if matches_table_binding(right_binding, left_join_ref.table) { - (right_join_ref, left_join_ref) - } else { - return Ok(None); - }; - left_probe_refs.push(left_probe_ref); - right_probe_refs.push(right_probe_ref); - } - - let mut left_join_indexes = Vec::with_capacity(left_probe_refs.len()); - for left_probe_ref in &left_probe_refs { - let Some(left_join_index) = - dataset_column_index(left, left_probe_ref.table, left_probe_ref.column) - else { - return Ok(None); - }; - left_join_indexes.push(left_join_index); - } - let mut right_join_indexes = Vec::with_capacity(right_probe_refs.len()); - let mut right_columns = right_dataset.columns.clone(); - if let Some(alias) = right_alias { - for column in &mut right_columns { - column.table = Some(alias.clone()); - } - } - for right_join_ref in &right_probe_refs { - let right_join_indexes_for_ref = right_columns - .iter() - .enumerate() - .filter(|(_, binding)| { - if !identifiers_equal(&binding.name, right_join_ref.column) { - return false; - } - if let Some(qualifier) = right_join_ref.table { - binding - .table - .as_deref() - .is_some_and(|table| identifiers_equal(table, qualifier)) - } else { - !binding.hidden - } - }) - .map(|(index, _)| index) - .collect::>(); - let [right_join_index] = right_join_indexes_for_ref.as_slice() else { - return Ok(None); - }; - right_join_indexes.push(*right_join_index); - } - - let mut hashed_right_rows: BTreeMap, Vec>> = BTreeMap::new(); - for right_row in right_dataset.rows.iter() { - let Some(join_key) = simple_join_key_from_indexes(right_row, &right_join_indexes)? - else { - continue; - }; - hashed_right_rows - .entry(join_key) - .or_default() - .push(right_row.clone()); - } - - let mut columns = left.columns.clone(); - columns.extend(right_columns); - let right_column_count = columns.len().saturating_sub(left.columns.len()); - - let mut rows = Vec::new(); - for left_row in left.rows.iter() { - let Some(join_key) = simple_join_key_from_indexes(left_row, &left_join_indexes)? else { - continue; - }; - if let Some(matching_rows) = hashed_right_rows.get(&join_key) { - for right_row in matching_rows { - let mut row = Vec::with_capacity(left_row.len() + right_column_count); - row.extend_from_slice(left_row); - row.extend_from_slice(right_row); - rows.push(row); - } - } - } - Ok(Some(Dataset::with_rows(columns, rows))) - } - - fn evaluate_from_item( - &self, - item: &FromItem, - params: &[Value], - ctes: &BTreeMap, - ) -> Result { - self.evaluate_from_item_in_scope(item, params, ctes, &Dataset::empty(), &[]) - } - - fn evaluate_from_item_in_scope( - &self, - item: &FromItem, - params: &[Value], - ctes: &BTreeMap, - scope_dataset: &Dataset, - scope_row: &[Value], - ) -> Result { - match item { - FromItem::Table { name, alias } => { - if let Some(dataset) = ctes.get(name) { - let mut columns = dataset.columns.clone(); - if let Some(alias) = alias { - for column in &mut columns { - column.table = Some(alias.clone()); - } - } - return Ok(dataset.share_rows(columns)); - } - if let Some(mut dataset) = self.compatibility_virtual_table(name)? { - if let Some(alias) = alias { - for column in &mut dataset.columns { - column.table = Some(alias.clone()); - } - } - return Ok(dataset); - } - if let Some(view) = self.visible_view(name, NameResolutionScope::Session) { - let query = self.cached_view_query(view)?; - let mut dataset = if view.temporary { - self.evaluate_query(query.as_ref(), params, ctes)? - } else { - let persistent_runtime = self.persistent_resolution_runtime(); - persistent_runtime.evaluate_query(query.as_ref(), params, ctes)? - }; - if let Some(alias) = alias { - for column in &mut dataset.columns { - column.table = Some(alias.clone()); - } - } else { - for column in &mut dataset.columns { - column.table = Some(view.name.clone()); - } - } - return Ok(dataset); - } - let table = self - .table_schema(name) - .ok_or_else(|| DbError::sql(format!("unknown table or view {name}")))?; - let row_source = self.visible_table_row_source(name).ok_or_else(|| { - DbError::internal(format!( - "table row source for {name} was not loaded before FROM evaluation" - )) - })?; - self.dataset_from_visible_row_source(table, row_source, alias) - } - FromItem::Subquery { - query, - alias, - column_names, - lateral, - } => { - let mut dataset = if *lateral || query_references_outer_scope(query, scope_dataset) - { - self.evaluate_query_with_outer(query, params, ctes, scope_dataset, scope_row)? - } else { - self.evaluate_query(query, params, ctes)? - }; - if !column_names.is_empty() { - if column_names.len() != dataset.columns.len() { - return Err(DbError::sql(format!( - "subquery alias {} expected {} column names but produced {} columns", - alias, - column_names.len(), - dataset.columns.len() - ))); - } - for (binding, column_name) in dataset.columns.iter_mut().zip(column_names) { - binding.name = column_name.clone(); - } - } - for column in &mut dataset.columns { - column.table = Some(alias.clone()); - } - Ok(dataset) - } - FromItem::Function { - name, - args, - alias, - lateral, - } => { - let eval_dataset = if *lateral { - scope_dataset - } else { - &Dataset::empty() - }; - let eval_row = if *lateral { scope_row } else { &[] }; - let values = args - .iter() - .map(|expr| self.eval_expr(expr, eval_dataset, eval_row, params, ctes, None)) - .collect::>>()?; - self.evaluate_table_function(name, values, alias) - } - FromItem::Join { - left, - right, - kind, - constraint, - } => { - let left = - self.evaluate_from_item_in_scope(left, params, ctes, scope_dataset, scope_row)?; - if from_item_is_lateral(right) { - return self.evaluate_join_with_lateral_right( - left, - right, - *kind, - constraint, - params, - ctes, - scope_dataset, - scope_row, - ); - } - if matches!( - kind, - JoinKind::Inner | JoinKind::Left | JoinKind::Right | JoinKind::Full - ) { - if let Some(dataset) = self.try_indexed_equi_join_with_right_table( - &left, right, *kind, constraint, ctes, - )? { - return Ok(dataset); - } - if let Some(dataset) = self.try_indexed_equi_join_with_right_cte( - &left, right, constraint, *kind, ctes, - )? { - return Ok(dataset); - } - } - let right = self.evaluate_from_item_in_scope( - right, - params, - ctes, - scope_dataset, - scope_row, - )?; - nested_loop_join(left, right, *kind, constraint, self, params, ctes) - } - } - } - - #[allow(clippy::too_many_arguments)] - fn evaluate_join_with_lateral_right( - &self, - left: Dataset, - right_item: &FromItem, - kind: JoinKind, - constraint: &JoinConstraint, - params: &[Value], - ctes: &BTreeMap, - scope_dataset: &Dataset, - scope_row: &[Value], - ) -> Result { - if matches!(kind, JoinKind::Right | JoinKind::Full) { - return Err(DbError::sql( - "LATERAL is only supported with INNER, LEFT, and CROSS joins", - )); - } - - let mut columns = left.columns.clone(); - let mut rows = Vec::new(); - for left_row in left.rows.iter() { - let left_single = Dataset::with_rows(left.columns.clone(), vec![left_row.clone()]); - let scope_with_left = - augment_dataset_with_outer_scope(left_single.clone(), scope_dataset, scope_row); - let scope_values = scope_with_left - .rows - .first() - .map(Vec::as_slice) - .unwrap_or(&[]); - let right = self.evaluate_from_item_in_scope( - right_item, - params, - ctes, - &scope_with_left, - scope_values, - )?; - let joined = - nested_loop_join(left_single, right, kind, constraint, self, params, ctes)?; - columns = joined.columns.clone(); - rows.extend(joined.into_rows()); - } - Ok(Dataset::with_rows(columns, rows)) - } - - fn evaluate_table_function( - &self, - name: &str, - values: Vec, - alias: &Option, - ) -> Result { - let table_name = alias.clone().unwrap_or_else(|| name.to_string()); - match name { - "json_each" | "pg_catalog.json_each" => { - self.evaluate_json_table_function(table_name, values, false) - } - "json_tree" | "pg_catalog.json_tree" => { - self.evaluate_json_table_function(table_name, values, true) - } - "generate_series" | "pg_catalog.generate_series" => { - self.evaluate_generate_series(table_name, values) - } - "pragma_table_info" | "main.pragma_table_info" | "temp.pragma_table_info" => { - self.evaluate_pragma_table_info_function(table_name, values, false) - } - "pragma_table_xinfo" | "main.pragma_table_xinfo" | "temp.pragma_table_xinfo" => { - self.evaluate_pragma_table_info_function(table_name, values, true) - } - "pragma_table_list" | "main.pragma_table_list" | "temp.pragma_table_list" => { - self.evaluate_pragma_table_list_function(table_name, values) - } - "pragma_index_list" | "main.pragma_index_list" | "temp.pragma_index_list" => { - self.evaluate_pragma_index_list_function(table_name, values) - } - "pragma_index_info" | "main.pragma_index_info" | "temp.pragma_index_info" => { - self.evaluate_pragma_index_info_function(table_name, values, false) - } - "pragma_index_xinfo" | "main.pragma_index_xinfo" | "temp.pragma_index_xinfo" => { - self.evaluate_pragma_index_info_function(table_name, values, true) - } - "pragma_foreign_key_list" - | "main.pragma_foreign_key_list" - | "temp.pragma_foreign_key_list" => { - self.evaluate_pragma_foreign_key_list_function(table_name, values) - } - "pragma_database_list" | "main.pragma_database_list" | "temp.pragma_database_list" => { - self.evaluate_pragma_database_list_function(table_name, values) - } - other => { - if let Some(dataset) = crate::extensions::evaluate_table_function_from_runtime( - self, other, values, table_name, - )? { - return Ok(dataset); - } - Err(DbError::sql(format!("unsupported table function {other}"))) - } - } - } - - fn compatibility_virtual_table(&self, name: &str) -> Result> { - let normalized = name.to_ascii_lowercase(); - let table_name = match normalized.as_str() { - "sqlite_schema" | "sqlite_master" | "main.sqlite_schema" | "main.sqlite_master" => { - return Ok(Some(self.sqlite_schema_dataset("sqlite_schema", false))); - } - "sqlite_temp_schema" | "sqlite_temp_master" | "temp.sqlite_schema" - | "temp.sqlite_master" => { - return Ok(Some(self.sqlite_schema_dataset("sqlite_temp_schema", true))); - } - "information_schema.schemata" => { - return Ok(Some(self.information_schema_schemata_dataset())); - } - "information_schema.tables" => { - return Ok(Some(self.information_schema_tables_dataset())); - } - "information_schema.columns" => { - return Ok(Some(self.information_schema_columns_dataset())); - } - "sys_audit_context" => { - return self.sys_audit_context_dataset().map(Some); - } - _ => name, - }; - let _ = table_name; - Ok(None) - } - - fn sys_audit_context_dataset(&self) -> Result { - let context = self - .audit_context - .lock() - .map_err(|_| DbError::internal("audit context lock poisoned"))? - .snapshot(); - let rows = context - .into_iter() - .map(|(key, value)| vec![Value::Text(key), value]) - .collect::>(); - Ok(Dataset::with_rows( - visible_columns("sys_audit_context", &["key", "value"]), - rows, - )) - } - - fn evaluate_generate_series(&self, table_name: String, values: Vec) -> Result { - if !(values.len() == 2 || values.len() == 3) { - return Err(DbError::sql("generate_series expects 2 or 3 arguments")); - } - let rows = generate_series_rows(&values)?; - Ok(Dataset::with_rows( - vec![ColumnBinding::visible( - Some(table_name), - "value".to_string(), - )], - rows.into_iter().map(|value| vec![value]).collect(), - )) - } - - fn evaluate_pragma_table_info_function( - &self, - table_name: String, - values: Vec, - extended: bool, - ) -> Result { - let target = one_text_arg("pragma_table_info", values)?; - let Some(table) = self.table_schema(&target) else { - return Ok(pragma_table_info_dataset(table_name, &[], extended)); - }; - Ok(pragma_table_info_dataset( - table_name, - &table.columns, - extended, - )) - } - - fn evaluate_pragma_table_list_function( - &self, - table_name: String, - values: Vec, - ) -> Result { - if !values.is_empty() { - return Err(DbError::sql("pragma_table_list expects no arguments")); - } - Ok(self.pragma_table_list_dataset(table_name)) - } - - fn evaluate_pragma_index_list_function( - &self, - table_name: String, - values: Vec, - ) -> Result { - let target = one_text_arg("pragma_index_list", values)?; - let mut rows = Vec::new(); - for (seq, index) in self.indexes_for_table(&target).into_iter().enumerate() { - rows.push(vec![ - Value::Int64(seq as i64), - Value::Text(index.name.clone()), - Value::Int64(i64::from(index.unique)), - Value::Text(if index.unique { "u" } else { "c" }.to_string()), - Value::Int64(i64::from(index.predicate_sql.is_some())), - ]); - } - Ok(Dataset::with_rows( - visible_columns(&table_name, &["seq", "name", "unique", "origin", "partial"]), - rows, - )) - } - - fn evaluate_pragma_index_info_function( - &self, - table_name: String, - values: Vec, - extended: bool, - ) -> Result { - let target = one_text_arg("pragma_index_info", values)?; - let rows = self - .index_by_name(&target) - .map(|index| index_info_rows(index, extended)) - .unwrap_or_default(); - let columns = if extended { - visible_columns( - &table_name, - &["seqno", "cid", "name", "desc", "coll", "key"], - ) - } else { - visible_columns(&table_name, &["seqno", "cid", "name"]) - }; - Ok(Dataset::with_rows(columns, rows)) - } - - fn evaluate_pragma_foreign_key_list_function( - &self, - table_name: String, - values: Vec, - ) -> Result { - let target = one_text_arg("pragma_foreign_key_list", values)?; - let mut rows = Vec::new(); - if let Some(table) = self.table_schema(&target) { - rows.extend(foreign_key_rows(table)); - } - Ok(Dataset::with_rows( - visible_columns( - &table_name, - &[ - "id", - "seq", - "table", - "from", - "to", - "on_update", - "on_delete", - "match", - ], - ), - rows, - )) - } - - fn evaluate_pragma_database_list_function( - &self, - table_name: String, - values: Vec, - ) -> Result { - if !values.is_empty() { - return Err(DbError::sql("pragma_database_list expects no arguments")); - } - Ok(Dataset::with_rows( - visible_columns(&table_name, &["seq", "name", "file"]), - vec![vec![ - Value::Int64(0), - Value::Text("main".to_string()), - Value::Text("main".to_string()), - ]], - )) - } - - fn pragma_table_list_dataset(&self, table_name: String) -> Dataset { - let mut rows = Vec::new(); - for table in self.catalog.tables.values() { - if !compat_catalog_object_is_visible(&table.name) { - continue; - } - rows.push(table_list_row( - "main", - &table.name, - "table", - table.columns.len(), - )); - } - for view in self.catalog.views.values() { - rows.push(table_list_row( - "main", - &view.name, - "view", - view.column_names.len(), - )); - } - for table in self.temp_tables.values() { - rows.push(table_list_row( - "temp", - &table.name, - "table", - table.columns.len(), - )); - } - for view in self.temp_views.values() { - rows.push(table_list_row( - "temp", - &view.name, - "view", - view.column_names.len(), - )); - } - Dataset::with_rows( - visible_columns( - &table_name, - &["schema", "name", "type", "ncol", "wr", "strict"], - ), - rows, - ) - } - - fn sqlite_schema_dataset(&self, table_name: &str, temporary: bool) -> Dataset { - let mut rows = Vec::new(); - if temporary { - for table in self.temp_tables.values() { - if !compat_catalog_object_is_visible(&table.name) { - continue; - } - rows.push(sqlite_schema_row( - "table", - &table.name, - &table.name, - Some(render_compat_create_table(table)), - )); - } - for view in self.temp_views.values() { - rows.push(sqlite_schema_row( - "view", - &view.name, - &view.name, - Some(render_compat_create_view(view)), - )); - } - for index in self.temp_indexes.values() { - if !compat_catalog_object_is_visible(&index.name) - || !compat_catalog_object_is_visible(&index.table_name) - { - continue; - } - rows.push(sqlite_schema_row( - "index", - &index.name, - &index.table_name, - Some(render_compat_create_index(index)), - )); - } - } else { - for table in self.catalog.tables.values() { - if !compat_catalog_object_is_visible(&table.name) { - continue; - } - rows.push(sqlite_schema_row( - "table", - &table.name, - &table.name, - Some(render_compat_create_table(table)), - )); - } - for view in self.catalog.views.values() { - rows.push(sqlite_schema_row( - "view", - &view.name, - &view.name, - Some(render_compat_create_view(view)), - )); - } - for index in self.catalog.indexes.values() { - if !compat_catalog_object_is_visible(&index.name) - || !compat_catalog_object_is_visible(&index.table_name) - { - continue; - } - rows.push(sqlite_schema_row( - "index", - &index.name, - &index.table_name, - Some(render_compat_create_index(index)), - )); - } - for trigger in self.catalog.triggers.values() { - rows.push(sqlite_schema_row( - "trigger", - &trigger.name, - &trigger.target_name, - Some(render_compat_create_trigger(trigger)), - )); - } - } - Dataset::with_rows( - visible_columns(table_name, &["type", "name", "tbl_name", "rootpage", "sql"]), - rows, - ) - } - - fn information_schema_schemata_dataset(&self) -> Dataset { - let table_name = "schemata"; - let mut rows = vec![ - information_schema_schemata_row("main"), - information_schema_schemata_row("temp"), - ]; - for schema in self.catalog.schemas.values() { - if !identifiers_equal(&schema.name, "main") && !identifiers_equal(&schema.name, "temp") - { - rows.push(information_schema_schemata_row(&schema.name)); - } - } - Dataset::with_rows( - visible_columns( - table_name, - &[ - "catalog_name", - "schema_name", - "schema_owner", - "default_character_set_catalog", - "default_character_set_schema", - "default_character_set_name", - ], - ), - rows, - ) - } - - fn information_schema_tables_dataset(&self) -> Dataset { - let table_name = "tables"; - let mut rows = Vec::new(); - for table in self.catalog.tables.values() { - if !compat_catalog_object_is_visible(&table.name) { - continue; - } - rows.push(information_schema_table_row( - "main", - &table.name, - "BASE TABLE", - )); - } - for view in self.catalog.views.values() { - rows.push(information_schema_table_row("main", &view.name, "VIEW")); - } - for table in self.temp_tables.values() { - if !compat_catalog_object_is_visible(&table.name) { - continue; - } - rows.push(information_schema_table_row( - "temp", - &table.name, - "LOCAL TEMPORARY", - )); - } - for view in self.temp_views.values() { - rows.push(information_schema_table_row( - "temp", - &view.name, - "LOCAL TEMPORARY", - )); - } - Dataset::with_rows( - visible_columns( - table_name, - &["table_catalog", "table_schema", "table_name", "table_type"], - ), - rows, - ) - } - - fn information_schema_columns_dataset(&self) -> Dataset { - let table_name = "columns"; - let mut rows = Vec::new(); - for table in self.catalog.tables.values() { - if !compat_catalog_object_is_visible(&table.name) { - continue; - } - rows.extend(information_schema_column_rows( - "main", - &table.name, - &table.columns, - )); - } - for table in self.temp_tables.values() { - if !compat_catalog_object_is_visible(&table.name) { - continue; - } - rows.extend(information_schema_column_rows( - "temp", - &table.name, - &table.columns, - )); - } - Dataset::with_rows( - visible_columns( - table_name, - &[ - "table_catalog", - "table_schema", - "table_name", - "column_name", - "ordinal_position", - "column_default", - "is_nullable", - "data_type", - ], - ), - rows, - ) - } - - fn indexes_for_table(&self, table_name: &str) -> Vec<&IndexSchema> { - let (qualifier, object) = compat_schema_qualified_name(table_name); - let mut indexes = self - .catalog - .indexes - .values() - .filter(|index| { - qualifier != Some(CompatSchemaQualifier::Temp) - && identifiers_equal(&index.table_name, object) - }) - .chain(self.temp_indexes.values().filter(|index| { - qualifier != Some(CompatSchemaQualifier::Main) - && identifiers_equal(&index.table_name, object) - })) - .collect::>(); - indexes.sort_by(|left, right| left.name.cmp(&right.name)); - indexes - } - - fn index_by_name(&self, index_name: &str) -> Option<&IndexSchema> { - let (qualifier, object) = compat_schema_qualified_name(index_name); - match qualifier { - Some(CompatSchemaQualifier::Main) => map_get_ci(&self.catalog.indexes, object), - Some(CompatSchemaQualifier::Temp) => map_get_ci(&self.temp_indexes, object), - None => map_get_ci(&self.catalog.indexes, object) - .or_else(|| map_get_ci(&self.temp_indexes, object)), - } - } - - fn evaluate_json_table_function( - &self, - table_name: String, - values: Vec, - recursive: bool, - ) -> Result { - if values.len() != 1 { - return Err(DbError::sql(if recursive { - "json_tree expects 1 argument" - } else { - "json_each expects 1 argument" - })); - } - let rows = if recursive { - expand_json_tree_rows(&values[0])? - } else { - expand_json_each_rows(&values[0])? - }; - let mut columns = vec![ - ColumnBinding::visible(Some(table_name.clone()), "key".to_string()), - ColumnBinding::visible(Some(table_name.clone()), "value".to_string()), - ColumnBinding::visible(Some(table_name.clone()), "type".to_string()), - ]; - if recursive { - columns.push(ColumnBinding::visible(Some(table_name), "path".to_string())); - } - Ok(Dataset::with_rows(columns, rows)) - } - - fn dataset_from_row_id_set( - &self, - table: &TableSchema, - row_source: Option<&TableRowSource>, - alias: &Option, - row_ids: RuntimeRowIdSet<'_>, - include_hidden_row_id: bool, - ) -> Result { - let table_name = alias.clone().unwrap_or_else(|| table.name.clone()); - let mut rows = Vec::with_capacity(row_ids.len()); - let mut row_lookup_error = None; - row_ids.for_each(|row_id| { - if row_lookup_error.is_some() { - return; - } - match row_source - .map(|source| source.row_by_id(row_id)) - .transpose() - { - Ok(Some(Some(row))) => { - let mut values = row.values().to_vec(); - if include_hidden_row_id { - values.push(Value::Int64(row.row_id())); - } - rows.push(values); - } - Ok(Some(None)) | Ok(None) => {} - Err(error) => row_lookup_error = Some(error), - } - }); - if let Some(error) = row_lookup_error { - return Err(error); - } - let mut columns = table - .columns - .iter() - .map(|column| { - ColumnBinding::visible_source( - Some(table_name.clone()), - Some(table.name.clone()), - column.name.clone(), - ) - }) - .collect::>(); - if include_hidden_row_id { - columns.push(ColumnBinding::hidden_source( - Some(table_name), - Some(table.name.clone()), - FTS_HIDDEN_ROW_ID_COLUMN.to_string(), - )); - } - Ok(Dataset::with_rows(columns, rows)) - } - - fn dataset_from_visible_row_source( - &self, - table: &TableSchema, - row_source: VisibleTableRowSource<'_>, - alias: &Option, - ) -> Result { - let table_name = alias.clone().unwrap_or_else(|| table.name.clone()); - let mut rows = Vec::with_capacity(row_source.row_count()); - for row in row_source.rows() { - let row = row?; - let mut values = row.values().to_vec(); - if !generated_columns_are_stored(table) { - self.apply_virtual_generated_columns(table, &mut values)?; - } - rows.push(values); - } - let columns = table - .columns - .iter() - .map(|column| { - ColumnBinding::visible_source( - Some(table_name.clone()), - Some(table.name.clone()), - column.name.clone(), - ) - }) - .collect::>(); - let mut dataset = Dataset::with_rows(columns, rows); - self.apply_row_policies(table, &mut dataset)?; - Ok(dataset) - } - - fn apply_row_policies(&self, table: &TableSchema, dataset: &mut Dataset) -> Result<()> { - if table.temporary || crate::security::is_security_internal_table(&table.name) { - return Ok(()); - } - let policies = self.active_row_policies_for_table(&table.name)?; - if policies.is_empty() { - return Ok(()); - } - let filter_dataset = Dataset::with_rows(dataset.columns.clone(), Vec::new()); - let mut kept = Vec::with_capacity(dataset.rows.len()); - for row in dataset.rows.iter() { - let mut visible = true; - for policy in &policies { - match self.eval_expr( - &policy.expr, - &filter_dataset, - row, - &[], - &BTreeMap::new(), - None, - )? { - Value::Bool(true) => {} - Value::Bool(false) | Value::Null => { - visible = false; - break; - } - other => { - return Err(DbError::sql(format!( - "policy {} did not evaluate to BOOL: {other:?}", - policy.name - ))) - } - } - } - if visible { - kept.push(row.clone()); - } - } - dataset.set_rows(kept); - Ok(()) - } - - fn active_row_policies_for_table(&self, table_name: &str) -> Result> { - let Some(row_source) = self.visible_table_row_source(crate::security::POLICIES_TABLE) - else { - return Ok(Vec::new()); - }; - let mut policies = Vec::new(); - for row in row_source.rows() { - let row = row?; - let values = row.values(); - let enabled = matches!(values.get(3), Some(Value::Bool(true))); - if !enabled { - continue; - } - let Some(Value::Text(policy_name)) = values.first() else { - continue; - }; - let Some(Value::Text(policy_table)) = values.get(1) else { - continue; - }; - if !identifiers_equal(policy_table, table_name) { - continue; - } - let Some(Value::Text(using_sql)) = values.get(2) else { - continue; - }; - policies.push(ActiveRowPolicy { - name: policy_name.clone(), - expr: parse_sql_statement(&format!("SELECT {using_sql}")).and_then( - |statement| { - let Statement::Query(query) = statement else { - return Err(DbError::sql("policy expression did not parse as SELECT")); - }; - let QueryBody::Select(select) = query.body else { - return Err(DbError::sql("policy expression did not parse as SELECT")); - }; - let Some(SelectItem::Expr { expr, .. }) = select.projection.first() else { - return Err(DbError::sql("policy expression is not scalar")); - }; - Ok(expr.clone()) - }, - )?, - }); - } - Ok(policies) - } - - fn active_column_masks(&self) -> Result> { - let Some(row_source) = self.visible_table_row_source(crate::security::MASKS_TABLE) else { - return Ok(Vec::new()); - }; - let mut masks = Vec::new(); - for row in row_source.rows() { - let row = row?; - let values = row.values(); - if !matches!(values.get(4), Some(Value::Bool(true))) { - continue; - } - let ( - Some(Value::Text(_mask_name)), - Some(Value::Text(table_name)), - Some(Value::Text(column_name)), - Some(Value::Text(expression_sql)), - ) = (values.first(), values.get(1), values.get(2), values.get(3)) - else { - continue; - }; - masks.push(ActiveColumnMask { - table_name: table_name.clone(), - column_name: column_name.clone(), - expr: parse_sql_statement(&format!("SELECT {expression_sql}")).and_then( - |statement| { - let Statement::Query(query) = statement else { - return Err(DbError::sql("mask expression did not parse as SELECT")); - }; - let QueryBody::Select(select) = query.body else { - return Err(DbError::sql("mask expression did not parse as SELECT")); - }; - let Some(SelectItem::Expr { expr, .. }) = select.projection.first() else { - return Err(DbError::sql("mask expression is not scalar")); - }; - Ok(expr.clone()) - }, - )?, - }); - } - Ok(masks) - } - - pub(crate) fn security_rules_active(&self) -> Result { - if let Some(row_source) = self.visible_table_row_source(crate::security::POLICIES_TABLE) { - for row in row_source.rows() { - if matches!(row?.values().get(3), Some(Value::Bool(true))) { - return Ok(true); - } - } - } - self.security_masks_active() - } - - fn security_masks_active(&self) -> Result { - let Some(row_source) = self.visible_table_row_source(crate::security::MASKS_TABLE) else { - return Ok(false); - }; - for row in row_source.rows() { - if matches!(row?.values().get(4), Some(Value::Bool(true))) { - return Ok(true); - } - } - Ok(false) - } - - fn masked_output_value( - &self, - binding: &ColumnBinding, - value: &Value, - dataset: &Dataset, - row: &[Value], - params: &[Value], - ctes: &BTreeMap, - ) -> Result { - let masks = self.active_column_masks()?; - let exact = masks.iter().find(|mask| { - identifiers_equal(&mask.column_name, &binding.name) - && binding - .source_table - .as_deref() - .or(binding.table.as_deref()) - .is_some_and(|table| identifiers_equal(table, &mask.table_name)) - }); - let display_table = if exact.is_none() { - binding.table.as_deref().filter(|table| { - binding - .source_table - .as_deref() - .is_none_or(|source| !identifiers_equal(source, table)) - }) - } else { - None - }; - let alias_match = display_table.and_then(|table| { - let matches = masks - .iter() - .filter(|mask| { - identifiers_equal(&mask.column_name, &binding.name) - && identifiers_equal(&mask.table_name, table) - }) - .collect::>(); - if matches.len() == 1 { - matches.first().copied() - } else { - None - } - }); - let fallback = if exact.is_none() && alias_match.is_none() { - let matches = masks - .iter() - .filter(|mask| identifiers_equal(&mask.column_name, &binding.name)) - .collect::>(); - if matches.len() == 1 { - matches.first().copied() - } else { - None - } - } else { - None - }; - if let Some(mask) = exact.or(alias_match).or(fallback) { - self.eval_expr(&mask.expr, dataset, row, params, ctes, None) - } else { - Ok(value.clone()) - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -struct RootHeader { - schema_cookie: u32, - payload_checksum: u32, - pointer: OverflowPointer, -} - -struct SimpleIndexedProjectionPlan<'a> { - table_name: &'a str, - table_schema: &'a TableSchema, - filter_column: &'a str, - lookup_value: Value, - extra_lookup_terms: Vec<(&'a str, Value)>, - projection_indexes: Vec, - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, -} - -struct SimpleUnionRangeProjectionSide { - table_name: String, - alias: Option, - projection_indexes: Vec, - column_names: Vec, - filter_column_index: usize, - lower_bound: Option, - upper_bound: Option, -} - -struct LeftJoinStatusAggregatePlan<'a> { - parent_table_name: &'a str, - parent_join_index: usize, - child_table_name: &'a str, - child_join_index: usize, - child_status_index: usize, - child_id_index: usize, - child_index_name: Option, - group_column_indexes: Vec, - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, -} - -fn add_status_aggregate_child_row( - counts: &mut LeftJoinStatusCounts, - child_values: &[Value], - plan: &LeftJoinStatusAggregatePlan<'_>, -) -> Result<()> { - let Some(child_status) = child_values.get(plan.child_status_index) else { - return Err(DbError::internal("child join row is shorter than schema")); - }; - let Some(child_id) = child_values.get(plan.child_id_index) else { - return Err(DbError::internal("child join row is shorter than schema")); - }; - counts.add_child(child_status, child_id) -} - -#[derive(Clone, Copy, Debug, Default)] -struct LeftJoinStatusCounts { - open_count: i64, - in_progress_count: i64, - resolved_count: i64, - closed_count: i64, - total_count: i64, -} - -impl LeftJoinStatusCounts { - fn bump(value: &mut i64) -> Result<()> { - *value = value - .checked_add(1) - .ok_or_else(|| DbError::sql("aggregate count exceeds INT64 limits"))?; - Ok(()) - } - - fn add_child(&mut self, status: &Value, id: &Value) -> Result<()> { - if let Value::Text(status) = status { - match status.as_str() { - "open" => Self::bump(&mut self.open_count)?, - "in_progress" => Self::bump(&mut self.in_progress_count)?, - "resolved" => Self::bump(&mut self.resolved_count)?, - "closed" => Self::bump(&mut self.closed_count)?, - _ => {} - } - } - if !matches!(id, Value::Null) { - Self::bump(&mut self.total_count)?; - } - Ok(()) - } -} - -#[derive(Clone, Copy, Debug)] -enum IndexedJoinAggregateKind { - CountRows, - CountNonNull(usize), - CountDistinct(usize), - Sum(usize), - Avg(usize), - Min(usize), - Max(usize), -} - -#[allow(dead_code)] -struct LeftJoinAggregatePlan<'a> { - parent_table_name: &'a str, - parent_join_index: usize, - child_table_name: &'a str, - child_join_index: usize, - child_index_name: Option, - group_column_indexes: Vec, - aggregate_kinds: Vec, - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, - include_empty_parent: bool, -} - -struct ThreeTableGenrePopularityPlan<'a> { - genre_table_name: &'a str, - genre_id_index: usize, - genre_name_index: usize, - bridge_table_name: &'a str, - bridge_movie_id_index: usize, - bridge_genre_index_name: String, - movie_table_name: &'a str, - movie_rating_index: usize, - movie_index_name: Option, - movie_id_is_rowid_alias: bool, - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, -} - -struct MovieTagSearchPlan<'a> { - tag_table_name: &'a str, - tag_id_index: usize, - tag_name_index_name: String, - tag_name_value: Value, - bridge_table_name: &'a str, - bridge_movie_id_index: usize, - bridge_tag_index_name: String, - movie_table_name: &'a str, - movie_index_name: Option, - movie_id_is_rowid_alias: bool, - projection_indexes: Vec, - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, -} - -struct MovieWatchlistPlan<'a> { - watchlist_table_name: &'a str, - watchlist_movie_id_index: usize, - watchlist_priority_index: usize, - watchlist_user_index_name: String, - user_handle_value: Value, - movie_table_name: &'a str, - movie_id_index: usize, - movie_title_index: usize, - movie_index_name: Option, - movie_id_is_rowid_alias: bool, - review_table_name: &'a str, - review_score_index: usize, - review_movie_index_name: String, - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, -} - -struct MovieTopRatedByYearPlan<'a> { - movie_table_name: &'a str, - movie_id_index: usize, - movie_release_year_index: usize, - movie_release_year_index_name: Option, - movie_projection_indexes: Vec, - release_year_value: Value, - review_table_name: &'a str, - review_score_index: usize, - review_movie_index_name: String, - min_review_count: i64, - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, -} - -struct MovieBusiestPeoplePlan<'a> { - people_table_name: &'a str, - people_projection_indexes: Vec, - people_index_name: Option, - people_id_is_rowid_alias: bool, - roles_person_index_name: String, - column_names: Vec, - limit: Option, - offset: usize, -} - -struct MovieBusiestPeopleCount { - person_key: RuntimeBtreeKey, - role_count: i64, -} - -struct DirectorsCtePlan<'a> { - roles_table_name: &'a str, - role_person_id_index: usize, - role_movie_id_index: usize, - role_job_index: usize, - director_job: String, - movie_table_name: &'a str, - movie_title_index: usize, - movie_rating_index: usize, - movie_index_name: Option, - movie_id_is_rowid_alias: bool, - min_films: i64, - title_separator: String, - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, -} - -struct DirectedMoviesCtePlan<'a> { - roles_table_name: &'a str, - role_person_id_index: usize, - role_movie_id_index: usize, - role_job_index: usize, - director_job: String, - movie_table_name: &'a str, - movie_title_index: usize, - movie_rating_index: usize, - movie_index_name: Option, - movie_id_is_rowid_alias: bool, -} - -struct DirectorsTopDirsCtePlan { - min_films: i64, -} - -type DirectorsFinalSelectAnalysis = ( - Vec, - Option>, - Option, - usize, - String, -); - -struct DirectorsCteAccumulator { - person_id: Value, - films: i64, - rating_sum: f64, - rating_count: i64, - titles: Vec, -} - -impl DirectorsCteAccumulator { - fn new(person_id: Value) -> Self { - Self { - person_id, - films: 0, - rating_sum: 0.0, - rating_count: 0, - titles: Vec::new(), - } - } - - fn add_movie(&mut self, movie_values: &[Value], title_index: usize, rating_index: usize) { - self.films = self.films.saturating_add(1); - if let Some(rating) = movie_values - .get(rating_index) - .and_then(indexed_join_aggregate_as_f64) - { - self.rating_sum += rating; - self.rating_count = self.rating_count.saturating_add(1); - } - if let Some(Value::Text(title)) = movie_values.get(title_index) { - self.titles.push(title.clone()); - } - } -} - -struct IndexedJoinAggregateState { - accumulators: Vec, -} - -enum IndexedJoinAccumulator { - CountRows { count: i64 }, - CountNonNull { col: usize, count: i64 }, - CountDistinct { col: usize, seen: BTreeSet> }, - Sum { col: usize, sum: f64, count: i64 }, - Avg { col: usize, sum: f64, count: i64 }, - Min { col: usize, value: Option }, - Max { col: usize, value: Option }, -} - -impl IndexedJoinAggregateState { - fn new(kinds: &[IndexedJoinAggregateKind]) -> Self { - let accumulators = kinds - .iter() - .map(|kind| match kind { - IndexedJoinAggregateKind::CountRows => { - IndexedJoinAccumulator::CountRows { count: 0 } - } - IndexedJoinAggregateKind::CountNonNull(col) => { - IndexedJoinAccumulator::CountNonNull { - col: *col, - count: 0, - } - } - IndexedJoinAggregateKind::CountDistinct(col) => { - IndexedJoinAccumulator::CountDistinct { - col: *col, - seen: BTreeSet::new(), - } - } - IndexedJoinAggregateKind::Sum(col) => IndexedJoinAccumulator::Sum { - col: *col, - sum: 0.0, - count: 0, - }, - IndexedJoinAggregateKind::Avg(col) => IndexedJoinAccumulator::Avg { - col: *col, - sum: 0.0, - count: 0, - }, - IndexedJoinAggregateKind::Min(col) => IndexedJoinAccumulator::Min { - col: *col, - value: None, - }, - IndexedJoinAggregateKind::Max(col) => IndexedJoinAccumulator::Max { - col: *col, - value: None, - }, - }) - .collect(); - Self { accumulators } - } - - fn accumulate(&mut self, child_values: &[Value]) -> Result<()> { - for acc in &mut self.accumulators { - match acc { - IndexedJoinAccumulator::CountRows { count } => { - *count = count.saturating_add(1); - } - IndexedJoinAccumulator::CountNonNull { col, count } => { - if let Some(value) = child_values.get(*col) { - if !matches!(value, Value::Null) { - *count = count.saturating_add(1); - } - } - } - IndexedJoinAccumulator::CountDistinct { col, seen } => { - if let Some(value) = child_values.get(*col) { - if !matches!(value, Value::Null) { - seen.insert(row_identity(std::slice::from_ref(value))?); - } - } - } - IndexedJoinAccumulator::Sum { col, sum, count } => { - if let Some(value) = child_values.get(*col) { - if let Some(f) = indexed_join_aggregate_as_f64(value) { - *sum += f; - *count = count.saturating_add(1); - } - } - } - IndexedJoinAccumulator::Avg { col, sum, count } => { - if let Some(value) = child_values.get(*col) { - if let Some(f) = indexed_join_aggregate_as_f64(value) { - *sum += f; - *count = count.saturating_add(1); - } - } - } - IndexedJoinAccumulator::Min { col, value } => { - if let Some(v) = child_values.get(*col) { - if !matches!(v, Value::Null) { - match value { - None => *value = Some(v.clone()), - Some(curr) => { - if compare_values_no_error(v, curr) - == Some(std::cmp::Ordering::Less) - { - *value = Some(v.clone()); - } - } - } - } - } - } - IndexedJoinAccumulator::Max { col, value } => { - if let Some(v) = child_values.get(*col) { - if !matches!(v, Value::Null) { - match value { - None => *value = Some(v.clone()), - Some(curr) => { - if compare_values_no_error(v, curr) - == Some(std::cmp::Ordering::Greater) - { - *value = Some(v.clone()); - } - } - } - } - } - } - } - } - Ok(()) - } - - fn finalize_into(self, output: &mut Vec) { - for acc in self.accumulators { - match acc { - IndexedJoinAccumulator::CountRows { count } => { - output.push(Value::Int64(count)); - } - IndexedJoinAccumulator::CountNonNull { count, .. } => { - output.push(Value::Int64(count)); - } - IndexedJoinAccumulator::CountDistinct { seen, .. } => { - output.push(Value::Int64(seen.len() as i64)); - } - IndexedJoinAccumulator::Sum { sum, count, .. } => { - if count == 0 { - output.push(Value::Null); - } else { - output.push(Value::Float64(sum)); - } - } - IndexedJoinAccumulator::Avg { sum, count, .. } => { - if count == 0 { - output.push(Value::Null); - } else { - output.push(Value::Float64(sum / count as f64)); - } - } - IndexedJoinAccumulator::Min { value, .. } => { - output.push(value.unwrap_or(Value::Null)); - } - IndexedJoinAccumulator::Max { value, .. } => { - output.push(value.unwrap_or(Value::Null)); - } - } - } - } -} - -fn indexed_join_aggregate_as_f64(value: &Value) -> Option { - match value { - Value::Int64(v) => Some(*v as f64), - Value::Float64(v) => Some(*v), - Value::Decimal { scaled, scale } => { - let scaled_u = if *scaled >= 0 { - *scaled as u64 - } else { - return None; - }; - let divisor = 10u64.checked_pow(*scale as u32).unwrap_or(1); - Some(scaled_u as f64 / divisor as f64) - } - _ => None, - } -} - -fn compare_values_no_error(a: &Value, b: &Value) -> Option { - crate::exec::expressions::compare_values(a, b).ok() -} - -enum SimpleIndexedProjectionRowIds<'a> { - Borrowed(RuntimeRowIdSet<'a>), - Owned(Vec), -} - -impl SimpleIndexedProjectionRowIds<'_> { - fn len(&self) -> usize { - match self { - Self::Borrowed(row_ids) => row_ids.len(), - Self::Owned(row_ids) => row_ids.len(), - } - } - - fn into_vec(self) -> Vec { - let mut row_ids = Vec::with_capacity(self.len()); - self.for_each(|row_id| row_ids.push(row_id)); - row_ids - } - - fn into_sorted_vec(self, descending: bool) -> Vec { - let mut row_ids = self.into_vec(); - row_ids.sort_unstable(); - if descending { - row_ids.reverse(); - } - row_ids - } - - fn for_each(self, mut f: impl FnMut(i64)) { - match self { - Self::Borrowed(row_ids) => row_ids.for_each(f), - Self::Owned(row_ids) => { - for row_id in row_ids { - f(row_id); - } - } - } - } -} - -#[derive(Clone, Debug)] -struct ActiveRowPolicy { - name: String, - expr: Expr, -} - -#[derive(Clone, Debug)] -struct ActiveColumnMask { - table_name: String, - column_name: String, - expr: Expr, -} - -struct SimpleCountQueryPlan<'a> { - table_name: &'a str, - table_ref: &'a str, - filter: Option<&'a Expr>, - column_name: String, -} - -struct SimpleMinMaxQueryPlan<'a> { - table_name: &'a str, - column_index: usize, - is_max: bool, - column_name: String, -} - -struct SimpleGroupedCountPlan<'a> { - table_name: &'a str, - group_exprs: &'a [Expr], - group_eval_bindings: Vec, - filter_expr: Option, - column_names: Vec, - projection_exprs: Option>, - raw_projection_bindings: Option>, - having: Option, - having_bindings: Vec, - order_by: Option>, - limit: Option, - offset: usize, -} - -struct SimpleGroupedNumericAggregatePlan<'a> { - table_name: &'a str, - group_exprs: &'a [Expr], - group_eval_bindings: Vec, - filter_expr: Option, - column_names: Vec, - aggregate_bindings: Vec, - projection_exprs: Option>, - raw_projection_bindings: Option>, - having: Option, - having_bindings: Vec, - order_by: Option>, - limit: Option, - offset: usize, -} - -struct GeneralGroupedSingleTablePlan<'a> { - table_name: &'a str, - table_alias: Option<&'a str>, - group_by: &'a [Expr], - filter: Option<&'a Expr>, - projection: &'a [SelectItem], - having: Option<&'a Expr>, - order_by: &'a [crate::sql::ast::OrderBy], - distinct: bool, - limit: Option<&'a Expr>, - offset: Option<&'a Expr>, -} - -#[derive(Clone, Copy)] -struct WindowEvalContext<'a> { - dataset: &'a Dataset, - params: &'a [Value], - ctes: &'a BTreeMap, -} - -struct WindowSortedRow { - row_index: usize, - order_keys: Vec, -} - -struct ReviewRankingFastRow { - row_id: i64, - movie_id: i64, - score: i64, - author: Value, -} - -struct CastBillingFastRow { - row_id: i64, - movie_id: i64, - person_id: Value, - billing_order: i64, - billing_value: Value, -} - -struct RollingAvgFastRow { - row_id: i64, - movie_id: i64, - id_value: Value, - rating: Value, -} - -fn compare_window_sorted_rows( - left: &WindowSortedRow, - right: &WindowSortedRow, - order_by: &[OrderBy], -) -> std::cmp::Ordering { - for (order, (left_value, right_value)) in order_by - .iter() - .zip(left.order_keys.iter().zip(right.order_keys.iter())) - { - let ordering = compare_values(left_value, right_value).unwrap_or(std::cmp::Ordering::Equal); - if ordering != std::cmp::Ordering::Equal { - return if order.descending { - ordering.reverse() - } else { - ordering - }; - } - } - left.row_index.cmp(&right.row_index) -} - -fn simple_window_column_positions( - dataset: &Dataset, - expressions: &[Expr], -) -> Result>> { - let mut positions = Vec::with_capacity(expressions.len()); - for expr in expressions { - let Expr::Column { table, column } = expr else { - return Ok(None); - }; - positions.push(resolve_dataset_column_position( - dataset, - table.as_deref(), - column, - )?); - } - Ok(Some(positions)) -} - -fn simple_window_order_column_positions( - dataset: &Dataset, - order_by: &[OrderBy], -) -> Result>> { - let mut positions = Vec::with_capacity(order_by.len()); - for order in order_by { - if order.collation.is_some() { - return Ok(None); - } - let Expr::Column { table, column } = &order.expr else { - return Ok(None); - }; - positions.push(resolve_dataset_column_position( - dataset, - table.as_deref(), - column, - )?); - } - Ok(Some(positions)) -} - -fn resolve_dataset_column_position( - dataset: &Dataset, - table: Option<&str>, - column: &str, -) -> Result { - let mut matched_index = None; - for (index, binding) in dataset.columns.iter().enumerate() { - let visible_match = table.is_some() || !binding.hidden; - if !visible_match || !identifiers_equal(&binding.name, column) { - continue; - } - if table.is_some_and(|table| { - !binding - .table - .as_deref() - .is_some_and(|binding_table| identifiers_equal(binding_table, table)) - }) { - continue; - } - if matched_index.replace(index).is_some() { - return Err(DbError::sql(format!("ambiguous column reference {column}"))); - } - } - matched_index.ok_or_else(|| DbError::sql(format!("unknown column {column}"))) -} - -fn values_from_positions(row: &[Value], positions: &[usize]) -> Result> { - positions - .iter() - .map(|position| { - row.get(*position) - .cloned() - .ok_or_else(|| DbError::internal("window row is shorter than its bindings")) - }) - .collect() -} - -fn window_key_from_positions(row: &[Value], positions: &[usize]) -> Result> { - if let [position] = positions { - let value = row - .get(*position) - .ok_or_else(|| DbError::internal("window row is shorter than its bindings"))?; - return row_identity(std::slice::from_ref(value)); - } - row_identity(&values_from_positions(row, positions)?) -} - -fn rows_preceding_current_frame(frame: Option<&crate::sql::ast::WindowFrame>) -> Option { - let frame = frame?; - if frame.unit != crate::sql::ast::WindowFrameUnit::Rows { - return None; - } - if !matches!( - frame.end.as_ref(), - None | Some(crate::sql::ast::WindowFrameBound::CurrentRow) - ) { - return None; - } - let crate::sql::ast::WindowFrameBound::Preceding(offset) = &frame.start else { - return None; - }; - let Expr::Literal(Value::Int64(offset)) = offset.as_ref() else { - return None; - }; - usize::try_from(*offset).ok() -} - -struct IndexedJoinGroupedCountPlan<'a> { - parent_table_name: &'a str, - parent_join_column: &'a str, - child_index_name: String, - group_column_indexes: Vec, - column_names: Vec, - order_by: Option>, - limit: Option, - offset: usize, -} - -impl IndexedJoinGroupedCountPlan<'_> { - fn scalar_count_top_n_limit(&self) -> Option { - let order_by = self.order_by.as_deref()?; - let [order] = order_by else { - return None; - }; - if self.offset != 0 - || order.projection_index != self.group_column_indexes.len() - || !order.descending - || order.collation.is_some() - { - return None; - } - self.limit - } -} - -#[derive(Clone, Copy)] -struct IndexedJoinLimitTablePlan<'a> { - name: &'a str, - alias: &'a Option, -} - -struct IndexedJoinLimitStep { - previous_table_index: usize, - previous_column_index: usize, - right_index_name: Option, -} - -struct IndexedJoinLimitProjection { - table_index: usize, - column_index: usize, - column_name: String, -} - -struct IndexedJoinLimitPlan<'a> { - tables: Vec>, - steps: Vec, - projections: Vec, - limit: usize, - offset: usize, -} - -struct BaseTableJoinPlan<'a> { - left_name: &'a str, - left_alias: Option<&'a str>, - right_name: &'a str, - right_alias: Option<&'a str>, - kind: JoinKind, - constraint: &'a JoinConstraint, - filter: Option<&'a Expr>, - projection: &'a [SelectItem], - order_by: &'a [crate::sql::ast::OrderBy], - distinct: bool, - limit: Option<&'a Expr>, - offset: Option<&'a Expr>, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum SimpleGroupedNumericAggregateKind { - CountRows, - CountNonNull, - CountDistinct, - Sum, - SumDistinct, - Avg, - AvgDistinct, - Total, - TotalDistinct, - StddevSamp, - StddevSampDistinct, - StddevPop, - StddevPopDistinct, - VarSamp, - VarSampDistinct, - VarPop, - VarPopDistinct, - BoolAnd, - BoolAndDistinct, - BoolOr, - BoolOrDistinct, - Min, - Max, -} - -impl SimpleGroupedNumericAggregateKind { - fn aggregate_name(self) -> &'static str { - match self { - Self::CountRows | Self::CountNonNull | Self::CountDistinct => "count", - Self::Sum | Self::SumDistinct => "sum", - Self::Avg | Self::AvgDistinct => "avg", - Self::Total | Self::TotalDistinct => "total", - Self::StddevSamp | Self::StddevSampDistinct => "stddev", - Self::StddevPop | Self::StddevPopDistinct => "stddev_pop", - Self::VarSamp | Self::VarSampDistinct => "variance", - Self::VarPop | Self::VarPopDistinct => "var_pop", - Self::BoolAnd | Self::BoolAndDistinct => "bool_and", - Self::BoolOr | Self::BoolOrDistinct => "bool_or", - Self::Min => "min", - Self::Max => "max", - } - } - - fn uses_distinct(self) -> bool { - matches!( - self, - Self::CountDistinct - | Self::SumDistinct - | Self::AvgDistinct - | Self::TotalDistinct - | Self::StddevSampDistinct - | Self::StddevPopDistinct - | Self::VarSampDistinct - | Self::VarPopDistinct - | Self::BoolAndDistinct - | Self::BoolOrDistinct - ) - } - - fn matches_aggregate_name(self, name: &str) -> bool { - match self { - Self::StddevSamp | Self::StddevSampDistinct => { - name.eq_ignore_ascii_case("stddev") || name.eq_ignore_ascii_case("stddev_samp") - } - Self::VarSamp | Self::VarSampDistinct => { - name.eq_ignore_ascii_case("variance") || name.eq_ignore_ascii_case("var_samp") - } - _ => name.eq_ignore_ascii_case(self.aggregate_name()), - } - } -} - -#[derive(Clone, Debug)] -struct SimpleGroupedNumericAggregateBinding { - kind: SimpleGroupedNumericAggregateKind, - projection_index: usize, - source_column_name: Option, - source_column_index: Option, - source_expr: Option, -} - -#[derive(Default)] -struct SimpleScalarInt64AggregateStats { - row_count: i64, - non_null_count: i64, - total_int: i64, - total_float: f64, - min_value: Option, - max_value: Option, -} - -impl SimpleScalarInt64AggregateStats { - fn add(&mut self, value: Option) { - self.row_count += 1; - let Some(value) = value else { - return; - }; - self.non_null_count += 1; - self.total_int += value; - self.total_float += value as f64; - self.min_value = Some(self.min_value.map_or(value, |min| min.min(value))); - self.max_value = Some(self.max_value.map_or(value, |max| max.max(value))); - } - - fn into_values( - self, - aggregate_bindings: &[SimpleGroupedNumericAggregateBinding], - ) -> Vec { - aggregate_bindings - .iter() - .map(|aggregate| match aggregate.kind { - SimpleGroupedNumericAggregateKind::CountRows => Value::Int64(self.row_count), - SimpleGroupedNumericAggregateKind::CountNonNull => { - Value::Int64(self.non_null_count) - } - SimpleGroupedNumericAggregateKind::Sum => { - if self.non_null_count == 0 { - Value::Null - } else { - Value::Int64(self.total_int) - } - } - SimpleGroupedNumericAggregateKind::Avg => { - if self.non_null_count == 0 { - Value::Null - } else { - Value::Float64(self.total_float / self.non_null_count as f64) - } - } - SimpleGroupedNumericAggregateKind::Min => { - self.min_value.map(Value::Int64).unwrap_or(Value::Null) - } - SimpleGroupedNumericAggregateKind::Max => { - self.max_value.map(Value::Int64).unwrap_or(Value::Null) - } - _ => Value::Null, - }) - .collect() - } -} - -#[derive(Clone, Debug)] -struct ManifestTemplate { - schema_cookie: u32, - table_next_row_id_offsets: BTreeMap, - table_state_offsets: BTreeMap, - table_pk_index_root_offsets: BTreeMap, - bytes: Vec, -} - -#[derive(Clone, Debug)] -struct ManifestEncoding { - bytes: Vec, - table_next_row_id_offsets: BTreeMap, - table_state_offsets: BTreeMap, - table_pk_index_root_offsets: BTreeMap, -} - -#[derive(Debug)] -struct SnapshotPageStore<'a> { - pager: &'a PagerHandle, - wal: &'a WalHandle, - snapshot_lsn: u64, -} - -impl PageStore for SnapshotPageStore<'_> { - fn page_size(&self) -> u32 { - self.pager.page_size() - } - - fn allocate_page(&mut self) -> Result { - Err(DbError::internal( - "snapshot page store does not support allocation", - )) - } - - fn free_page(&mut self, _page_id: PageId) -> Result<()> { - Err(DbError::internal( - "snapshot page store does not support free", - )) - } - - fn read_page(&self, page_id: PageId) -> Result> { - if let Some(page) = - self.wal - .read_page_at_snapshot(self.pager, page_id, self.snapshot_lsn)? - { - Ok(page) - } else { - self.pager.read_page_from_disk(page_id) - } - } - - fn advise_sequential(&self) -> Result<()> { - self.pager.advise_sequential() - } - - fn write_page(&mut self, _page_id: PageId, _data: &[u8]) -> Result<()> { - Err(DbError::internal( - "snapshot page store does not support writes", - )) - } -} - -struct OverflowPayloadCursor<'a, S: PageStore> { - store: &'a S, - next_page_id: PageId, - page: Option>, - chunk_offset: usize, - chunk_remaining: usize, - remaining: usize, -} - -impl<'a, S: PageStore> OverflowPayloadCursor<'a, S> { - fn new(store: &'a S, pointer: OverflowPointer) -> Self { - Self { - store, - next_page_id: pointer.head_page_id, - page: None, - chunk_offset: OVERFLOW_HEADER_SIZE, - chunk_remaining: 0, - remaining: pointer.logical_len as usize, - } - } - - fn read_i64(&mut self) -> Result { - let mut bytes = [0_u8; 8]; - self.read_exact(&mut bytes)?; - Ok(i64::from_le_bytes(bytes)) - } - - fn read_u32(&mut self) -> Result { - let mut bytes = [0_u8; 4]; - self.read_exact(&mut bytes)?; - Ok(u32::from_le_bytes(bytes)) - } - - fn read_vec(&mut self, len: usize) -> Result> { - let mut bytes = vec![0_u8; len]; - self.read_exact(&mut bytes)?; - Ok(bytes) - } - - fn read_exact(&mut self, out: &mut [u8]) -> Result<()> { - if out.len() > self.remaining { - return Err(DbError::corruption("overflow payload length mismatch")); - } - let mut written = 0; - while written < out.len() { - self.ensure_chunk()?; - let take = (out.len() - written).min(self.chunk_remaining); - if take == 0 { - return Err(DbError::corruption("overflow payload truncated")); - } - let page = self - .page - .as_ref() - .ok_or_else(|| DbError::corruption("overflow payload page is missing"))?; - out[written..written + take] - .copy_from_slice(&page[self.chunk_offset..self.chunk_offset + take]); - self.chunk_offset += take; - self.chunk_remaining -= take; - self.remaining -= take; - written += take; - } - Ok(()) - } - - fn skip(&mut self, len: usize) -> Result<()> { - if len > self.remaining { - return Err(DbError::corruption("overflow payload length mismatch")); - } - let mut skipped = 0; - while skipped < len { - self.ensure_chunk()?; - let take = (len - skipped).min(self.chunk_remaining); - if take == 0 { - return Err(DbError::corruption("overflow payload truncated")); - } - self.chunk_offset += take; - self.chunk_remaining -= take; - self.remaining -= take; - skipped += take; - } - Ok(()) - } - - fn ensure_chunk(&mut self) -> Result<()> { - while self.chunk_remaining == 0 { - if self.remaining == 0 { - return Ok(()); - } - if self.next_page_id == 0 { - return Err(DbError::corruption("overflow payload truncated")); - } - let page = self.store.read_page(self.next_page_id)?; - if page.len() < OVERFLOW_HEADER_SIZE { - return Err(DbError::corruption("overflow page shorter than header")); - } - let next_page_id = u32::from_le_bytes(page[0..4].try_into().expect("header next page")); - let chunk_len = u32::from_le_bytes(page[4..8].try_into().expect("header chunk len")); - if chunk_len == 0 { - return Err(DbError::corruption( - "overflow chunk made no progress toward logical payload length", - )); - } - let chunk_end = OVERFLOW_HEADER_SIZE + chunk_len as usize; - if chunk_end > page.len() { - return Err(DbError::corruption( - "overflow chunk length exceeds page payload", - )); - } - self.next_page_id = next_page_id; - self.page = Some(page); - self.chunk_offset = OVERFLOW_HEADER_SIZE; - self.chunk_remaining = chunk_len as usize; - } - Ok(()) - } -} - -#[derive(Debug)] -struct DbTxnPageStore<'a> { - db: &'a crate::db::Db, -} - -impl PageStore for DbTxnPageStore<'_> { - fn page_size(&self) -> u32 { - self.db.config().page_size - } - - fn allocate_page(&mut self) -> Result { - self.db.allocate_page() - } - - fn free_page(&mut self, page_id: PageId) -> Result<()> { - self.db.free_page(page_id) - } - - fn read_page(&self, page_id: PageId) -> Result> { - self.db.read_page_in_write_txn(page_id) - } - - fn advise_sequential(&self) -> Result<()> { - self.db.advise_sequential() - } - - fn write_page(&mut self, page_id: PageId, data: &[u8]) -> Result<()> { - self.db.write_page(page_id, data) - } - - fn write_page_owned(&mut self, page_id: PageId, data: Vec) -> Result<()> { - self.db.write_page_owned(page_id, data) - } -} - -fn covering_payloads_for_index( - index: &IndexSchema, - table: &TableSchema, -) -> Option { - let columns = covering_payload_column_names(index, table)?; - Some(RuntimeCoveringPayloads::new(columns)) -} - -fn covering_payload_column_names(index: &IndexSchema, table: &TableSchema) -> Option> { - if index.kind != IndexKind::Btree - || index.predicate_sql.is_some() - || index.include_columns.is_empty() - || !generated_columns_are_stored(table) - { - return None; - } - - let mut columns: Vec = Vec::new(); - for column in index - .columns - .iter() - .map(|column| column.column_name.as_deref()) - .chain( - index - .include_columns - .iter() - .map(|column| Some(column.as_str())), - ) - { - let column = column?; - schema_column_index(table, column)?; - if !columns - .iter() - .any(|existing| identifiers_equal(existing, column)) - { - columns.push(column.to_string()); - } - } - if columns.is_empty() { - None - } else { - Some(columns) - } -} - -fn covering_payload_values_for_row( - index: &IndexSchema, - table: &TableSchema, - row_values: &[Value], -) -> Option> { - let columns = covering_payload_column_names(index, table)?; - columns - .iter() - .map(|column| { - schema_column_index(table, column) - .and_then(|offset| row_values.get(offset)) - .cloned() - }) - .collect() -} - -fn preserve_resident_payload_offsets_for_delete_tombstones( - config: &crate::config::DbConfig, -) -> bool { - !config.paged_row_storage && !config.persistent_pk_index -} - -fn build_runtime_index( - index: &IndexSchema, - runtime: &EngineRuntime, - page_size: u32, -) -> Result { - let table = runtime.catalog.table(&index.table_name).ok_or_else(|| { - DbError::corruption(format!( - "index {} references missing table {}", - index.name, index.table_name - )) - })?; - let source = runtime.table_row_source(&index.table_name).ok_or_else(|| { - DbError::corruption(format!("table data for {} is missing", index.table_name)) - })?; - - match index.kind { - IndexKind::Btree => { - let int64_keys = btree_uses_typed_int64_keys(index, table); - let uuid_keys = btree_uses_typed_uuid_keys(index, table); - let mut covering = covering_payloads_for_index(index, table); - if index.unique && int64_keys { - let mut keys = UniqueInt64Keys::new(); - for row in source.rows() { - let row = row?; - let Some(key) = compute_index_key(runtime, index, table, row.values())? else { - continue; - }; - let RuntimeBtreeKey::Int64(key) = key else { - return Err(DbError::internal( - "typed INT64 runtime index received an encoded key", - )); - }; - if keys.insert(key, row.row_id()).is_some() { - return Err(DbError::corruption(format!( - "unique index {} contains duplicate keys", - index.name - ))); - } - if let Some(covering) = covering.as_mut() { - if let Some(values) = - covering_payload_values_for_row(index, table, row.values()) - { - covering.insert_row_values(row.row_id(), values); - } - } - } - Ok(RuntimeIndex::Btree { - keys: RuntimeBtreeKeys::UniqueInt64(Arc::new(keys), BTreeSet::new()), - covering, - }) - } else if index.unique && uuid_keys { - let mut keys = BTreeMap::<[u8; 16], i64>::new(); - for row in source.rows() { - let row = row?; - let Some(key) = compute_index_key(runtime, index, table, row.values())? else { - continue; - }; - let RuntimeBtreeKey::Uuid(key) = key else { - return Err(DbError::internal( - "typed UUID runtime index received an encoded key", - )); - }; - if keys.insert(key, row.row_id()).is_some() { - return Err(DbError::corruption(format!( - "unique index {} contains duplicate keys", - index.name - ))); - } - if let Some(covering) = covering.as_mut() { - if let Some(values) = - covering_payload_values_for_row(index, table, row.values()) - { - covering.insert_row_values(row.row_id(), values); - } - } - } - Ok(RuntimeIndex::Btree { - keys: RuntimeBtreeKeys::UniqueUuid(Arc::new(keys), BTreeSet::new()), - covering, - }) - } else if index.unique { - let mut keys = BTreeMap::::new(); - for row in source.rows() { - let row = row?; - let Some(key) = compute_index_key(runtime, index, table, row.values())? else { - continue; - }; - let RuntimeBtreeKey::Encoded(key) = key else { - return Err(DbError::internal( - "encoded runtime index received an INT64 key", - )); - }; - if keys.insert(key, row.row_id()).is_some() { - return Err(DbError::corruption(format!( - "unique index {} contains duplicate keys", - index.name - ))); - } - if let Some(covering) = covering.as_mut() { - if let Some(values) = - covering_payload_values_for_row(index, table, row.values()) - { - covering.insert_row_values(row.row_id(), values); - } - } - } - Ok(RuntimeIndex::Btree { - keys: RuntimeBtreeKeys::UniqueEncoded(Arc::new(keys), BTreeSet::new()), - covering, - }) - } else if int64_keys { - let mut keys = NonUniqueInt64Keys::new(); - for row in source.rows() { - let row = row?; - let Some(key) = compute_index_key(runtime, index, table, row.values())? else { - continue; - }; - let RuntimeBtreeKey::Int64(key) = key else { - return Err(DbError::internal( - "typed INT64 runtime index received an encoded key", - )); - }; - keys.insert_row_id(key, row.row_id()); - if let Some(covering) = covering.as_mut() { - if let Some(values) = - covering_payload_values_for_row(index, table, row.values()) - { - covering.insert_row_values(row.row_id(), values); - } - } - } - Ok(RuntimeIndex::Btree { - keys: RuntimeBtreeKeys::NonUniqueInt64(Arc::new(keys), BTreeSet::new()), - covering, - }) - } else if uuid_keys { - let mut keys = BTreeMap::<[u8; 16], Vec>::new(); - for row in source.rows() { - let row = row?; - let Some(key) = compute_index_key(runtime, index, table, row.values())? else { - continue; - }; - let RuntimeBtreeKey::Uuid(key) = key else { - return Err(DbError::internal( - "typed UUID runtime index received an encoded key", - )); - }; - keys.entry(key).or_default().push(row.row_id()); - if let Some(covering) = covering.as_mut() { - if let Some(values) = - covering_payload_values_for_row(index, table, row.values()) - { - covering.insert_row_values(row.row_id(), values); - } - } - } - Ok(RuntimeIndex::Btree { - keys: RuntimeBtreeKeys::NonUniqueUuid(Arc::new(keys), BTreeSet::new()), - covering, - }) - } else { - let mut keys = BTreeMap::::new(); - // Pre-parse the partial-index predicate once instead of - // re-parsing the predicate SQL for every row in the table - // (row_satisfies_index_predicate parses on each call). Also - // pre-resolve the single indexed column position for the - // common single-column plain-column index so the build loop - // avoids per-row column lookups and Value clones. - let predicate_expr = index - .predicate_sql - .as_ref() - .map(|sql| crate::sql::parser::parse_expression_sql(sql)) - .transpose()?; - let single_column_position = single_plain_index_column_position(index, table); - let multi_column_positions = if single_column_position.is_none() { - plain_index_column_positions(index, table) - } else { - None - }; - let has_virtual_generated = !generated_columns_are_stored(table); - for row in source.rows() { - let row = row?; - let values = row.values(); - if let Some(predicate_expr) = &predicate_expr { - let row_materialized = if has_virtual_generated { - let mut materialized = values.to_vec(); - runtime.apply_virtual_generated_columns(table, &mut materialized)?; - Cow::Owned(materialized) - } else { - Cow::Borrowed(values) - }; - let row_for_eval = row_materialized.as_ref(); - let dataset = table_row_dataset(table, row_for_eval, &table.name); - let bindings = dataset.rows.first().map(Vec::as_slice).unwrap_or(&[]); - if !matches!( - runtime.eval_expr( - predicate_expr, - &dataset, - bindings, - &[], - &BTreeMap::new(), - None - )?, - Value::Bool(true) - ) { - continue; - } - } - let key = if let Some(position) = single_column_position { - // Fast path: encode the single indexed column value - // directly from the borrowed row slice, avoiding the - // intermediate Value clone that compute_index_values - // would perform. - encode_runtime_index_key(&values[position])? - } else if let Some(positions) = &multi_column_positions { - // Fast path for composite plain-column indexes: read - // each indexed column value by position and encode the - // composite key without building a Dataset. - let key_values: Vec = positions - .iter() - .map(|position| values.get(*position).cloned().unwrap_or(Value::Null)) - .collect(); - if index.unique && key_values.iter().any(|v| matches!(v, Value::Null)) { - continue; - } - RuntimeEncodedKey::from_vec(Row::new(key_values).encode()?) - } else { - let Some(encoded) = compute_index_key(runtime, index, table, values)? - else { - continue; - }; - let RuntimeBtreeKey::Encoded(encoded) = encoded else { - return Err(DbError::internal( - "encoded runtime index received an INT64 key", - )); - }; - encoded - }; - match keys.entry(key) { - std::collections::btree_map::Entry::Vacant(entry) => { - entry.insert(RuntimeEncodedRowIds::one(row.row_id())); - } - std::collections::btree_map::Entry::Occupied(mut entry) => { - entry.get_mut().push(row.row_id()); - } - } - if let Some(covering) = covering.as_mut() { - if let Some(values) = covering_payload_values_for_row(index, table, values) - { - covering.insert_row_values(row.row_id(), values); - } - } - } - Ok(RuntimeIndex::Btree { - keys: RuntimeBtreeKeys::NonUniqueEncoded( - Arc::new(RuntimeEncodedPostings::new(keys)), - BTreeSet::new(), - ), - covering, - }) - } - } - IndexKind::Trigram => { - let mut trigram = TrigramIndex::new(page_size, 100_000); - let mut builder = TrigramIndexBuilder::new(); - // Fast path: trigram indexes are constrained by DDL to a single - // plain text column with no predicate. Resolve its position once - // and read the text directly, avoiding the per-row Dataset - // construction in compute_index_values. - let single_text_position = plain_single_text_index_column_position(index, table); - let has_predicate = index.predicate_sql.is_some(); - let predicate_expr = index - .predicate_sql - .as_ref() - .map(|sql| crate::sql::parser::parse_expression_sql(sql)) - .transpose()?; - let has_virtual_generated = !generated_columns_are_stored(table); - for row in source.rows() { - let row = row?; - let values = row.values(); - if has_predicate { - if let Some(predicate_expr) = &predicate_expr { - let row_materialized = if has_virtual_generated { - let mut materialized = values.to_vec(); - runtime.apply_virtual_generated_columns(table, &mut materialized)?; - Cow::Owned(materialized) - } else { - Cow::Borrowed(values) - }; - let row_for_eval = row_materialized.as_ref(); - let dataset = table_row_dataset(table, row_for_eval, &table.name); - let bindings = dataset.rows.first().map(Vec::as_slice).unwrap_or(&[]); - if !matches!( - runtime.eval_expr( - predicate_expr, - &dataset, - bindings, - &[], - &BTreeMap::new(), - None - )?, - Value::Bool(true) - ) { - continue; - } - } - } - let text = if let Some(position) = single_text_position { - match values.get(position) { - Some(Value::Text(text)) => Some(text.clone()), - // NULL or non-text: skip, matching compute_index_values - // which would error on non-text for a trigram index. - _ => None, - } - } else { - compute_index_values(runtime, index, table, values)? - .into_iter() - .next() - .and_then(|value| match value { - Value::Text(text) => Some(text), - _ => None, - }) - }; - if let Some(text) = text { - builder.insert(row.row_id() as u64, &text); - } - } - builder.finish_into(&mut trigram)?; - Ok(RuntimeIndex::Trigram { index: trigram }) - } - IndexKind::Spatial => { - let backend = spatial_index_backend(index, table)?; - let mut spatial = SpatialRuntimeIndex::new(backend); - for row in source.rows() { - let row = row?; - if let Some(value) = - spatial_index_value_for_row(runtime, index, table, row.values())? - { - spatial.insert(row.row_id(), value).map_err(spatial_error)?; - } - } - Ok(RuntimeIndex::Spatial { index: spatial }) - } - IndexKind::FullText => { - let config = index - .full_text - .clone() - .ok_or_else(|| DbError::corruption("fulltext index is missing analyzer config"))?; - let mut fulltext = FullTextIndexBuilder::with_capacity(config, source.row_count()); - // Fast path: fulltext indexes are constrained by DDL to plain text - // columns with no predicate. Resolve their positions once and read - // the text directly, avoiding the per-row Dataset construction in - // full_text_fields_for_row / compute_index_values. - let text_positions = plain_text_index_column_positions(index, table); - let has_predicate = index.predicate_sql.is_some(); - let predicate_expr = index - .predicate_sql - .as_ref() - .map(|sql| crate::sql::parser::parse_expression_sql(sql)) - .transpose()?; - let has_virtual_generated = !generated_columns_are_stored(table); - for row in source.rows() { - let row = row?; - let values = row.values(); - if has_predicate { - if let Some(predicate_expr) = &predicate_expr { - let row_materialized = if has_virtual_generated { - let mut materialized = values.to_vec(); - runtime.apply_virtual_generated_columns(table, &mut materialized)?; - Cow::Owned(materialized) - } else { - Cow::Borrowed(values) - }; - let row_for_eval = row_materialized.as_ref(); - let dataset = table_row_dataset(table, row_for_eval, &table.name); - let bindings = dataset.rows.first().map(Vec::as_slice).unwrap_or(&[]); - if !matches!( - runtime.eval_expr( - predicate_expr, - &dataset, - bindings, - &[], - &BTreeMap::new(), - None - )?, - Value::Bool(true) - ) { - continue; - } - } - } - if let Some(positions) = &text_positions { - match positions.len() { - 1 => { - let text_ref = positions - .first() - .and_then(|position| values.get(*position)) - .and_then(|value| value.as_text()); - let fields = [text_ref]; - fulltext.add_row(row.row_id() as u64, &fields); - } - 2 => { - let fields = [ - values.get(positions[0]).and_then(Value::as_text), - values.get(positions[1]).and_then(Value::as_text), - ]; - fulltext.add_row(row.row_id() as u64, &fields); - } - _ => { - let field_refs: Vec> = positions - .iter() - .map(|position| match values.get(*position) { - Some(Value::Text(text)) => Some(text.as_str()), - _ => None, - }) - .collect(); - fulltext.add_row(row.row_id() as u64, &field_refs); - } - } - } else { - let fields = full_text_fields_for_row(runtime, index, table, values)?; - let field_refs = fields.iter().map(Option::as_deref).collect::>(); - fulltext.add_row(row.row_id() as u64, &field_refs); - } - } - Ok(RuntimeIndex::FullText { - index: fulltext.finish(), - }) - } - } -} - -/// Returns the row-position of the single indexed column for a plain -/// single-column BTREE index (no expression, no INCLUDE columns), so the -/// bulk-build fast path can encode the key directly from the borrowed row -/// slice without cloning the indexed `Value` or re-resolving the column on -/// every row. Returns `None` for composite, expression, or covering indexes. -fn single_plain_index_column_position(index: &IndexSchema, table: &TableSchema) -> Option { - if !index.include_columns.is_empty() { - return None; - } - let [column] = index.columns.as_slice() else { - return None; - }; - let column_name = column.column_name.as_deref()?; - if column.expression_sql.is_some() { - return None; - } - column_position(table, column_name) -} - -/// Resolves the stored-column positions for a btree index whose columns are -/// all plain stored columns (no expressions, no INCLUDE columns, no virtual -/// generated columns). Used by the build loop to read index key values -/// directly by position without building a `Dataset`. -fn plain_index_column_positions(index: &IndexSchema, table: &TableSchema) -> Option> { - if !index.include_columns.is_empty() { - return None; - } - if index.columns.is_empty() { - return None; - } - let stored_generated_ok = generated_columns_are_stored(table); - let mut positions = Vec::with_capacity(index.columns.len()); - for column in &index.columns { - if column.expression_sql.is_some() { - return None; - } - let Some(column_name) = &column.column_name else { - return None; - }; - let position = column_position(table, column_name)?; - if !stored_generated_ok - && table - .columns - .get(position) - .is_some_and(|col| col.generated_sql.is_some() && !col.generated_stored) - { - return None; - } - positions.push(position); - } - Some(positions) -} - -/// Resolves the single stored-column position for a trigram index over a -/// plain TEXT column (no expression, no INCLUDE columns, no predicate, no -/// virtual generated column). Returns `None` for any unsupported shape so -/// the trigram build loop falls back to `compute_index_values`. -pub(super) fn plain_single_text_index_column_position( - index: &IndexSchema, - table: &TableSchema, -) -> Option { - if !index.include_columns.is_empty() || index.predicate_sql.is_some() { - return None; - } - if index.columns.len() != 1 { - return None; - } - plain_index_column_positions(index, table)? - .into_iter() - .next() -} - -/// Resolves the stored-column positions for a fulltext index over plain TEXT -/// columns (no expressions, no INCLUDE columns, no predicate, no virtual -/// generated columns). Returns `None` for any unsupported shape so the -/// fulltext build loop falls back to `full_text_fields_for_row`. -fn plain_text_index_column_positions( - index: &IndexSchema, - table: &TableSchema, -) -> Option> { - if index.predicate_sql.is_some() { - return None; - } - let positions = plain_index_column_positions(index, table)?; - // Confirm every indexed column is actually TEXT so the fast path matches - // full_text_fields_for_row's TEXT requirement. - for position in &positions { - let column = table.columns.get(*position)?; - if column.column_type != ColumnType::Text { - return None; - } - } - Some(positions) -} - -pub(super) fn compute_index_key( - runtime: &EngineRuntime, - index: &IndexSchema, - table: &TableSchema, - row_values: &[Value], -) -> Result> { - compute_index_key_with_predicate(runtime, index, table, row_values, None) -} - -/// Like [`compute_index_key`], but optionally accepts a pre-parsed predicate -/// expression. See [`prepare_index_predicate_expr`] and -/// [`row_satisfies_index_predicate_with_expr`]. -pub(super) fn compute_index_key_with_predicate( - runtime: &EngineRuntime, - index: &IndexSchema, - table: &TableSchema, - row_values: &[Value], - pre_parsed_predicate: Option<&Expr>, -) -> Result> { - if !row_satisfies_index_predicate_with_expr( - runtime, - index, - table, - row_values, - pre_parsed_predicate, - )? { - return Ok(None); - } - if btree_uses_typed_int64_keys(index, table) { - let [column] = index.columns.as_slice() else { - return Err(DbError::internal( - "typed INT64 runtime indexes require exactly one indexed column", - )); - }; - if let Some(column_name) = &column.column_name { - let position = column_position(table, column_name).ok_or_else(|| { - DbError::constraint(format!("index column {} does not exist", column_name)) - })?; - let Value::Int64(value) = row_values - .get(position) - .ok_or_else(|| DbError::internal("row is shorter than table schema"))? - else { - return Err(DbError::internal( - "typed INT64 runtime index expected an INT64 row value", - )); - }; - return Ok(Some(RuntimeBtreeKey::Int64(*value))); - } - } - if btree_uses_typed_uuid_keys(index, table) { - let [column] = index.columns.as_slice() else { - return Err(DbError::internal( - "typed UUID runtime indexes require exactly one indexed column", - )); - }; - if let Some(column_name) = &column.column_name { - let position = column_position(table, column_name).ok_or_else(|| { - DbError::constraint(format!("index column {} does not exist", column_name)) - })?; - let Value::Uuid(value) = row_values - .get(position) - .ok_or_else(|| DbError::internal("row is shorter than table schema"))? - else { - return Err(DbError::internal( - "typed UUID runtime index expected a UUID row value", - )); - }; - return Ok(Some(RuntimeBtreeKey::Uuid(*value))); - } - } - if let Some(value) = compute_single_column_index_key_fast(index, table, row_values)? { - if index.unique && matches!(value, Value::Null) { - return Ok(None); - } - return Ok(Some(RuntimeBtreeKey::Encoded(encode_runtime_index_key( - value, - )?))); - } - if let Some(positions) = plain_index_column_positions(index, table) { - if positions.len() > 1 { - let values = positions - .iter() - .map(|position| { - row_values - .get(*position) - .cloned() - .ok_or_else(|| DbError::internal("row is shorter than table schema")) - }) - .collect::>>()?; - if index.unique && values.iter().any(|value| matches!(value, Value::Null)) { - return Ok(None); - } - return Ok(Some(RuntimeBtreeKey::Encoded(RuntimeEncodedKey::from_vec( - Row::new(values).encode()?, - )))); - } - } - let values = compute_index_values(runtime, index, table, row_values)?; - if index.unique && values.iter().any(|value| matches!(value, Value::Null)) { - return Ok(None); - } - let key = if values.len() == 1 { - encode_runtime_index_key(&values[0])? - } else { - RuntimeEncodedKey::from_vec(Row::new(values).encode()?) - }; - Ok(Some(RuntimeBtreeKey::Encoded(key))) -} - -/// Fast path for single-column btree indexes whose only column is a plain -/// stored column (no expression, no virtual generated column). Reads the value -/// directly by position without building a `Dataset` or cloning the full row, -/// which is the hot path for index maintenance during bulk DML. -fn compute_single_column_index_key_fast<'a>( - index: &IndexSchema, - table: &TableSchema, - row_values: &'a [Value], -) -> Result> { - if index.columns.len() != 1 { - return Ok(None); - } - let Some(column) = index.columns.first() else { - return Ok(None); - }; - if column.expression_sql.is_some() { - return Ok(None); - } - let Some(column_name) = &column.column_name else { - return Ok(None); - }; - let Some(position) = column_position(table, column_name) else { - return Ok(None); - }; - // Virtual generated columns are not stored in `row_values`, so they must go - // through the materializing path. Stored generated columns are present. - if generated_columns_are_stored(table) { - // All generated columns are stored: safe to read by position. - } else if table - .columns - .get(position) - .is_some_and(|col| col.generated_sql.is_some() && !col.generated_stored) - { - return Ok(None); - } - let Some(value) = row_values.get(position) else { - return Ok(None); - }; - Ok(Some(value)) -} - -pub(super) fn spatial_index_backend( - index: &IndexSchema, - table: &TableSchema, -) -> Result { - let column_index = spatial_index_column_index(index, table)?; - match table.columns[column_index].column_type { - ColumnType::Geography => Ok(SpatialIndexBackend::GeographyS2), - ColumnType::Geometry => Ok(SpatialIndexBackend::GeometryQuadCell), - _ => Err(DbError::internal(format!( - "SPATIAL index {} targets a non-spatial column", - index.name - ))), - } -} - -pub(super) fn spatial_index_value_for_row( - runtime: &EngineRuntime, - index: &IndexSchema, - table: &TableSchema, - row_values: &[Value], -) -> Result> { - if !row_satisfies_index_predicate(runtime, index, table, row_values)? { - return Ok(None); - } - let column_index = spatial_index_column_index(index, table)?; - let value = row_values - .get(column_index) - .ok_or_else(|| DbError::internal("row is shorter than table schema"))?; - let Some((is_geography, spatial)) = spatial_value_from_db(value)? else { - return Ok(None); - }; - match (table.columns[column_index].column_type, is_geography) { - (ColumnType::Geography, true) | (ColumnType::Geometry, false) => Ok(Some(spatial)), - (ColumnType::Geography, false) => Err(DbError::constraint(format!( - "SPATIAL index {} expected GEOGRAPHY values", - index.name - ))), - (ColumnType::Geometry, true) => Err(DbError::constraint(format!( - "SPATIAL index {} expected GEOMETRY values", - index.name - ))), - _ => Err(DbError::internal(format!( - "SPATIAL index {} targets a non-spatial column", - index.name - ))), - } -} - -fn spatial_index_column_index(index: &IndexSchema, table: &TableSchema) -> Result { - if index.kind != IndexKind::Spatial || index.columns.len() != 1 { - return Err(DbError::internal(format!( - "SPATIAL index {} must target exactly one column", - index.name - ))); - } - let column_name = index.columns[0].column_name.as_deref().ok_or_else(|| { - DbError::internal(format!( - "SPATIAL index {} must target a plain column", - index.name - )) - })?; - if index.columns[0].expression_sql.is_some() { - return Err(DbError::internal(format!( - "SPATIAL index {} cannot target an expression", - index.name - ))); - } - table - .columns - .iter() - .position(|entry| identifiers_equal(&entry.name, column_name)) - .ok_or_else(|| DbError::constraint(format!("index column {} does not exist", column_name))) -} - -fn btree_uses_typed_int64_keys(index: &IndexSchema, table: &TableSchema) -> bool { - let [column] = index.columns.as_slice() else { - return false; - }; - if column.expression_sql.is_some() { - return false; - } - let Some(column_name) = &column.column_name else { - return false; - }; - column_schema(table, column_name).is_some_and(|column| { - column.column_type == crate::catalog::ColumnType::Int64 && !column.nullable - }) -} - -fn btree_uses_typed_uuid_keys(index: &IndexSchema, table: &TableSchema) -> bool { - let [column] = index.columns.as_slice() else { - return false; - }; - if column.expression_sql.is_some() { - return false; - } - let Some(column_name) = &column.column_name else { - return false; - }; - column_schema(table, column_name).is_some_and(|column| { - column.column_type == crate::catalog::ColumnType::Uuid && !column.nullable - }) -} - -pub(super) fn compute_index_values( - runtime: &EngineRuntime, - index: &IndexSchema, - table: &TableSchema, - row_values: &[Value], -) -> Result> { - let row_materialized = if generated_columns_are_stored(table) { - Cow::Borrowed(row_values) - } else { - let mut materialized = row_values.to_vec(); - runtime.apply_virtual_generated_columns(table, &mut materialized)?; - Cow::Owned(materialized) - }; - let row_for_eval = row_materialized.as_ref(); - let dataset = table_row_dataset(table, row_for_eval, &table.name); - let bindings = dataset.rows.first().map(Vec::as_slice).unwrap_or(&[]); - index - .columns - .iter() - .map(|column| { - if let Some(column_name) = &column.column_name { - let position = column_position(table, column_name).ok_or_else(|| { - DbError::constraint(format!("index column {} does not exist", column_name)) - })?; - Ok(row_for_eval[position].clone()) - } else if let Some(expression_sql) = &column.expression_sql { - let expr = crate::sql::parser::parse_expression_sql(expression_sql)?; - runtime.eval_expr(&expr, &dataset, bindings, &[], &BTreeMap::new(), None) - } else { - Err(DbError::constraint("index column definition is empty")) - } - }) - .collect() -} - -pub(super) fn full_text_fields_for_row( - runtime: &EngineRuntime, - index: &IndexSchema, - table: &TableSchema, - row_values: &[Value], -) -> Result>> { - if !row_satisfies_index_predicate(runtime, index, table, row_values)? { - return Ok(Vec::new()); - } - compute_index_values(runtime, index, table, row_values)? - .into_iter() - .map(|value| match value { - Value::Text(text) => Ok(Some(text)), - Value::Null => Ok(None), - other => Err(DbError::constraint(format!( - "fulltext index requires TEXT columns, got {other:?}" - ))), - }) - .collect() -} - -pub(super) fn row_satisfies_index_predicate( - runtime: &EngineRuntime, - index: &IndexSchema, - table: &TableSchema, - row_values: &[Value], -) -> Result { - row_satisfies_index_predicate_with_expr(runtime, index, table, row_values, None) -} - -/// Like [`row_satisfies_index_predicate`], but accepts an optional pre-parsed -/// predicate expression. When provided, the predicate SQL is not re-parsed for -/// every row, which can dominate wall time for bulk DML on tables with partial -/// or expression-indexed indexes. -pub(super) fn row_satisfies_index_predicate_with_expr( - runtime: &EngineRuntime, - index: &IndexSchema, - table: &TableSchema, - row_values: &[Value], - pre_parsed_predicate: Option<&Expr>, -) -> Result { - let Some(predicate_sql) = &index.predicate_sql else { - return Ok(true); - }; - let expr_owned; - let expr = match pre_parsed_predicate { - Some(expr) => expr, - None => { - expr_owned = crate::sql::parser::parse_expression_sql(predicate_sql)?; - &expr_owned - } - }; - if let Some(result) = simple_stored_column_eq_literal_predicate(table, row_values, expr)? { - return Ok(result); - } - let row_materialized = if generated_columns_are_stored(table) { - Cow::Borrowed(row_values) - } else { - let mut materialized = row_values.to_vec(); - runtime.apply_virtual_generated_columns(table, &mut materialized)?; - Cow::Owned(materialized) - }; - let row_for_eval = row_materialized.as_ref(); - let dataset = table_row_dataset(table, row_for_eval, &table.name); - let bindings = dataset.rows.first().map(Vec::as_slice).unwrap_or(&[]); - Ok(matches!( - runtime.eval_expr(expr, &dataset, bindings, &[], &BTreeMap::new(), None)?, - Value::Bool(true) - )) -} - -/// Pre-parse an index's predicate expression once. Returns `Ok(None)` if the -/// index has no predicate. The returned `Expr` can be reused across many rows -/// to avoid re-parsing the predicate SQL on every per-row index update. -pub(super) fn prepare_index_predicate_expr(index: &IndexSchema) -> Result> { - let Some(predicate_sql) = &index.predicate_sql else { - return Ok(None); - }; - Ok(Some(crate::sql::parser::parse_expression_sql( - predicate_sql, - )?)) -} - -pub(crate) fn row_satisfies_expression( - runtime: &EngineRuntime, - table_name: &str, - column_names: &[String], - row_values: &[Value], - expr: &Expr, -) -> Result { - if column_names.len() != row_values.len() { - return Err(DbError::internal( - "row filter evaluation received mismatched column/value counts", - )); - } - let dataset = Dataset::with_rows( - column_names - .iter() - .map(|column| ColumnBinding::visible(Some(table_name.to_string()), column.clone())) - .collect(), - vec![row_values.to_vec()], - ); - let row = dataset.rows.first().map(Vec::as_slice).unwrap_or(&[]); - Ok(matches!( - runtime.eval_expr(expr, &dataset, row, &[], &BTreeMap::new(), None)?, - Value::Bool(true) - )) -} - -fn simple_stored_column_eq_literal_predicate( - table: &TableSchema, - row_values: &[Value], - expr: &Expr, -) -> Result> { - if !generated_columns_are_stored(table) { - return Ok(None); - } - let Expr::Binary { - left, - op: BinaryOp::Eq, - right, - } = expr - else { - return Ok(None); - }; - let Some((table_qualifier, column_name, literal_value)) = - simple_column_literal_eq(left, right).or_else(|| simple_column_literal_eq(right, left)) - else { - return Ok(None); - }; - if table_qualifier.is_some_and(|qualifier| !identifiers_equal(qualifier, &table.name)) { - return Ok(None); - } - let Some(position) = column_position(table, column_name) else { - return Ok(None); - }; - let Some(column) = table.columns.get(position) else { - return Ok(None); - }; - let Some(row_value) = row_values.get(position) else { - return Ok(None); - }; - if matches!(row_value, Value::Null) || matches!(literal_value, Value::Null) { - return Ok(Some(false)); - } - let literal_value = constraints::coerce_column_value(column, literal_value.clone())?; - Ok(Some( - compare_values(row_value, &literal_value)? == std::cmp::Ordering::Equal, - )) -} - -fn simple_column_literal_eq<'a>( - left: &'a Expr, - right: &'a Expr, -) -> Option<(Option<&'a str>, &'a str, &'a Value)> { - let Expr::Column { table, column } = left else { - return None; - }; - let Expr::Literal(value) = right else { - return None; - }; - Some((table.as_deref(), column.as_str(), value)) -} - -pub(super) fn table_row_dataset(table: &TableSchema, row: &[Value], table_name: &str) -> Dataset { - Dataset::with_rows( - table - .columns - .iter() - .map(|column| ColumnBinding::visible(Some(table_name.to_string()), column.name.clone())) - .collect(), - vec![row.to_vec()], - ) -} - -fn table_bindings_with_hidden_row_id(table: &TableSchema, table_name: &str) -> Vec { - let mut columns = table - .columns - .iter() - .map(|column| { - ColumnBinding::visible_source( - Some(table_name.to_string()), - Some(table.name.clone()), - column.name.clone(), - ) - }) - .collect::>(); - columns.push(ColumnBinding::hidden_source( - Some(table_name.to_string()), - Some(table.name.clone()), - FTS_HIDDEN_ROW_ID_COLUMN.to_string(), - )); - columns -} - -fn decode_root_header(page_bytes: &[u8]) -> Result> { - if page_bytes.iter().all(|byte| *byte == 0) { - return Ok(None); - } - if page_bytes.len() < ENGINE_ROOT_HEADER_SIZE { - return Err(DbError::corruption("catalog root page is truncated")); - } - if page_bytes[0..ENGINE_ROOT_MAGIC.len()] != ENGINE_ROOT_MAGIC { - return Err(DbError::corruption("catalog root page magic is invalid")); - } - let version = u32::from_le_bytes(page_bytes[8..12].try_into().expect("version")); - if version != ENGINE_ROOT_VERSION { - return Err(DbError::corruption(format!( - "unsupported catalog root version {version}" - ))); - } - Ok(Some(RootHeader { - schema_cookie: u32::from_le_bytes(page_bytes[12..16].try_into().expect("cookie")), - payload_checksum: u32::from_le_bytes(page_bytes[16..20].try_into().expect("checksum")), - pointer: OverflowPointer { - head_page_id: u32::from_le_bytes(page_bytes[20..24].try_into().expect("head page")), - logical_len: u32::from_le_bytes(page_bytes[24..28].try_into().expect("logical len")), - flags: page_bytes[28], - }, - })) -} - -fn encode_root_header(page_size: u32, header: RootHeader) -> Vec { - let mut page = vec![0_u8; page_size as usize]; - page[0..8].copy_from_slice(&ENGINE_ROOT_MAGIC); - page[8..12].copy_from_slice(&ENGINE_ROOT_VERSION.to_le_bytes()); - page[12..16].copy_from_slice(&header.schema_cookie.to_le_bytes()); - page[16..20].copy_from_slice(&header.payload_checksum.to_le_bytes()); - page[20..24].copy_from_slice(&header.pointer.head_page_id.to_le_bytes()); - page[24..28].copy_from_slice(&header.pointer.logical_len.to_le_bytes()); - page[28] = header.pointer.flags; - page -} - -#[cfg(test)] -fn encode_runtime_payload(runtime: &EngineRuntime) -> Result> { - let mut output = Vec::new(); - output.extend_from_slice(LEGACY_RUNTIME_PAYLOAD_MAGIC); - encode_u32(&mut output, runtime.catalog.schema_cookie); - encode_u32(&mut output, runtime.catalog.tables.len() as u32); - for table in runtime.catalog.tables.values() { - encode_string(&mut output, &table.name)?; - encode_u32(&mut output, table.columns.len() as u32); - for column in &table.columns { - encode_string(&mut output, &column.name)?; - output.push(encode_column_type(column.column_type)); - output.push(u8::from(column.nullable)); - encode_optional_string(&mut output, column.default_sql.as_deref())?; - output.push(u8::from(column.primary_key)); - output.push(u8::from(column.unique)); - output.push(u8::from(column.auto_increment)); - encode_u32(&mut output, column.checks.len() as u32); - for check in &column.checks { - encode_optional_string(&mut output, check.name.as_deref())?; - encode_string(&mut output, &check.expression_sql)?; - } - output.push(u8::from(column.foreign_key.is_some())); - if let Some(foreign_key) = &column.foreign_key { - encode_foreign_key(&mut output, foreign_key)?; - } - } - encode_u32(&mut output, table.checks.len() as u32); - for check in &table.checks { - encode_optional_string(&mut output, check.name.as_deref())?; - encode_string(&mut output, &check.expression_sql)?; - } - encode_u32(&mut output, table.foreign_keys.len() as u32); - for foreign_key in &table.foreign_keys { - encode_foreign_key(&mut output, foreign_key)?; - } - encode_strings(&mut output, &table.primary_key_columns)?; - encode_i64(&mut output, table.next_row_id); - let data = runtime - .tables - .get(&table.name) - .map(|source| source.resident_data().clone()) - .unwrap_or_default(); - encode_u32(&mut output, data.row_count() as u32); - for row in data.visible_rows() { - encode_i64(&mut output, row.row_id); - let encoded = Row::new(row.values.clone()).encode()?; - encode_bytes(&mut output, &encoded)?; - } - } - - encode_u32(&mut output, runtime.catalog.indexes.len() as u32); - for index in runtime.catalog.indexes.values() { - encode_string(&mut output, &index.name)?; - encode_string(&mut output, &index.table_name)?; - output.push(index.kind as u8); - output.push(u8::from(index.unique)); - encode_u32(&mut output, index.columns.len() as u32); - for column in &index.columns { - encode_optional_string(&mut output, column.column_name.as_deref())?; - encode_optional_string(&mut output, column.expression_sql.as_deref())?; - } - encode_optional_string(&mut output, index.predicate_sql.as_deref())?; - output.push(u8::from(index.fresh)); - } - encode_u32(&mut output, runtime.catalog.views.len() as u32); - for view in runtime.catalog.views.values() { - encode_string(&mut output, &view.name)?; - encode_string(&mut output, &view.sql_text)?; - encode_strings(&mut output, &view.column_names)?; - encode_strings(&mut output, &view.dependencies)?; - } - - encode_u32(&mut output, runtime.catalog.triggers.len() as u32); - for trigger in runtime.catalog.triggers.values() { - encode_string(&mut output, &trigger.name)?; - encode_string(&mut output, &trigger.target_name)?; - output.push(trigger.kind as u8); - output.push(trigger.event as u8); - output.push(u8::from(trigger.on_view)); - encode_string(&mut output, &trigger.action_sql)?; - } - encode_schemas_section(&mut output, &runtime.catalog.schemas)?; - encode_index_include_columns_section(&mut output, &runtime.catalog.indexes)?; - encode_full_text_options_section(&mut output, &runtime.catalog.indexes)?; - encode_generated_columns_section(&mut output, &runtime.catalog.tables)?; - encode_spatial_columns_section(&mut output, &runtime.catalog.tables)?; - encode_enum_columns_section(&mut output, &runtime.catalog.tables)?; - Ok(output) -} - -fn decode_runtime_payload(bytes: &[u8]) -> Result { - let mut cursor = Cursor::new(bytes); - let magic = cursor.read_slice(9)?; - if magic != LEGACY_RUNTIME_PAYLOAD_MAGIC { - return Err(DbError::corruption("catalog state magic is invalid")); - } - let mut runtime = EngineRuntime::empty(cursor.read_u32()?); - let table_count = cursor.read_u32()?; - for _ in 0..table_count { - let table_name = cursor.read_string()?; - let column_count = cursor.read_u32()?; - let mut table = TableSchema { - name: table_name.clone(), - temporary: false, - columns: Vec::with_capacity(column_count as usize), - checks: Vec::new(), - foreign_keys: Vec::new(), - primary_key_columns: Vec::new(), - next_row_id: 1, - pk_index_root: None, - }; - for _ in 0..column_count { - let name = cursor.read_string()?; - let column_type = decode_column_type(cursor.read_u8()?)?; - let nullable = cursor.read_bool()?; - let default_sql = cursor.read_optional_string()?; - let primary_key = cursor.read_bool()?; - let unique = cursor.read_bool()?; - let auto_increment = cursor.read_bool()?; - let check_count = cursor.read_u32()?; - let mut checks = Vec::with_capacity(check_count as usize); - for _ in 0..check_count { - checks.push(crate::catalog::CheckConstraint { - name: cursor.read_optional_string()?, - expression_sql: cursor.read_string()?, - }); - } - let has_fk = cursor.read_bool()?; - let foreign_key = if has_fk { - Some(decode_foreign_key(&mut cursor)?) - } else { - None - }; - table.columns.push(crate::catalog::ColumnSchema { - name, - column_type, - spatial_type: None, - enum_type: None, - nullable, - default_sql, - generated_sql: None, - generated_stored: true, - primary_key, - unique, - auto_increment, - checks, - foreign_key, - }); - } - let table_check_count = cursor.read_u32()?; - for _ in 0..table_check_count { - table.checks.push(crate::catalog::CheckConstraint { - name: cursor.read_optional_string()?, - expression_sql: cursor.read_string()?, - }); - } - let fk_count = cursor.read_u32()?; - for _ in 0..fk_count { - table.foreign_keys.push(decode_foreign_key(&mut cursor)?); - } - table.primary_key_columns = cursor.read_strings()?; - table.next_row_id = cursor.read_i64()?; - let row_count = cursor.read_u32()?; - let mut data = TableData::default(); - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let row_bytes_len = cursor.read_u32()? as usize; - let row_bytes = cursor.read_slice(row_bytes_len)?; - let row = Row::decode(row_bytes)?; - data.push_row(StoredRow { - row_id, - values: row.into_values(), - }); - } - runtime - .catalog_mut() - .tables - .insert(table_name.clone(), table); - runtime.tables_mut().insert(table_name, data.into()); - } - - let index_count = cursor.read_u32()?; - for _ in 0..index_count { - let name = cursor.read_string()?; - let table_name = cursor.read_string()?; - let kind = decode_index_kind(cursor.read_u8()?)?; - let unique = cursor.read_bool()?; - let column_count = cursor.read_u32()?; - let mut columns = Vec::with_capacity(column_count as usize); - for _ in 0..column_count { - columns.push(crate::catalog::IndexColumn { - column_name: cursor.read_optional_string()?, - expression_sql: cursor.read_optional_string()?, - }); - } - let predicate_sql = cursor.read_optional_string()?; - let fresh = cursor.read_bool()?; - runtime.catalog_mut().indexes.insert( - name.clone(), - crate::catalog::IndexSchema { - name, - table_name, - kind, - unique, - columns, - include_columns: Vec::new(), - predicate_sql, - full_text: None, - fresh, - }, - ); - } - let view_count = cursor.read_u32()?; - for _ in 0..view_count { - let view = crate::catalog::ViewSchema { - name: cursor.read_string()?, - temporary: false, - sql_text: cursor.read_string()?, - column_names: cursor.read_strings()?, - dependencies: cursor.read_strings()?, - }; - runtime.catalog_mut().views.insert(view.name.clone(), view); - } - - let trigger_count = cursor.read_u32()?; - for _ in 0..trigger_count { - let trigger = crate::catalog::TriggerSchema { - name: cursor.read_string()?, - target_name: cursor.read_string()?, - kind: decode_trigger_kind(cursor.read_u8()?)?, - event: decode_trigger_event(cursor.read_u8()?)?, - on_view: cursor.read_bool()?, - action_sql: cursor.read_string()?, - }; - runtime - .catalog_mut() - .triggers - .insert(trigger.name.clone(), trigger); - } - if cursor.offset < cursor.bytes.len() { - decode_schemas_section(&mut cursor, &mut runtime.catalog_mut().schemas)?; - } - if cursor.offset < cursor.bytes.len() { - decode_index_include_columns_section(&mut cursor, &mut runtime.catalog_mut().indexes)?; - } - if cursor.offset < cursor.bytes.len() { - decode_full_text_options_section(&mut cursor, &mut runtime.catalog_mut().indexes)?; - } - if cursor.offset < cursor.bytes.len() { - decode_generated_columns_section(&mut cursor, &mut runtime.catalog_mut().tables)?; - } - if cursor.offset < cursor.bytes.len() { - decode_spatial_columns_section(&mut cursor, &mut runtime.catalog_mut().tables)?; - } - if cursor.offset < cursor.bytes.len() { - decode_enum_columns_section(&mut cursor, &mut runtime.catalog_mut().tables)?; - } - if cursor.offset < cursor.bytes.len() { - decode_pk_index_roots_section(&mut cursor, &mut runtime.catalog_mut().tables)?; - } - Ok(runtime) -} - -#[cfg(test)] -fn encode_manifest_payload( - runtime: &EngineRuntime, - table_states: &BTreeMap, -) -> Result> { - Ok(encode_manifest_payload_with_offsets(runtime, table_states)?.bytes) -} - -fn encode_manifest_payload_with_offsets( - runtime: &EngineRuntime, - table_states: &BTreeMap, -) -> Result { - let mut output = Vec::new(); - let mut table_next_row_id_offsets = BTreeMap::new(); - let mut table_state_offsets = BTreeMap::new(); - let mut table_pk_index_root_offsets = BTreeMap::new(); - output.extend_from_slice(MANIFEST_PAYLOAD_MAGIC); - encode_u32(&mut output, runtime.catalog.schema_cookie); - encode_u32(&mut output, runtime.catalog.tables.len() as u32); - for table in runtime.catalog.tables.values() { - encode_string(&mut output, &table.name)?; - encode_u32(&mut output, table.columns.len() as u32); - for column in &table.columns { - encode_string(&mut output, &column.name)?; - output.push(encode_column_type(column.column_type)); - output.push(u8::from(column.nullable)); - encode_optional_string(&mut output, column.default_sql.as_deref())?; - output.push(u8::from(column.primary_key)); - output.push(u8::from(column.unique)); - output.push(u8::from(column.auto_increment)); - encode_u32(&mut output, column.checks.len() as u32); - for check in &column.checks { - encode_optional_string(&mut output, check.name.as_deref())?; - encode_string(&mut output, &check.expression_sql)?; - } - output.push(u8::from(column.foreign_key.is_some())); - if let Some(foreign_key) = &column.foreign_key { - encode_foreign_key(&mut output, foreign_key)?; - } - } - encode_u32(&mut output, table.checks.len() as u32); - for check in &table.checks { - encode_optional_string(&mut output, check.name.as_deref())?; - encode_string(&mut output, &check.expression_sql)?; - } - encode_u32(&mut output, table.foreign_keys.len() as u32); - for foreign_key in &table.foreign_keys { - encode_foreign_key(&mut output, foreign_key)?; - } - encode_strings(&mut output, &table.primary_key_columns)?; - table_next_row_id_offsets.insert(table.name.clone(), output.len()); - encode_i64(&mut output, table.next_row_id); - table_state_offsets.insert(table.name.clone(), output.len()); - let state = table_states.get(&table.name).copied().unwrap_or_default(); - encode_u32(&mut output, state.checksum); - encode_u32(&mut output, state.pointer.head_page_id); - encode_u32(&mut output, state.pointer.logical_len); - output.push(state.pointer.flags); - } - - encode_u32(&mut output, runtime.catalog.indexes.len() as u32); - for index in runtime.catalog.indexes.values() { - encode_string(&mut output, &index.name)?; - encode_string(&mut output, &index.table_name)?; - output.push(index.kind as u8); - output.push(u8::from(index.unique)); - encode_u32(&mut output, index.columns.len() as u32); - for column in &index.columns { - encode_optional_string(&mut output, column.column_name.as_deref())?; - encode_optional_string(&mut output, column.expression_sql.as_deref())?; - } - encode_optional_string(&mut output, index.predicate_sql.as_deref())?; - output.push(u8::from(index.fresh)); - } - encode_u32(&mut output, runtime.catalog.views.len() as u32); - for view in runtime.catalog.views.values() { - encode_string(&mut output, &view.name)?; - encode_string(&mut output, &view.sql_text)?; - encode_strings(&mut output, &view.column_names)?; - encode_strings(&mut output, &view.dependencies)?; - } - - encode_u32(&mut output, runtime.catalog.triggers.len() as u32); - for trigger in runtime.catalog.triggers.values() { - encode_string(&mut output, &trigger.name)?; - encode_string(&mut output, &trigger.target_name)?; - output.push(trigger.kind as u8); - output.push(trigger.event as u8); - output.push(u8::from(trigger.on_view)); - encode_string(&mut output, &trigger.action_sql)?; - } - - let table_stats = runtime - .catalog - .table_stats - .iter() - .filter(|(name, _)| runtime.catalog.tables.contains_key(*name)) - .collect::>(); - encode_u32(&mut output, table_stats.len() as u32); - for (name, stats) in table_stats { - encode_string(&mut output, name)?; - encode_i64(&mut output, stats.row_count); - } - - let index_stats = runtime - .catalog - .index_stats - .iter() - .filter(|(name, _)| runtime.catalog.indexes.contains_key(*name)) - .collect::>(); - encode_u32(&mut output, index_stats.len() as u32); - for (name, stats) in index_stats { - encode_string(&mut output, name)?; - encode_i64(&mut output, stats.entry_count); - encode_i64(&mut output, stats.distinct_key_count); - } - encode_schemas_section(&mut output, &runtime.catalog.schemas)?; - encode_index_include_columns_section(&mut output, &runtime.catalog.indexes)?; - encode_full_text_options_section(&mut output, &runtime.catalog.indexes)?; - encode_generated_columns_section(&mut output, &runtime.catalog.tables)?; - encode_spatial_columns_section(&mut output, &runtime.catalog.tables)?; - encode_enum_columns_section(&mut output, &runtime.catalog.tables)?; - encode_pk_index_roots_section( - &mut output, - &runtime.catalog.tables, - Some(&mut table_pk_index_root_offsets), - )?; - Ok(ManifestEncoding { - bytes: output, - table_next_row_id_offsets, - table_state_offsets, - table_pk_index_root_offsets, - }) -} - -fn patch_manifest_table_next_row_id( - payload: &mut [u8], - offset: usize, - next_row_id: i64, -) -> Result<()> { - let end = offset - .checked_add(8) - .ok_or_else(|| DbError::internal("manifest next_row_id offset overflow"))?; - if end > payload.len() { - return Err(DbError::internal( - "manifest next_row_id offset exceeded payload length", - )); - } - payload[offset..end].copy_from_slice(&next_row_id.to_le_bytes()); - Ok(()) -} - -fn patch_manifest_table_state( - payload: &mut [u8], - offset: usize, - state: PersistedTableState, -) -> Result<()> { - let end = offset - .checked_add(13) - .ok_or_else(|| DbError::internal("manifest table-state offset overflow"))?; - if end > payload.len() { - return Err(DbError::internal( - "manifest table-state offset exceeded payload length", - )); - } - payload[offset..offset + 4].copy_from_slice(&state.checksum.to_le_bytes()); - payload[offset + 4..offset + 8].copy_from_slice(&state.pointer.head_page_id.to_le_bytes()); - payload[offset + 8..offset + 12].copy_from_slice(&state.pointer.logical_len.to_le_bytes()); - payload[offset + 12] = state.pointer.flags; - Ok(()) -} - -fn patch_manifest_table_pk_index_root( - payload: &mut [u8], - offset: usize, - pk_index_root: Option, -) -> Result<()> { - let end = offset - .checked_add(4) - .ok_or_else(|| DbError::internal("manifest pk_index_root offset overflow"))?; - if end > payload.len() { - return Err(DbError::internal( - "manifest pk_index_root offset exceeded payload length", - )); - } - payload[offset..end].copy_from_slice(&pk_index_root.unwrap_or(0).to_le_bytes()); - Ok(()) -} - -fn decode_manifest_payload(_store: &S, bytes: &[u8]) -> Result { - let mut cursor = Cursor::new(bytes); - let magic = cursor.read_slice(MANIFEST_PAYLOAD_MAGIC.len())?; - if magic != MANIFEST_PAYLOAD_MAGIC { - return Err(DbError::corruption("catalog manifest magic is invalid")); - } - let mut runtime = EngineRuntime::empty(cursor.read_u32()?); - let table_count = cursor.read_u32()?; - for _ in 0..table_count { - let table_name = cursor.read_string()?; - let column_count = cursor.read_u32()?; - let mut table = TableSchema { - name: table_name.clone(), - temporary: false, - columns: Vec::with_capacity(column_count as usize), - checks: Vec::new(), - foreign_keys: Vec::new(), - primary_key_columns: Vec::new(), - next_row_id: 1, - pk_index_root: None, - }; - for _ in 0..column_count { - let name = cursor.read_string()?; - let column_type = decode_column_type(cursor.read_u8()?)?; - let nullable = cursor.read_bool()?; - let default_sql = cursor.read_optional_string()?; - let primary_key = cursor.read_bool()?; - let unique = cursor.read_bool()?; - let auto_increment = cursor.read_bool()?; - let check_count = cursor.read_u32()?; - let mut checks = Vec::with_capacity(check_count as usize); - for _ in 0..check_count { - checks.push(crate::catalog::CheckConstraint { - name: cursor.read_optional_string()?, - expression_sql: cursor.read_string()?, - }); - } - let has_fk = cursor.read_bool()?; - let foreign_key = if has_fk { - Some(decode_foreign_key(&mut cursor)?) - } else { - None - }; - table.columns.push(crate::catalog::ColumnSchema { - name, - column_type, - spatial_type: None, - enum_type: None, - nullable, - default_sql, - generated_sql: None, - generated_stored: true, - primary_key, - unique, - auto_increment, - checks, - foreign_key, - }); - } - let table_check_count = cursor.read_u32()?; - for _ in 0..table_check_count { - table.checks.push(crate::catalog::CheckConstraint { - name: cursor.read_optional_string()?, - expression_sql: cursor.read_string()?, - }); - } - let fk_count = cursor.read_u32()?; - for _ in 0..fk_count { - table.foreign_keys.push(decode_foreign_key(&mut cursor)?); - } - table.primary_key_columns = cursor.read_strings()?; - table.next_row_id = cursor.read_i64()?; - let state = PersistedTableState { - checksum: cursor.read_u32()?, - pointer: OverflowPointer { - head_page_id: cursor.read_u32()?, - logical_len: cursor.read_u32()?, - flags: cursor.read_u8()?, - }, - row_count: 0, - tail: OverflowTailInfo::default(), - pk_index_root: None, - }; - runtime - .catalog_mut() - .tables - .insert(table_name.clone(), table); - let has_data = state.pointer.head_page_id != 0 && state.pointer.logical_len != 0; - if has_data { - // Defer row data loading to first statement execution. - runtime.deferred_tables_mut().insert(table_name.clone()); - } - runtime - .persisted_tables_mut() - .insert(table_name.clone(), state); - if !has_data { - // Empty tables are immediately available. - runtime - .tables_mut() - .insert(table_name, TableData::default().into()); - } - } - - let index_count = cursor.read_u32()?; - for _ in 0..index_count { - let name = cursor.read_string()?; - let table_name = cursor.read_string()?; - let kind = decode_index_kind(cursor.read_u8()?)?; - let unique = cursor.read_bool()?; - let column_count = cursor.read_u32()?; - let mut columns = Vec::with_capacity(column_count as usize); - for _ in 0..column_count { - columns.push(crate::catalog::IndexColumn { - column_name: cursor.read_optional_string()?, - expression_sql: cursor.read_optional_string()?, - }); - } - let predicate_sql = cursor.read_optional_string()?; - let fresh = cursor.read_bool()?; - runtime.catalog_mut().indexes.insert( - name.clone(), - crate::catalog::IndexSchema { - name, - table_name, - kind, - unique, - columns, - include_columns: Vec::new(), - predicate_sql, - full_text: None, - fresh, - }, - ); - } - let view_count = cursor.read_u32()?; - for _ in 0..view_count { - let view = crate::catalog::ViewSchema { - name: cursor.read_string()?, - temporary: false, - sql_text: cursor.read_string()?, - column_names: cursor.read_strings()?, - dependencies: cursor.read_strings()?, - }; - runtime.catalog_mut().views.insert(view.name.clone(), view); - } - - let trigger_count = cursor.read_u32()?; - for _ in 0..trigger_count { - let trigger = crate::catalog::TriggerSchema { - name: cursor.read_string()?, - target_name: cursor.read_string()?, - kind: decode_trigger_kind(cursor.read_u8()?)?, - event: decode_trigger_event(cursor.read_u8()?)?, - on_view: cursor.read_bool()?, - action_sql: cursor.read_string()?, - }; - runtime - .catalog_mut() - .triggers - .insert(trigger.name.clone(), trigger); - } - if cursor.offset < cursor.bytes.len() { - let table_stats_count = cursor.read_u32()?; - for _ in 0..table_stats_count { - let name = cursor.read_string()?; - let stats = crate::catalog::TableStats { - row_count: cursor.read_i64()?, - }; - if let Some(state) = runtime.persisted_tables_mut().get_mut(&name) { - state.row_count = usize::try_from(stats.row_count.max(0)).unwrap_or(usize::MAX); - } - runtime.catalog_mut().table_stats.insert(name, stats); - } - } - if cursor.offset < cursor.bytes.len() { - let index_stats_count = cursor.read_u32()?; - for _ in 0..index_stats_count { - let name = cursor.read_string()?; - let stats = crate::catalog::IndexStats { - entry_count: cursor.read_i64()?, - distinct_key_count: cursor.read_i64()?, - }; - runtime.catalog_mut().index_stats.insert(name, stats); - } - } - if cursor.offset < cursor.bytes.len() { - decode_schemas_section(&mut cursor, &mut runtime.catalog_mut().schemas)?; - } - if cursor.offset < cursor.bytes.len() { - decode_index_include_columns_section(&mut cursor, &mut runtime.catalog_mut().indexes)?; - } - if cursor.offset < cursor.bytes.len() { - decode_full_text_options_section(&mut cursor, &mut runtime.catalog_mut().indexes)?; - } - if cursor.offset < cursor.bytes.len() { - decode_generated_columns_section(&mut cursor, &mut runtime.catalog_mut().tables)?; - } - if cursor.offset < cursor.bytes.len() { - decode_spatial_columns_section(&mut cursor, &mut runtime.catalog_mut().tables)?; - } - if cursor.offset < cursor.bytes.len() { - decode_enum_columns_section(&mut cursor, &mut runtime.catalog_mut().tables)?; - } - if cursor.offset < cursor.bytes.len() { - decode_pk_index_roots_section(&mut cursor, &mut runtime.catalog_mut().tables)?; - } - let table_pk_roots = runtime - .catalog - .tables - .iter() - .map(|(table_name, table)| (table_name.clone(), table.pk_index_root)) - .collect::>(); - for (table_name, pk_index_root) in table_pk_roots { - if let Some(state) = runtime.persisted_tables_mut().get_mut(&table_name) { - state.pk_index_root = pk_index_root; - } - } - Ok(runtime) -} - -fn encode_table_payload(data: &TableData) -> Result> { - encode_table_payload_with_tombstone_locators(data).map(|(payload, _)| payload) -} - -fn encode_table_payload_with_tombstone_locators( - data: &TableData, -) -> Result<(Vec, Int64Map)> { - let row_count = data.row_count(); - if row_count == 0 { - return Ok(( - Vec::new(), - Int64Map::with_hasher(Int64HashBuilder::default()), - )); - } - let mut output = Vec::with_capacity(TABLE_PAYLOAD_MAGIC.len() + 4 + row_count * 32); - let mut locators = Int64Map::with_capacity_and_hasher(row_count, Int64HashBuilder::default()); - output.extend_from_slice(TABLE_PAYLOAD_MAGIC); - encode_u32(&mut output, row_count as u32); - let mut encoded_row = Vec::with_capacity(64); - for row in data.visible_rows() { - encode_i64(&mut output, row.row_id); - Row::encode_values_into(&row.values, &mut encoded_row)?; - let row_body_len = encoded_row - .len() - .saturating_add(TABLE_PAYLOAD_ROW_BODY_PADDING_BYTES); - encode_u32( - &mut output, - u32::try_from(row_body_len) - .ok() - .filter(|len| *len < TABLE_PAYLOAD_ROW_TOMBSTONE_FLAG) - .ok_or_else(|| DbError::constraint("table row body length exceeds u32"))?, - ); - locators.insert( - row.row_id, - u32::try_from(output.len().saturating_sub(4)) - .map_err(|_| DbError::constraint("resident tombstone locator exceeds u32"))?, - ); - output.extend_from_slice(&encoded_row); - output.extend(std::iter::repeat_n( - 0u8, - row_body_len.saturating_sub(encoded_row.len()), - )); - } - Ok((output, locators)) -} - -fn encoded_table_row_len(row: &StoredRow, scratch: &mut Vec) -> Result { - scratch.clear(); - Row::encode_values_into(&row.values, scratch)?; - Ok(8usize - .saturating_add(4) - .saturating_add(scratch.len()) - .saturating_add(TABLE_PAYLOAD_ROW_BODY_PADDING_BYTES)) -} - -fn resident_table_should_use_paged_storage( - data: &TableData, - previous_state: PersistedTableState, - delta: &PagedMutationDelta, - page_size: u32, -) -> Result { - if data.rows.is_empty() { - return Ok(false); - } - - let target_chunk_bytes = paged_table_target_chunk_bytes(page_size); - if previous_state.pointer.head_page_id != 0 - && previous_state.pointer.logical_len as usize > target_chunk_bytes - { - return Ok(true); - } - - let append_only = delta.append_count > 0 - && delta.updated_rows.is_empty() - && delta.deleted_rows.is_empty() - && !data.has_tombstoned_rows(); - let mut encoded_len = if append_only && previous_state.pointer.head_page_id != 0 { - previous_state.pointer.logical_len as usize - } else { - TABLE_PAYLOAD_MAGIC.len() + 4 - }; - let start = if append_only && previous_state.pointer.head_page_id != 0 { - data.rows.len().saturating_sub(delta.append_count) - } else { - 0 - }; - let mut scratch = Vec::with_capacity(64); - for row in &data.rows[start..] { - encoded_len = encoded_len.saturating_add(encoded_table_row_len(row, &mut scratch)?); - if encoded_len > target_chunk_bytes { - return Ok(true); - } - } - Ok(false) -} - -fn paged_table_target_chunk_bytes(page_size: u32) -> usize { - (page_size as usize) - .saturating_mul(PAGED_TABLE_TARGET_CHUNK_PAGES) - .max(TABLE_PAYLOAD_MAGIC.len() + 4) -} - -fn paged_table_checkpoint_compaction_min_bytes(page_size: u32) -> usize { - paged_table_target_chunk_bytes(page_size).saturating_div(2) -} - -fn finalize_encoded_paged_table_chunk( - mut payload: Vec, - row_count: usize, -) -> Result { - if payload.len() < TABLE_PAYLOAD_MAGIC.len() + 4 { - return Err(DbError::internal( - "paged table chunk payload shorter than header", - )); - } - payload[TABLE_PAYLOAD_MAGIC.len()..TABLE_PAYLOAD_MAGIC.len() + 4].copy_from_slice( - &u32::try_from(row_count) - .map_err(|_| DbError::constraint("paged table chunk row count exceeds u32"))? - .to_le_bytes(), - ); - let checksum = crc32c_parts(&[payload.as_slice()]); - Ok(EncodedPagedTableChunk { - payload, - checksum, - row_count, - }) -} - -fn encode_paged_table_chunks_from_rows( - rows: &[StoredRow], - page_size: u32, -) -> Result> { - if rows.is_empty() { - return Ok(Vec::new()); - } - - let target_chunk_bytes = paged_table_target_chunk_bytes(page_size); - let mut chunks = Vec::new(); - let mut chunk = Vec::with_capacity(target_chunk_bytes); - chunk.extend_from_slice(TABLE_PAYLOAD_MAGIC); - chunk.extend_from_slice(&0_u32.to_le_bytes()); - let mut chunk_row_count = 0usize; - let mut encoded_row = Vec::with_capacity(64); - - for row in rows { - encoded_row.clear(); - Row::encode_values_into(&row.values, &mut encoded_row)?; - let encoded_row_len = 8usize.saturating_add(4).saturating_add(encoded_row.len()); - if chunk_row_count > 0 && chunk.len().saturating_add(encoded_row_len) > target_chunk_bytes { - chunks.push(finalize_encoded_paged_table_chunk(chunk, chunk_row_count)?); - chunk = Vec::with_capacity(target_chunk_bytes); - chunk.extend_from_slice(TABLE_PAYLOAD_MAGIC); - chunk.extend_from_slice(&0_u32.to_le_bytes()); - chunk_row_count = 0; - } - encode_i64(&mut chunk, row.row_id); - encode_bytes(&mut chunk, &encoded_row)?; - chunk_row_count += 1; - } - - if chunk_row_count > 0 { - chunks.push(finalize_encoded_paged_table_chunk(chunk, chunk_row_count)?); - } - Ok(chunks) -} - -fn encode_paged_table_chunks( - data: &TableData, - page_size: u32, -) -> Result> { - if !data.has_tombstoned_rows() { - return encode_paged_table_chunks_from_rows(&data.rows, page_size); - } - let rows = data.visible_rows().cloned().collect::>(); - encode_paged_table_chunks_from_rows(&rows, page_size) -} - -fn encode_paged_table_manifest_payload(manifest: &PersistedPagedTableManifest) -> Result> { - let mut output = Vec::with_capacity( - TABLE_PAGED_MANIFEST_MAGIC.len() + 4 + manifest.chunks.len().saturating_mul(30), - ); - output.extend_from_slice(TABLE_PAGED_MANIFEST_MAGIC); - encode_u32( - &mut output, - u32::try_from(manifest.chunks.len()) - .map_err(|_| DbError::constraint("paged table chunk count exceeds u32"))?, - ); - for chunk in &manifest.chunks { - encode_u32(&mut output, chunk.checksum); - encode_u32(&mut output, chunk.pointer.head_page_id); - encode_u32(&mut output, chunk.pointer.logical_len); - output.push(chunk.pointer.flags); - encode_u32( - &mut output, - u32::try_from(chunk.row_count) - .map_err(|_| DbError::constraint("paged table chunk row count exceeds u32"))?, - ); - encode_u32( - &mut output, - u32::try_from(chunk.tombstoned_row_ids.len()).map_err(|_| { - DbError::constraint("paged table chunk tombstone count exceeds u32") - })?, - ); - for row_id in &chunk.tombstoned_row_ids { - encode_i64(&mut output, *row_id); - } - output.push(if chunk.overlay_pointer.is_some() { - 1 - } else { - 0 - }); - if let Some(overlay_pointer) = chunk.overlay_pointer { - encode_u32(&mut output, overlay_pointer.head_page_id); - encode_u32(&mut output, overlay_pointer.logical_len); - output.push(overlay_pointer.flags); - encode_u32( - &mut output, - chunk.overlay_checksum.ok_or_else(|| { - DbError::internal("paged table chunk overlay checksum missing") - })?, - ); - } - } - Ok(output) -} - -pub(crate) fn decode_paged_table_manifest_payload( - bytes: &[u8], -) -> Result { - let mut cursor = Cursor::new(bytes); - let magic = cursor.read_slice(TABLE_PAGED_MANIFEST_MAGIC.len())?; - if magic != TABLE_PAGED_MANIFEST_MAGIC { - return Err(DbError::corruption("paged table manifest magic is invalid")); - } - let chunk_count = cursor.read_u32()? as usize; - let mut chunks = Vec::with_capacity(chunk_count); - for _ in 0..chunk_count { - let checksum = cursor.read_u32()?; - let pointer = OverflowPointer { - head_page_id: cursor.read_u32()?, - logical_len: cursor.read_u32()?, - flags: cursor.read_u8()?, - }; - let row_count = cursor.read_u32()? as usize; - let tombstoned_row_ids_len = cursor.read_u32()? as usize; - let mut tombstoned_row_ids = Vec::with_capacity(tombstoned_row_ids_len); - for _ in 0..tombstoned_row_ids_len { - tombstoned_row_ids.push(cursor.read_i64()?); - } - let has_overlay = cursor.read_bool()?; - let mut overlay_pointer = None; - let mut overlay_checksum = None; - if has_overlay { - overlay_pointer = Some(OverflowPointer { - head_page_id: cursor.read_u32()?, - logical_len: cursor.read_u32()?, - flags: cursor.read_u8()?, - }); - overlay_checksum = Some(cursor.read_u32()?); - } - chunks.push(PersistedTableChunkState { - checksum, - pointer, - row_count, - tombstoned_row_ids, - overlay_pointer, - overlay_checksum, - }); - } - if cursor.offset != cursor.bytes.len() { - return Err(DbError::corruption( - "paged table manifest payload had trailing bytes", - )); - } - Ok(PersistedPagedTableManifest { chunks }) -} - -fn read_paged_table_chunk_payloads( - store: &S, - state: PersistedTableState, -) -> Result> { - if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { - return Ok(Vec::new()); - } - let manifest_payload = read_overflow(store, state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); - } - let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - let mut chunks = Vec::with_capacity(manifest.chunks.len()); - let mut total_row_count = 0usize; - for chunk in manifest.chunks { - let payload = Arc::new(read_overflow(store, chunk.pointer)?); - if crc32c_parts(&[payload.as_slice()]) != chunk.checksum { - return Err(DbError::corruption("paged table chunk checksum mismatch")); - } - let tombstoned_row_ids = Arc::new( - chunk - .tombstoned_row_ids - .iter() - .copied() - .collect::>(), - ); - let mut overlay_payload = None; - if let Some(overlay_pointer) = chunk.overlay_pointer { - let p = Arc::new(read_overflow(store, overlay_pointer)?); - if Some(crc32c_parts(&[p.as_slice()])) != chunk.overlay_checksum { - return Err(DbError::corruption( - "paged table overlay chunk checksum mismatch", - )); - } - overlay_payload = Some(p); - } - total_row_count = total_row_count.saturating_add(chunk.row_count); - chunks.push(TablePageManifestChunk { - pointer: chunk.pointer, - checksum: chunk.checksum, - row_count: chunk.row_count, - payload, - tombstoned_row_ids, - overlay_pointer: chunk.overlay_pointer, - overlay_checksum: chunk.overlay_checksum, - overlay_payload, - }); - } - if state.row_count != 0 && total_row_count != state.row_count { - return Err(DbError::corruption( - "paged table manifest row count mismatch", - )); - } - Ok(chunks) -} - -fn visit_table_payload_rows_from_bytes(bytes: &[u8], visitor: &mut F) -> Result -where - F: FnMut(i64, &[Value]) -> Result<()>, -{ - if bytes.is_empty() { - return Ok(0); - } - let mut cursor = Cursor::new(bytes); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - let mut visited = 0usize; - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes = cursor.read_slice(row_bytes_len)?; - if is_tombstone { - continue; - } - let row = Row::decode(row_bytes)?; - visitor(row_id, row.values())?; - visited += 1; - } - Ok(visited) -} - -fn visit_table_payload_projected_values_from_bytes( - bytes: &[u8], - projection_indexes: &[usize], - tombstoned_row_ids: Option<&[i64]>, - visitor: &mut F, -) -> Result -where - F: FnMut(i64, &[Value]) -> Result<()>, -{ - if bytes.is_empty() { - return Ok(0); - } - let mut cursor = Cursor::new(bytes); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - let mut visible_count = 0usize; - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes = cursor.read_slice(row_bytes_len)?; - if is_tombstone { - continue; - } - if tombstoned_row_ids.is_some_and(|row_ids| row_ids.binary_search(&row_id).is_ok()) { - continue; - } - if projection_indexes.is_empty() { - visitor(row_id, &[])?; - } else { - let values = Row::decode_projection_sorted_unique_with_overflow::< - crate::storage::page::InMemoryPageStore, - >(row_bytes, None, projection_indexes)?; - visitor(row_id, &values)?; - } - visible_count += 1; - } - Ok(visible_count) -} - -fn visit_table_payload_projected_values_from_bytes_until( - bytes: &[u8], - projection_indexes: &[usize], - tombstoned_row_ids: Option<&[i64]>, - visitor: &mut F, -) -> Result<(usize, bool)> -where - F: FnMut(i64, &[Value]) -> Result, -{ - if bytes.is_empty() { - return Ok((0, false)); - } - let mut cursor = Cursor::new(bytes); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - let mut visible_count = 0usize; - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes = cursor.read_slice(row_bytes_len)?; - if is_tombstone { - continue; - } - if tombstoned_row_ids.is_some_and(|row_ids| row_ids.binary_search(&row_id).is_ok()) { - continue; - } - visible_count += 1; - if projection_indexes.is_empty() { - if visitor(row_id, &[])? { - return Ok((visible_count, true)); - } - } else { - let values = Row::decode_projection_sorted_unique_with_overflow::< - crate::storage::page::InMemoryPageStore, - >(row_bytes, None, projection_indexes)?; - if visitor(row_id, &values)? { - return Ok((visible_count, true)); - } - } - } - Ok((visible_count, false)) -} - -fn visit_table_payload_rows_from_pointer( - store: &S, - pointer: OverflowPointer, - visitor: &mut F, -) -> Result -where - F: FnMut(i64, &[Value]) -> Result<()>, -{ - if pointer.head_page_id == 0 || pointer.logical_len == 0 { - return Ok(0); - } - if pointer.is_compressed() { - let payload = read_overflow(store, pointer)?; - return visit_table_payload_rows_from_bytes(&payload, visitor); - } - - let mut cursor = OverflowPayloadCursor::new(store, pointer); - let mut magic = [0_u8; TABLE_PAYLOAD_MAGIC.len()]; - cursor.read_exact(&mut magic)?; - if magic != *TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - let mut visited = 0usize; - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes = cursor.read_vec(row_bytes_len)?; - if is_tombstone { - continue; - } - let row = Row::decode(&row_bytes)?; - visitor(row_id, row.values())?; - visited += 1; - } - Ok(visited) -} - -fn visit_table_payload_projected_values_from_pointer( - store: &S, - pointer: OverflowPointer, - projection_indexes: &[usize], - visitor: &mut F, -) -> Result -where - F: FnMut(i64, &[Value]) -> Result<()>, -{ - if pointer.head_page_id == 0 || pointer.logical_len == 0 { - return Ok(0); - } - if pointer.is_compressed() { - let payload = read_overflow(store, pointer)?; - return visit_table_payload_projected_values_from_bytes( - &payload, - projection_indexes, - None, - visitor, - ); - } - - let mut cursor = OverflowPayloadCursor::new(store, pointer); - let mut magic = [0_u8; TABLE_PAYLOAD_MAGIC.len()]; - cursor.read_exact(&mut magic)?; - if magic != *TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - let mut visited = 0usize; - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes = cursor.read_vec(row_bytes_len)?; - if is_tombstone { - continue; - } - if projection_indexes.is_empty() { - visitor(row_id, &[])?; - } else { - let values = Row::decode_projection_sorted_unique_with_overflow::< - crate::storage::page::InMemoryPageStore, - >(&row_bytes, None, projection_indexes)?; - visitor(row_id, &values)?; - } - visited += 1; - } - Ok(visited) -} - -fn visit_table_payload_projected_values_from_pointer_until( - store: &S, - pointer: OverflowPointer, - projection_indexes: &[usize], - visitor: &mut F, -) -> Result<(usize, bool)> -where - F: FnMut(i64, &[Value]) -> Result, -{ - if pointer.head_page_id == 0 || pointer.logical_len == 0 { - return Ok((0, false)); - } - if pointer.is_compressed() { - let payload = read_overflow(store, pointer)?; - return visit_table_payload_projected_values_from_bytes_until( - &payload, - projection_indexes, - None, - visitor, - ); - } - - let mut cursor = OverflowPayloadCursor::new(store, pointer); - let mut magic = [0_u8; TABLE_PAYLOAD_MAGIC.len()]; - cursor.read_exact(&mut magic)?; - if magic != *TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - let mut visited = 0usize; - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes = cursor.read_vec(row_bytes_len)?; - if is_tombstone { - continue; - } - visited += 1; - if projection_indexes.is_empty() { - if visitor(row_id, &[])? { - return Ok((visited, true)); - } - } else { - let values = Row::decode_projection_sorted_unique_with_overflow::< - crate::storage::page::InMemoryPageStore, - >(&row_bytes, None, projection_indexes)?; - if visitor(row_id, &values)? { - return Ok((visited, true)); - } - } - } - Ok((visited, false)) -} - -fn visit_table_payload_int64_column_from_bytes( - bytes: &[u8], - column_index: usize, - tombstoned_row_ids: Option<&[i64]>, - visitor: &mut F, -) -> Result -where - F: FnMut(i64, Option) -> Result<()>, -{ - if bytes.is_empty() { - return Ok(0); - } - let mut cursor = Cursor::new(bytes); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - let mut visible_count = 0usize; - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes = cursor.read_slice(row_bytes_len)?; - if is_tombstone { - continue; - } - if tombstoned_row_ids.is_some_and(|row_ids| row_ids.binary_search(&row_id).is_ok()) { - continue; - } - visitor(row_id, Row::decode_int64_at(row_bytes, column_index)?)?; - visible_count += 1; - } - Ok(visible_count) -} - -fn visit_table_payload_int64_column_from_pointer( - store: &S, - pointer: OverflowPointer, - column_index: usize, - tombstoned_row_ids: Option<&[i64]>, - visitor: &mut F, -) -> Result -where - F: FnMut(i64, Option) -> Result<()>, -{ - let mut payload_scratch = Vec::new(); - visit_table_payload_int64_column_from_pointer_with_scratch( - store, - pointer, - column_index, - tombstoned_row_ids, - &mut payload_scratch, - visitor, - ) -} - -fn visit_table_payload_int64_column_from_pointer_with_scratch( - store: &S, - pointer: OverflowPointer, - column_index: usize, - tombstoned_row_ids: Option<&[i64]>, - payload_scratch: &mut Vec, - visitor: &mut F, -) -> Result -where - F: FnMut(i64, Option) -> Result<()>, -{ - if pointer.head_page_id == 0 || pointer.logical_len == 0 { - return Ok(0); - } - read_overflow_into(store, pointer, payload_scratch)?; - visit_table_payload_int64_column_from_bytes( - payload_scratch, - column_index, - tombstoned_row_ids, - visitor, - ) -} - -fn visit_persisted_table_int64_column( - store: &S, - state: PersistedTableState, - column_index: usize, - mut visitor: F, -) -> Result -where - F: FnMut(i64, Option) -> Result<()>, -{ - if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { - return Ok(0); - } - if !state.pointer.is_table_paged_manifest() { - let row_count = visit_table_payload_int64_column_from_pointer( - store, - state.pointer, - column_index, - None, - &mut visitor, - )?; - if state.row_count != 0 && row_count != state.row_count { - return Err(DbError::corruption("table payload row count mismatch")); - } - return Ok(row_count); - } - - let manifest_payload = read_overflow(store, state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); - } - let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - let mut total_row_count = 0usize; - let mut payload_scratch = Vec::new(); - for chunk in manifest.chunks { - let mut count = 0usize; - let tombstones = if chunk.tombstoned_row_ids.is_empty() { - None - } else { - Some(chunk.tombstoned_row_ids.as_slice()) - }; - - count += visit_table_payload_int64_column_from_pointer_with_scratch( - store, - chunk.pointer, - column_index, - tombstones, - &mut payload_scratch, - &mut visitor, - )?; - - if let Some(overlay_pointer) = chunk.overlay_pointer { - count += visit_table_payload_int64_column_from_pointer_with_scratch( - store, - overlay_pointer, - column_index, - None, - &mut payload_scratch, - &mut visitor, - )?; - } - - if count != chunk.row_count { - return Err(DbError::corruption("paged table chunk row count mismatch")); - } - total_row_count = total_row_count.saturating_add(count); - } - if state.row_count != 0 && total_row_count != state.row_count { - return Err(DbError::corruption( - "paged table manifest row count mismatch", - )); - } - Ok(total_row_count) -} - -fn visit_persisted_table_projected_values( - store: &S, - state: PersistedTableState, - projection_indexes: &[usize], - mut visitor: F, -) -> Result -where - F: FnMut(i64, &[Value]) -> Result<()>, -{ - if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { - return Ok(0); - } - if !state.pointer.is_table_paged_manifest() { - let row_count = visit_table_payload_projected_values_from_pointer( - store, - state.pointer, - projection_indexes, - &mut visitor, - )?; - if state.row_count != 0 && row_count != state.row_count { - return Err(DbError::corruption("table payload row count mismatch")); - } - return Ok(row_count); - } - - let manifest_payload = read_overflow(store, state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); - } - let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - let mut total_row_count = 0usize; - for chunk in manifest.chunks { - let mut count = 0usize; - let tombstones = if chunk.tombstoned_row_ids.is_empty() { - None - } else { - Some(chunk.tombstoned_row_ids.as_slice()) - }; - - let base_payload = read_overflow(store, chunk.pointer)?; - count += visit_table_payload_projected_values_from_bytes( - &base_payload, - projection_indexes, - tombstones, - &mut visitor, - )?; - - if let Some(overlay_pointer) = chunk.overlay_pointer { - let overlay_payload = read_overflow(store, overlay_pointer)?; - count += visit_table_payload_projected_values_from_bytes( - &overlay_payload, - projection_indexes, - None, - &mut visitor, - )?; - } - - if count != chunk.row_count { - return Err(DbError::corruption("paged table chunk row count mismatch")); - } - total_row_count = total_row_count.saturating_add(count); - } - if state.row_count != 0 && total_row_count != state.row_count { - return Err(DbError::corruption( - "paged table manifest row count mismatch", - )); - } - Ok(total_row_count) -} - -fn visit_persisted_table_projected_values_until( - store: &S, - state: PersistedTableState, - projection_indexes: &[usize], - mut visitor: F, -) -> Result -where - F: FnMut(i64, &[Value]) -> Result, -{ - if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { - return Ok(0); - } - if !state.pointer.is_table_paged_manifest() { - let (row_count, stopped) = visit_table_payload_projected_values_from_pointer_until( - store, - state.pointer, - projection_indexes, - &mut visitor, - )?; - if !stopped && state.row_count != 0 && row_count != state.row_count { - return Err(DbError::corruption("table payload row count mismatch")); - } - return Ok(row_count); - } - - let manifest_payload = read_overflow(store, state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); - } - let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - let mut visited_row_count = 0usize; - let mut total_row_count = 0usize; - for chunk in manifest.chunks { - let mut count = 0usize; - let tombstones = if chunk.tombstoned_row_ids.is_empty() { - None - } else { - Some(chunk.tombstoned_row_ids.as_slice()) - }; - - let base_payload = read_overflow(store, chunk.pointer)?; - let (base_count, stopped) = visit_table_payload_projected_values_from_bytes_until( - &base_payload, - projection_indexes, - tombstones, - &mut visitor, - )?; - visited_row_count = visited_row_count.saturating_add(base_count); - count = count.saturating_add(base_count); - if stopped { - return Ok(visited_row_count); - } - - if let Some(overlay_pointer) = chunk.overlay_pointer { - let overlay_payload = read_overflow(store, overlay_pointer)?; - let (overlay_count, stopped) = visit_table_payload_projected_values_from_bytes_until( - &overlay_payload, - projection_indexes, - None, - &mut visitor, - )?; - visited_row_count = visited_row_count.saturating_add(overlay_count); - count = count.saturating_add(overlay_count); - if stopped { - return Ok(visited_row_count); - } - } - - if count != chunk.row_count { - return Err(DbError::corruption("paged table chunk row count mismatch")); - } - total_row_count = total_row_count.saturating_add(count); - } - if state.row_count != 0 && total_row_count != state.row_count { - return Err(DbError::corruption( - "paged table manifest row count mismatch", - )); - } - Ok(visited_row_count) -} - -fn visit_persisted_table_rows( - store: &S, - state: PersistedTableState, - mut visitor: F, -) -> Result -where - F: FnMut(i64, &[Value]) -> Result<()>, -{ - if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { - return Ok(0); - } - if !state.pointer.is_table_paged_manifest() { - let row_count = visit_table_payload_rows_from_pointer(store, state.pointer, &mut visitor)?; - if state.row_count != 0 && row_count != state.row_count { - return Err(DbError::corruption("table payload row count mismatch")); - } - return Ok(row_count); - } - - let manifest_payload = read_overflow(store, state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); - } - let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - let mut total_row_count = 0usize; - for chunk in manifest.chunks { - let mut count = 0usize; - let has_tombstones = !chunk.tombstoned_row_ids.is_empty(); - - let base_payload = read_overflow(store, chunk.pointer)?; - for row in decode_table_payload_rows(&base_payload)? { - if has_tombstones && chunk.tombstoned_row_ids.binary_search(&row.row_id).is_ok() { - continue; - } - visitor(row.row_id, &row.values)?; - count += 1; - } - - if let Some(overlay_pointer) = chunk.overlay_pointer { - let overlay_payload = read_overflow(store, overlay_pointer)?; - for row in decode_table_payload_rows(&overlay_payload)? { - visitor(row.row_id, &row.values)?; - count += 1; - } - } - - if count != chunk.row_count { - return Err(DbError::corruption("paged table chunk row count mismatch")); - } - total_row_count = total_row_count.saturating_add(count); - } - if state.row_count != 0 && total_row_count != state.row_count { - return Err(DbError::corruption( - "paged table manifest row count mismatch", - )); - } - Ok(total_row_count) -} - -fn visit_persisted_table_rows_until( - store: &S, - state: PersistedTableState, - mut visitor: F, -) -> Result -where - F: FnMut(i64, &[Value]) -> Result, -{ - if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { - return Ok(0); - } - if !state.pointer.is_table_paged_manifest() { - let mut stopped = false; - let row_count = - visit_table_payload_rows_from_pointer(store, state.pointer, &mut |row_id, values| { - if stopped { - return Ok(()); - } - stopped = visitor(row_id, values)?; - Ok(()) - })?; - if !stopped && state.row_count != 0 && row_count != state.row_count { - return Err(DbError::corruption("table payload row count mismatch")); - } - return Ok(row_count); - } - - let manifest_payload = read_overflow(store, state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); - } - let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - let mut visited_row_count = 0usize; - let mut total_row_count = 0usize; - for chunk in manifest.chunks { - let mut count = 0usize; - let has_tombstones = !chunk.tombstoned_row_ids.is_empty(); - - let base_payload = read_overflow(store, chunk.pointer)?; - for row in decode_table_payload_rows(&base_payload)? { - if has_tombstones && chunk.tombstoned_row_ids.binary_search(&row.row_id).is_ok() { - continue; - } - visited_row_count = visited_row_count.saturating_add(1); - count += 1; - if visitor(row.row_id, &row.values)? { - return Ok(visited_row_count); - } - } - - if let Some(overlay_pointer) = chunk.overlay_pointer { - let overlay_payload = read_overflow(store, overlay_pointer)?; - for row in decode_table_payload_rows(&overlay_payload)? { - visited_row_count = visited_row_count.saturating_add(1); - count += 1; - if visitor(row.row_id, &row.values)? { - return Ok(visited_row_count); - } - } - } - - if count != chunk.row_count { - return Err(DbError::corruption("paged table chunk row count mismatch")); - } - total_row_count = total_row_count.saturating_add(count); - } - if state.row_count != 0 && total_row_count != state.row_count { - return Err(DbError::corruption( - "paged table manifest row count mismatch", - )); - } - Ok(visited_row_count) -} - -fn decode_table_payload_rows(bytes: &[u8]) -> Result> { - let mut rows = Vec::new(); - visit_table_payload_rows_from_bytes(bytes, &mut |row_id, values| { - rows.push(StoredRow { - row_id, - values: values.to_vec(), - }); - Ok(()) - })?; - Ok(rows) -} - -fn encode_legacy_table_payload_from_manifest(manifest: &TablePageManifest) -> Result> { - if manifest.row_count() == 0 { - return Ok(Vec::new()); - } - let mut rows = Vec::with_capacity(manifest.row_count()); - for row in manifest.rows() { - let row = row?; - rows.push(StoredRow { - row_id: row.row_id(), - values: row.values().to_vec(), - }); - } - encode_table_payload(&TableData::from_rows(rows)) -} - -pub(crate) fn read_table_payload_row_count_from_bytes(bytes: &[u8]) -> Result { - let mut cursor = Cursor::new(bytes); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - Ok(cursor.read_u32()? as usize) -} - -/// ADR 0200: count the live (non-tombstoned) rows in a resident table payload. -/// Unlike [`read_table_payload_row_count_from_bytes`] (which returns the -/// physical slot count from the header), this scans the row stream and skips -/// in-place delete tombstones, so it reports the logical row count used for -/// `COUNT(*)`-style metadata. For payloads without tombstones the result equals -/// the header count. -pub(crate) fn read_table_payload_live_row_count_from_bytes(bytes: &[u8]) -> Result { - if bytes.is_empty() { - return Ok(0); - } - let mut cursor = Cursor::new(bytes); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - let mut live = 0usize; - for _ in 0..row_count { - let _row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - cursor.read_slice(row_bytes_len)?; - if !is_tombstone { - live += 1; - } - } - Ok(live) -} - -fn apply_paged_row_deletions_to_manifest( - manifest: &TablePageManifest, - deleted_row_ids: &BTreeSet, -) -> Result { - if deleted_row_ids.is_empty() { - return Ok(manifest.clone()); - } - - if let Some(updated) = - try_apply_paged_row_deletions_to_manifest_without_base_decode(manifest, deleted_row_ids)? - { - return Ok(updated); - } - - // Partition deleted row ids by the chunk that owns them, using the - // manifest entry index. This avoids decoding any base payload row during a - // pure bulk delete: base rows are immutable, so tombstoning by id is - // sufficient, and only overlay rows that are updated-then-deleted need to - // be decoded and dropped. - let mut deletes_by_chunk: Vec> = (0..manifest.chunks.len()) - .map(|_| BTreeSet::new()) - .collect(); - for &row_id in deleted_row_ids { - let Some(chunk_index) = manifest.chunk_index_for_row_id(row_id) else { - // Row id is not present in the manifest (already gone or never - // existed). Skip it; callers already validated existence. - continue; - }; - if let Some(set) = deletes_by_chunk.get_mut(chunk_index) { - set.insert(row_id); - } - } - - let mut new_chunks = Vec::with_capacity(manifest.chunks.len()); - let mut changed_chunk_indexes = BTreeSet::new(); - for (chunk_index, chunk) in manifest.chunks.iter().enumerate() { - let Some(chunk_deletes) = deletes_by_chunk.get(chunk_index) else { - new_chunks.push(chunk.clone()); - continue; - }; - if chunk_deletes.is_empty() && chunk.overlay_payload.is_none() { - new_chunks.push(chunk.clone()); - continue; - } - - let mut new_tombstones: BTreeSet = chunk.tombstoned_row_ids.iter().copied().collect(); - let mut chunk_changed = false; - let mut overlay_rows: BTreeMap = BTreeMap::new(); - - // Drop overlay rows that are being deleted; keep the rest verbatim. - if let Some(overlay_payload) = &chunk.overlay_payload { - let previous_overlay_rows = decode_table_payload_rows(overlay_payload.as_slice())?; - for previous_row in previous_overlay_rows { - if chunk_deletes.contains(&previous_row.row_id) { - chunk_changed = true; - } else { - overlay_rows.insert(previous_row.row_id, previous_row); - } - } - } - - // Tombstone every deleted id that lives in this chunk's base payload. - for &id in chunk_deletes { - if new_tombstones.insert(id) { - chunk_changed = true; - } - } - - if !chunk_changed { - new_chunks.push(chunk.clone()); - continue; - } - - let overlay_payload = if overlay_rows.is_empty() { - None - } else { - let rows: Vec = overlay_rows.into_values().collect(); - Some(Arc::new(encode_table_payload(&TableData::from_rows(rows))?)) - }; - - new_chunks.push(TablePageManifestChunk { - pointer: chunk.pointer, - checksum: chunk.checksum, - row_count: chunk.row_count, - payload: Arc::clone(&chunk.payload), - tombstoned_row_ids: Arc::new(new_tombstones), - overlay_pointer: None, - overlay_checksum: None, - overlay_payload, - }); - changed_chunk_indexes.insert(chunk_index); - } - - rebuild_table_page_manifest_after_sparse_chunk_changes( - manifest, - new_chunks, - &changed_chunk_indexes, - ) -} - -fn try_apply_paged_row_deletions_to_manifest_without_base_decode( - manifest: &TablePageManifest, - deleted_row_ids: &BTreeSet, -) -> Result> { - let mut planned_deletions = Vec::new(); - try_reserve_paged_directory( - &mut planned_deletions, - deleted_row_ids.len(), - "paged row deletions", - )?; - for &row_id in deleted_row_ids { - let Some((entry_index, entry)) = manifest.rows.entry_for_row_id(row_id)? else { - continue; - }; - if entry.is_overlay { - return Ok(None); - } - let chunk_index = usize::try_from(entry.chunk_index).map_err(|_| { - DbError::corruption("paged table chunk index exceeded chunk list length") - })?; - planned_deletions.push((entry_index, chunk_index, row_id)); - } - - if planned_deletions.is_empty() { - return Ok(Some(manifest.clone())); - } - - planned_deletions.sort_unstable_by_key(|(entry_index, _, _)| *entry_index); - - let mut updated_manifest = manifest.clone(); - let chunks = Arc::make_mut(&mut updated_manifest.chunks); - let tombstoned_row_ids = Arc::make_mut(&mut updated_manifest.tombstoned_row_ids); - - let mut remaining_deletions = planned_deletions.iter().peekable(); - let rebuilt_len = manifest.rows.len().saturating_sub(planned_deletions.len()); - let mut rebuilt_rows = Vec::new(); - try_reserve_paged_directory(&mut rebuilt_rows, rebuilt_len, "sparse paged row entries")?; - for (entry_index, entry) in manifest.rows.iter().enumerate() { - let entry = entry?; - if remaining_deletions - .peek() - .is_some_and(|(delete_entry_index, _, _)| *delete_entry_index == entry_index) - { - remaining_deletions.next(); - continue; - } - rebuilt_rows.push(entry); - } - updated_manifest.rows = Arc::new(TablePageDirectory::Sparse(rebuilt_rows)); - - for &(_, chunk_index, row_id) in &planned_deletions { - let chunk = chunks.get_mut(chunk_index).ok_or_else(|| { - DbError::corruption("paged table chunk index exceeded chunk list length") - })?; - chunk.row_count = chunk - .row_count - .checked_sub(1) - .ok_or_else(|| DbError::corruption("paged table chunk row count underflow"))?; - Arc::make_mut(&mut chunk.tombstoned_row_ids).insert(row_id); - tombstoned_row_ids.insert(row_id); - } - - Ok(Some(updated_manifest)) -} - -fn apply_paged_row_changes_to_manifest( - manifest: &TablePageManifest, - row_changes: &BTreeMap>>, -) -> Result { - if row_changes.is_empty() { - return Ok(manifest.clone()); - } - if let Some(updated) = - try_apply_paged_row_changes_to_manifest_update_only(manifest, row_changes)? - { - return Ok(updated); - } - - let mut changes_by_chunk: Vec>>> = (0..manifest.chunks.len()) - .map(|_| BTreeMap::new()) - .collect(); - for (row_id, change) in row_changes { - let Some(chunk_index) = manifest.chunk_index_for_row_id(*row_id) else { - continue; - }; - if let Some(chunk_changes) = changes_by_chunk.get_mut(chunk_index) { - chunk_changes.insert(*row_id, change); - } - } - - let mut new_chunks = Vec::with_capacity(manifest.chunks.len()); - let mut changed_chunk_indexes = BTreeSet::new(); - for (chunk_index, chunk) in manifest.chunks.iter().enumerate() { - let Some(chunk_changes) = changes_by_chunk.get(chunk_index) else { - new_chunks.push(chunk.clone()); - continue; - }; - if chunk_changes.is_empty() { - new_chunks.push(chunk.clone()); - continue; - } - - let mut new_tombstones: BTreeSet = chunk.tombstoned_row_ids.iter().copied().collect(); - // Use BTreeMap so each row_id appears at most once in the overlay. - let mut overlay_rows: BTreeMap = BTreeMap::new(); - let mut reactivated_base_rows = BTreeSet::new(); - let mut chunk_changed = false; - - // Scan the base payload: tombstone touched rows and queue their - // replacements in the overlay map. - let previous_rows = decode_table_payload_rows(chunk.payload.as_slice())?; - for previous_row in previous_rows { - match chunk_changes.get(&previous_row.row_id).copied() { - Some(Some(next_values)) => { - chunk_changed = true; - if chunk.tombstoned_row_ids.contains(&previous_row.row_id) - && previous_row.values == *next_values - { - new_tombstones.remove(&previous_row.row_id); - reactivated_base_rows.insert(previous_row.row_id); - overlay_rows.remove(&previous_row.row_id); - } else { - new_tombstones.insert(previous_row.row_id); - overlay_rows.insert( - previous_row.row_id, - StoredRow { - row_id: previous_row.row_id, - values: next_values.clone(), - }, - ); - } - } - Some(None) => { - chunk_changed = true; - new_tombstones.insert(previous_row.row_id); - } - None => {} - } - } - - // Scan any existing overlay: replace rows that are re-updated, - // keep untouched rows, drop rows that are deleted. - if let Some(overlay_payload) = &chunk.overlay_payload { - let previous_overlay_rows = decode_table_payload_rows(overlay_payload.as_slice())?; - for previous_row in previous_overlay_rows { - match chunk_changes.get(&previous_row.row_id).copied() { - Some(Some(next_values)) => { - chunk_changed = true; - if reactivated_base_rows.contains(&previous_row.row_id) { - overlay_rows.remove(&previous_row.row_id); - } else { - overlay_rows.insert( - previous_row.row_id, - StoredRow { - row_id: previous_row.row_id, - values: next_values.clone(), - }, - ); - } - } - Some(None) => { - chunk_changed = true; - overlay_rows.remove(&previous_row.row_id); - } - None => { - overlay_rows - .entry(previous_row.row_id) - .or_insert(previous_row); - } - } - } - } - - if !chunk_changed { - new_chunks.push(chunk.clone()); - continue; - } - - let overlay_payload = if overlay_rows.is_empty() { - None - } else { - let rows: Vec = overlay_rows.into_values().collect(); - Some(Arc::new(encode_table_payload(&TableData::from_rows(rows))?)) - }; - - new_chunks.push(TablePageManifestChunk { - pointer: chunk.pointer, - checksum: chunk.checksum, - row_count: chunk.row_count, - payload: Arc::clone(&chunk.payload), - tombstoned_row_ids: Arc::new(new_tombstones), - overlay_pointer: None, - overlay_checksum: None, - overlay_payload, - }); - changed_chunk_indexes.insert(chunk_index); - } - - rebuild_table_page_manifest_after_sparse_chunk_changes( - manifest, - new_chunks, - &changed_chunk_indexes, - ) -} - -fn rebuild_table_page_manifest_after_sparse_chunk_changes( - manifest: &TablePageManifest, - mut new_chunks: Vec, - changed_chunk_indexes: &BTreeSet, -) -> Result { - if changed_chunk_indexes.is_empty() { - return Ok(manifest.clone()); - } - - let tombstoned_row_ids = new_chunks - .iter() - .flat_map(|chunk| chunk.tombstoned_row_ids.iter().copied()) - .collect::>(); - let mut rows = Vec::new(); - try_reserve_paged_directory(&mut rows, manifest.rows.len(), "sparse paged row entries")?; - for entry in manifest.rows.iter() { - let entry = entry?; - if !changed_chunk_indexes.contains(&(entry.chunk_index as usize)) { - rows.push(entry); - } - } - for chunk_index in changed_chunk_indexes { - let Some(chunk) = new_chunks.get(*chunk_index) else { - return Err(DbError::corruption( - "paged table changed chunk index exceeded chunk list length", - )); - }; - let chunk_rows = table_page_entries_for_chunk(*chunk_index, chunk)?; - if let Some(chunk) = new_chunks.get_mut(*chunk_index) { - chunk.row_count = chunk_rows.len(); - } - rows.extend(chunk_rows); - } - rows.sort_by_key(|entry| entry.row_id); - #[cfg(debug_assertions)] - { - for window in rows.windows(2) { - assert_ne!( - window[0].row_id, window[1].row_id, - "duplicate row_id in TablePageManifest rows" - ); - } - } - - Ok(TablePageManifest { - chunks: Arc::new(new_chunks), - rows: Arc::new(TablePageDirectory::Sparse(rows)), - tombstoned_row_ids: Arc::new(tombstoned_row_ids), - }) -} - -fn read_table_page_manifest_from_state( - store: &S, - state: PersistedTableState, -) -> Result { - if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { - return TablePageManifest::from_chunks(Vec::new()); - } - if state.pointer.is_table_paged_manifest() { - return TablePageManifest::from_chunks(read_paged_table_chunk_payloads(store, state)?); - } - let payload = Arc::new(read_overflow(store, state.pointer)?); - if crc32c_parts(&[payload.as_slice()]) != state.checksum { - return Err(DbError::corruption("table payload checksum mismatch")); - } - TablePageManifest::from_payload(payload) -} - -fn decode_persisted_table_data( - store: &S, - state: PersistedTableState, -) -> Result { - let manifest = read_table_page_manifest_from_state(store, state)?; - let mut rows = Vec::with_capacity(manifest.row_count()); - for row in manifest.rows() { - let row = row?; - rows.push(StoredRow { - row_id: row.row_id(), - values: row.values().to_vec(), - }); - } - Ok(TableData::from_rows(rows)) -} - -fn free_persisted_table_bytes( - store: &mut S, - state: PersistedTableState, -) -> Result<()> { - if state.pointer.head_page_id == 0 { - return Ok(()); - } - if state.pointer.is_table_paged_manifest() { - let manifest_payload = read_overflow(store, state.pointer)?; - let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - for chunk in manifest.chunks { - if chunk.pointer.head_page_id != 0 { - free_overflow(store, chunk.pointer.head_page_id)?; - } - } - free_overflow(store, state.pointer.head_page_id)?; - return Ok(()); - } - free_overflow(store, state.pointer.head_page_id)?; - Ok(()) -} - -fn wrap_legacy_table_state_as_paged_manifest( - store: &mut S, - state: PersistedTableState, -) -> Result { - if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { - return Ok(state); - } - let row_count = if state.row_count == 0 { - read_table_payload_row_count(store, state.pointer)? - } else { - state.row_count - }; - let manifest = PersistedPagedTableManifest { - chunks: vec![PersistedTableChunkState { - pointer: state.pointer.with_table_paged_manifest(false), - checksum: state.checksum, - row_count, - tombstoned_row_ids: Vec::new(), - overlay_pointer: None, - overlay_checksum: None, - }], - }; - let manifest_payload = encode_paged_table_manifest_payload(&manifest)?; - let checksum = crc32c_parts(&[manifest_payload.as_slice()]); - let pointer = write_overflow(store, &manifest_payload, CompressionMode::Never)? - .with_table_paged_manifest(true); - let tail = read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default(); - Ok(PersistedTableState { - pointer, - checksum, - row_count, - tail, - pk_index_root: state.pk_index_root, - }) -} - -fn append_paged_table_chunks( - store: &mut S, - mut previous_state: PersistedTableState, - appended_chunks: &[EncodedPagedTableChunk], - row_count: usize, -) -> Result { - if previous_state.pointer.head_page_id == 0 { - return persist_paged_table(store, previous_state, appended_chunks, row_count); - } - if !previous_state.pointer.is_table_paged_manifest() { - previous_state = wrap_legacy_table_state_as_paged_manifest(store, previous_state)?; - } - if appended_chunks.is_empty() { - return Ok(previous_state); - } - - let manifest_payload = read_overflow(store, previous_state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != previous_state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); - } - let mut manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - manifest.chunks.reserve(appended_chunks.len()); - for chunk in appended_chunks { - let pointer = write_overflow(store, &chunk.payload, CompressionMode::Never)?; - manifest.chunks.push(PersistedTableChunkState { - pointer, - checksum: chunk.checksum, - row_count: chunk.row_count, - tombstoned_row_ids: Vec::new(), - overlay_pointer: None, - overlay_checksum: None, - }); - } - - let updated_manifest_payload = encode_paged_table_manifest_payload(&manifest)?; - let checksum = crc32c_parts(&[updated_manifest_payload.as_slice()]); - let pointer = rewrite_overflow( - store, - previous_state.pointer.with_table_paged_manifest(false), - &updated_manifest_payload, - CompressionMode::Never, - )? - .with_table_paged_manifest(true); - let tail = read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default(); - Ok(PersistedTableState { - pointer, - checksum, - row_count, - tail, - pk_index_root: previous_state.pk_index_root, - }) -} - -fn persisted_paged_chunk_is_plain(chunk: &PersistedTableChunkState) -> bool { - chunk.tombstoned_row_ids.is_empty() - && chunk.overlay_pointer.is_none() - && chunk.overlay_checksum.is_none() -} - -fn table_page_manifest_chunk_is_plain(chunk: &TablePageManifestChunk) -> bool { - chunk.tombstoned_row_ids.is_empty() - && chunk.overlay_pointer.is_none() - && chunk.overlay_checksum.is_none() - && chunk.overlay_payload.is_none() -} - -fn persisted_chunk_metadata_matches_current( - persisted: &PersistedTableChunkState, - current: &TablePageManifestChunk, -) -> bool { - persisted.pointer == current.pointer - && persisted.checksum == current.checksum - && persisted.row_count == current.row_count - && persisted.overlay_pointer == current.overlay_pointer - && persisted.overlay_checksum == current.overlay_checksum - && persisted.tombstoned_row_ids.len() == current.tombstoned_row_ids.len() - && persisted - .tombstoned_row_ids - .iter() - .all(|row_id| current.tombstoned_row_ids.contains(row_id)) -} - -fn persisted_chunk_from_current( - pointer: OverflowPointer, - checksum: u32, - row_count: usize, - current_chunk: &TablePageManifestChunk, -) -> TablePageManifestChunk { - TablePageManifestChunk { - pointer, - checksum, - row_count, - payload: Arc::clone(¤t_chunk.payload), - tombstoned_row_ids: Arc::clone(¤t_chunk.tombstoned_row_ids), - overlay_pointer: None, - overlay_checksum: None, - overlay_payload: None, - } -} -fn table_page_manifest_chunk_visible_row_count(chunk: &TablePageManifestChunk) -> Result { - let base_physical = read_table_payload_row_count_from_bytes(&chunk.payload)?; - let overlay_physical = chunk - .overlay_payload - .as_ref() - .map(|payload| read_table_payload_row_count_from_bytes(payload)) - .transpose()? - .unwrap_or(0); - Ok(base_physical - .saturating_sub(chunk.tombstoned_row_ids.len()) - .saturating_add(overlay_physical)) -} - -fn table_page_manifest_with_persisted_chunks( - current: &TablePageManifest, - persisted_chunks: &[TablePageManifestChunk], -) -> TablePageManifest { - TablePageManifest { - chunks: Arc::new(persisted_chunks.to_vec()), - rows: Arc::clone(¤t.rows), - tombstoned_row_ids: Arc::clone(¤t.tombstoned_row_ids), - } -} - -fn try_append_only_paged_table_from_manifest( - store: &mut S, - previous_state: PersistedTableState, - manifest: &TablePageManifest, -) -> Result)>> { - if previous_state.pointer.head_page_id == 0 || !previous_state.pointer.is_table_paged_manifest() - { - return Ok(None); - } - if manifest.chunks.is_empty() { - return Ok(None); - } - - let manifest_payload = read_overflow(store, previous_state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != previous_state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); - } - let previous_manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - if previous_manifest.chunks.is_empty() || manifest.chunks.len() < previous_manifest.chunks.len() - { - return Ok(None); - } - - let previous_tail_index = previous_manifest.chunks.len() - 1; - let mut first_changed_index = None; - let mut current_checksums = Vec::with_capacity(manifest.chunks.len()); - for (index, current_chunk) in manifest.chunks.iter().enumerate() { - if !table_page_manifest_chunk_is_plain(current_chunk) { - return Ok(None); - } - let checksum = crc32c_parts(&[current_chunk.payload.as_slice()]); - current_checksums.push(checksum); - let Some(previous_chunk) = previous_manifest.chunks.get(index) else { - continue; - }; - if !persisted_paged_chunk_is_plain(previous_chunk) { - return Ok(None); - } - if previous_chunk.checksum == checksum - && previous_chunk.row_count == current_chunk.row_count - { - continue; - } - if index != previous_tail_index { - return Ok(None); - } - first_changed_index = Some(index); - } - - if first_changed_index.is_none() && manifest.chunks.len() == previous_manifest.chunks.len() { - return Ok(None); - } - - let mut new_chunks = Vec::with_capacity(manifest.chunks.len()); - let mut persisted_chunks = Vec::with_capacity(manifest.chunks.len()); - - for (index, current_chunk) in manifest.chunks.iter().enumerate() { - let checksum = current_checksums[index]; - if let Some(previous_chunk) = previous_manifest.chunks.get(index) { - if previous_chunk.checksum == checksum - && previous_chunk.row_count == current_chunk.row_count - { - new_chunks.push(previous_chunk.clone()); - persisted_chunks.push(persisted_chunk_from_current( - previous_chunk.pointer, - previous_chunk.checksum, - previous_chunk.row_count, - current_chunk, - )); - continue; - } - - let pointer = rewrite_overflow( - store, - previous_chunk.pointer, - current_chunk.payload.as_slice(), - CompressionMode::Never, - )?; - new_chunks.push(PersistedTableChunkState { - pointer, - checksum, - row_count: current_chunk.row_count, - tombstoned_row_ids: Vec::new(), - overlay_pointer: None, - overlay_checksum: None, - }); - persisted_chunks.push(persisted_chunk_from_current( - pointer, - checksum, - current_chunk.row_count, - current_chunk, - )); - continue; - } - - let pointer = write_overflow( - store, - current_chunk.payload.as_slice(), - CompressionMode::Never, - )?; - new_chunks.push(PersistedTableChunkState { - pointer, - checksum, - row_count: current_chunk.row_count, - tombstoned_row_ids: Vec::new(), - overlay_pointer: None, - overlay_checksum: None, - }); - persisted_chunks.push(persisted_chunk_from_current( - pointer, - checksum, - current_chunk.row_count, - current_chunk, - )); - } - - let updated_manifest_payload = - encode_paged_table_manifest_payload(&PersistedPagedTableManifest { chunks: new_chunks })?; - let checksum = crc32c_parts(&[updated_manifest_payload.as_slice()]); - let pointer = rewrite_overflow( - store, - previous_state.pointer.with_table_paged_manifest(false), - &updated_manifest_payload, - CompressionMode::Never, - )? - .with_table_paged_manifest(true); - let tail = read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default(); + if !db.config().paged_row_storage + && !db.config().persistent_pk_index + && previous_pointer.head_page_id != 0 + && !previous_pointer.is_compressed() + { + if let Some(cached) = self.cached_payload_take(&canonical_table_name) { + let mut payload = Arc::try_unwrap(cached) + .unwrap_or_else(|arc| arc.as_slice().to_vec()); + if let Some(dirty_ranges) = truncate_tail_deleted_rows_payload( + &mut payload, + &delta.deleted_rows, + data.row_count(), + )? { + let checksum = crc32c_parts(&[payload.as_slice()]); + let chain_cache = + match self.overflow_chain_caches.get(&canonical_table_name) { + Some(cache) => cache.clone(), + None => build_overflow_chain_cache( + &store, + previous_pointer.head_page_id, + )?, + }; + let (pointer, new_chain_cache, tail) = + rewrite_overflow_cached_with_dirty_byte_ranges( + &mut store, + previous_pointer, + &payload, + &chain_cache.page_ids, + 0, + Some(dirty_ranges.as_slice()), + )?; + self.overflow_chain_caches + .insert(canonical_table_name.clone(), new_chain_cache); + self.persisted_tables_mut().insert( + canonical_table_name.clone(), + PersistedTableState { + pointer, + checksum, + row_count: data.row_count(), + tail, + pk_index_root: previous_state.pk_index_root, + }, + ); + replace_table_pk_index_root(self, db, &canonical_table_name, None)?; + self.resident_tombstone_locators_mut() + .remove(&canonical_table_name); + self.cache_payload_insert( + canonical_table_name.clone(), + Arc::new(payload), + ); + continue; + } + self.cache_payload_insert( + canonical_table_name.clone(), + Arc::new(payload), + ); + } + } - Ok(Some(( - PersistedTableState { - pointer, - checksum, - row_count: manifest.row_count(), - tail, - pk_index_root: previous_state.pk_index_root, - }, - persisted_chunks, - ))) -} + if !db.config().paged_row_storage + && !db.config().persistent_pk_index + && previous_pointer.head_page_id != 0 + && !previous_pointer.is_compressed() + { + let chain_cache = + match self.overflow_chain_caches.get(&canonical_table_name) { + Some(cache) => Some(cache.clone()), + None => Some(build_overflow_chain_cache( + &store, + previous_pointer.head_page_id, + )?), + }; + if let (Some(locators), Some(chain_cache)) = + (resident_tombstone_locators.as_deref(), chain_cache.as_ref()) + { + let sparse_result = tombstone_deleted_rows_overflow_by_locator( + &mut store, + previous_state, + chain_cache, + &delta.deleted_rows, + locators, + data.row_count(), + )?; + if let Some((mut new_state, new_chain_cache)) = sparse_result { + new_state.pk_index_root = None; + self.persisted_tables_mut() + .insert(canonical_table_name.clone(), new_state); + self.overflow_chain_caches + .insert(canonical_table_name.clone(), new_chain_cache); + replace_table_pk_index_root(self, db, &canonical_table_name, None)?; + self.cache_payload_remove(&canonical_table_name); + continue; + } + } + } -fn compact_paged_table_state_for_checkpoint( - store: &mut S, - state: PersistedTableState, -) -> Result<(PersistedTableState, bool)> { - if state.pointer.head_page_id == 0 || !state.pointer.is_table_paged_manifest() { - return Ok((state, false)); - } + // ADR 0200: obtain the previous on-disk payload (cached + // or read back) so a delete can be applied as in-place + // tombstones instead of shifting every surviving byte. + let previous_payload: Option> = + if let Some(cached) = self.cached_payload_take(&canonical_table_name) { + match Arc::try_unwrap(cached) { + Ok(payload) => Some(payload), + Err(shared) => Some(shared.as_ref().clone()), + } + } else if previous_pointer.head_page_id != 0 { + Some(read_overflow(&store, previous_pointer)?) + } else { + None + }; - let manifest_payload = read_overflow(store, state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); - } - let mut manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - let mut changed = false; - let chunk_compaction_min_bytes = paged_table_checkpoint_compaction_min_bytes(store.page_size()); - let mut freed_pointers: Vec = Vec::new(); - for chunk in &mut manifest.chunks { - let needs_merge = !chunk.tombstoned_row_ids.is_empty() || chunk.overlay_pointer.is_some(); - if !needs_merge { - if chunk.pointer.head_page_id == 0 - || chunk.pointer.is_compressed() - || usize::try_from(chunk.pointer.logical_len) - .ok() - .is_none_or(|len| len < chunk_compaction_min_bytes) - { - continue; - } - let payload = read_overflow(store, chunk.pointer)?; - let pointer = rewrite_overflow( - store, - chunk.pointer, - &payload, - CompressionMode::AutoMinBytes(chunk_compaction_min_bytes), - )?; - if pointer != chunk.pointer { - chunk.pointer = pointer; - changed = true; - } - continue; - } + match previous_payload { + Some(mut payload) => { + // Prefer in-place tombstones unless the table has + // become heavily fragmented, in which case a full + // re-encode of the live resident rows reclaims the + // accumulated dead slots. + let physical = + read_table_payload_row_count_from_bytes(&payload).unwrap_or(0); + let live = data.row_count(); + let dead_after = physical.saturating_sub(live); + // ADR 0200: in-place delete tombstones apply + // only to the resident single-payload storage + // form (`paged_row_storage = false`, e.g. the + // embedded_fast / tuned_durable profiles). When + // `paged_row_storage` is enabled, a resident + // payload can be promoted to a paged manifest by + // later writes/checkpoints, and mixing the two + // representations is unsafe, so those profiles + // keep the compacting splice path. + let tombstoned = + if !db.config().paged_row_storage && live > 0 && dead_after <= live + { + if let Some(locators) = resident_tombstone_locators.as_ref() { + match tombstone_deleted_rows_payload_by_locator( + &mut payload, + &delta.deleted_rows, + locators, + )? { + Some(dirty) => { + resident_tombstone_locators_preserved = true; + Some(dirty) + } + None => tombstone_deleted_rows_payload_in_place( + &mut payload, + &delta.deleted_rows, + )?, + } + } else { + tombstone_deleted_rows_payload_in_place( + &mut payload, + &delta.deleted_rows, + )? + } + } else { + None + }; + if let Some(dirty) = tombstoned { + (payload, dirty, false) + } else if dead_after == 0 { + // No pre-existing tombstones: the surviving + // rows still map 1:1 to the payload, so the + // byte-shifting splice is valid and compacts. + if let Some(dirty_range) = splice_deleted_rows_payload_in_place( + &mut payload, + data, + &delta.deleted_rows, + )? { + (payload, dirty_range, false) + } else { + let splice = splice_deleted_rows_payload( + payload.as_slice(), + data, + &delta.deleted_rows, + )?; + let first = splice.first_dirty_byte; + let last = splice.last_dirty_byte; + (splice.payload, single_dirty_range(first..last), false) + } + } else { + // Fragmented payload (pre-existing tombstones): + // re-encode the authoritative live rows. + let payload = encode_table_payload(data)?; + let last = payload.len(); + (payload, single_dirty_range(0..last), false) + } + } + None => { + let payload = encode_table_payload(data)?; + let last = payload.len(); + (payload, single_dirty_range(0..last), false) + } + } + } else if delta.append_count > 0 + && delta.updated_rows.is_empty() + && delta.deleted_rows.is_empty() + && previous_pointer.head_page_id != 0 + && !data.has_tombstoned_rows() + { + let previous_payload = read_overflow(&store, previous_pointer)?; + // ADR 0200: the append fast path assumes the previous + // payload's physical slot count equals the live row + // count before this append. If the payload carries + // delete tombstones (physical > live), that assumption + // is false, so re-encode the live rows (which also + // reclaims the tombstone slots). + let previous_physical = + read_table_payload_row_count_from_bytes(&previous_payload).unwrap_or(0); + let expected_prior_live = data.row_count().saturating_sub(delta.append_count); + if previous_physical == expected_prior_live { + let payload = append_table_payload(previous_payload, data)?; + let last = payload.len(); + (payload, single_dirty_range(0..last), false) + } else { + let payload = encode_table_payload(data)?; + let last = payload.len(); + (payload, single_dirty_range(0..last), false) + } + } else { + let payload = encode_table_payload(data)?; + let last = payload.len(); + (payload, single_dirty_range(0..last), false) + }; - // Fold tombstones and overlay into a new base payload. - let base_payload = read_overflow(store, chunk.pointer)?; - let base_rows = decode_table_payload_rows(&base_payload)?; - let mut merged_rows: BTreeMap = BTreeMap::new(); - for row in base_rows { - if !chunk.tombstoned_row_ids.contains(&row.row_id) { - merged_rows.insert(row.row_id, row); - } - } - if let Some(overlay_pointer) = chunk.overlay_pointer { - let overlay_payload = read_overflow(store, overlay_pointer)?; - let overlay_rows = decode_table_payload_rows(&overlay_payload)?; - for row in overlay_rows { - merged_rows.insert(row.row_id, row); + let checksum = crc32c_parts(&[payload.as_slice()]); + let (pointer, new_chain_cache, tail) = if let Some(chain_cache) = + self.overflow_chain_caches.get(&canonical_table_name) + { + let page_size = db.config().page_size as usize; + let chunk_cap = page_size.saturating_sub(OVERFLOW_HEADER_SIZE); + let skip = if chunk_cap == 0 { + 0 + } else { + dirty_byte_ranges + .iter() + .map(|range| range.start.checked_div(chunk_cap).unwrap_or(0)) + .min() + .unwrap_or(0) + }; + rewrite_overflow_cached_with_dirty_byte_ranges( + &mut store, + previous_pointer, + &payload, + &chain_cache.page_ids, + skip, + Some(dirty_byte_ranges.as_slice()), + )? + } else { + let ptr = rewrite_overflow( + &mut store, + previous_pointer, + &payload, + CompressionMode::Never, + )?; + let cache = build_overflow_chain_cache(&store, ptr.head_page_id)?; + let tail = read_uncompressed_overflow_tail(&store, ptr)?.unwrap_or_default(); + (ptr, cache, tail) + }; + self.overflow_chain_caches + .insert(canonical_table_name.clone(), new_chain_cache); + let row_count = data.row_count(); + self.persisted_tables_mut().insert( + canonical_table_name.clone(), + PersistedTableState { + pointer, + checksum, + row_count, + tail, + pk_index_root: previous_state.pk_index_root, + }, + ); + let pk_index_root = if db.config().persistent_pk_index && pk_locator_preserved { + previous_state.pk_index_root + } else if db.config().persistent_pk_index { + build_persistent_pk_index_root(db, payload.as_slice())? + } else { + None + }; + replace_table_pk_index_root(self, db, &canonical_table_name, pk_index_root)?; + if use_paged_row_storage || pointer.head_page_id == 0 { + self.resident_tombstone_locators_mut() + .remove(&canonical_table_name); + } else if resident_tombstone_locators_preserved { + // Offsets are unchanged by in-place tombstone patches. + } else if resident_delete_only { + self.resident_tombstone_locators_mut() + .remove(&canonical_table_name); + } else { + let locators = build_resident_tombstone_locators_from_payload(&payload)?; + self.resident_tombstone_locators_mut() + .insert(canonical_table_name.clone(), Arc::new(locators)); + } + self.cache_payload_insert(canonical_table_name, Arc::new(payload)); } - } - let merged: Vec = merged_rows.into_values().collect(); - let merged_len = merged.len(); - let new_payload = encode_table_payload(&TableData::from_rows(merged))?; - let new_checksum = crc32c_parts(&[new_payload.as_slice()]); - let new_pointer = write_overflow( - store, - &new_payload, - CompressionMode::AutoMinBytes(chunk_compaction_min_bytes), - )?; - if chunk.pointer.head_page_id != 0 { - freed_pointers.push(chunk.pointer); - } - if let Some(overlay_pointer) = chunk.overlay_pointer { - if overlay_pointer.head_page_id != 0 { - freed_pointers.push(overlay_pointer); + for table_name in removed_tables { + let Some(state) = self.persisted_tables_mut().remove(&table_name) else { + continue; + }; + self.overflow_chain_caches.remove(&table_name); + self.deferred_paged_row_locator_caches_mut() + .remove(&table_name); + self.resident_tombstone_locators_mut().remove(&table_name); + self.cache_payload_remove(&table_name); + free_persisted_table_bytes(&mut store, state)?; + if state.pk_index_root.is_some() { + free_table_btree(&mut store, state.pk_index_root)?; + } } } - chunk.pointer = new_pointer; - chunk.checksum = new_checksum; - chunk.row_count = merged_len; - chunk.tombstoned_row_ids.clear(); - chunk.overlay_pointer = None; - chunk.overlay_checksum = None; - changed = true; - } - - let should_rewrite_manifest = changed - || (!state.pointer.is_compressed() - && usize::try_from(state.pointer.logical_len) - .ok() - .is_some_and(|len| len >= AUTO_MIN_PAYLOAD_BYTES)); - if !should_rewrite_manifest { - return Ok((state, false)); - } - - let manifest_payload = encode_paged_table_manifest_payload(&manifest)?; - let checksum = crc32c_parts(&[manifest_payload.as_slice()]); - let pointer = rewrite_overflow( - store, - state.pointer.with_table_paged_manifest(false), - &manifest_payload, - CompressionMode::Auto, - )? - .with_table_paged_manifest(true); - let tail = if pointer.is_compressed() { - OverflowTailInfo::default() - } else { - read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default() - }; - let new_state = PersistedTableState { - pointer, - checksum, - row_count: state.row_count, - tail, - pk_index_root: state.pk_index_root, - }; - - // Free old base and overlay pages that were replaced by merge compaction. - // (Non-merge rewrites use rewrite_overflow which reuses/frees old pages - // on its own.) - for old_pointer in freed_pointers { - if old_pointer.head_page_id != 0 { - free_overflow(store, old_pointer.head_page_id)?; - } - } - - Ok((new_state, new_state != state || changed)) -} - -fn paged_table_state_needs_checkpoint_compaction( - store: &S, - state: PersistedTableState, -) -> Result { - if state.pointer.head_page_id == 0 || !state.pointer.is_table_paged_manifest() { - return Ok(false); - } - - let manifest_payload = read_overflow(store, state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); - } - let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - let chunk_compaction_min_bytes = paged_table_checkpoint_compaction_min_bytes(store.page_size()); - for chunk in &manifest.chunks { - if !chunk.tombstoned_row_ids.is_empty() || chunk.overlay_pointer.is_some() { - return Ok(true); - } - if chunk.pointer.head_page_id != 0 - && !chunk.pointer.is_compressed() - && usize::try_from(chunk.pointer.logical_len) - .ok() - .is_some_and(|len| len >= chunk_compaction_min_bytes) - { - return Ok(true); - } - } - Ok(!state.pointer.is_compressed() - && usize::try_from(state.pointer.logical_len) - .ok() - .is_some_and(|len| len >= AUTO_MIN_PAYLOAD_BYTES)) -} + let (checksum, pointer) = { + // Take the chain cache to avoid overlapping borrows with + // manifest_payload (which mutates self.manifest_template). + let chain_cache = self.manifest_chain_cache.take(); + let manifest = self.manifest_payload()?; + let checksum = crc32c_parts(&[manifest]); + let previous_manifest_pointer = old_root.map_or( + OverflowPointer { + head_page_id: 0, + logical_len: 0, + flags: 0, + }, + |root| root.pointer, + ); + let pointer = { + let mut store = DbTxnPageStore { db }; + if let Some(chain_cache) = chain_cache { + let (ptr, new_cache, _tail) = rewrite_overflow_cached( + &mut store, + previous_manifest_pointer, + manifest, + &chain_cache.page_ids, + 0, + )?; + self.manifest_chain_cache = Some(new_cache); + ptr + } else { + let ptr = rewrite_overflow( + &mut store, + previous_manifest_pointer, + manifest, + CompressionMode::Never, + )?; + let cache = build_overflow_chain_cache(&store, ptr.head_page_id)?; + self.manifest_chain_cache = Some(cache); + ptr + } + }; + (checksum, pointer) + }; -fn persist_paged_table( - store: &mut S, - previous_state: PersistedTableState, - encoded_chunks: &[EncodedPagedTableChunk], - row_count: usize, -) -> Result { - if encoded_chunks.is_empty() { - if previous_state.pointer.head_page_id != 0 { - free_persisted_table_bytes(store, previous_state)?; - } - return Ok(PersistedTableState { - pointer: OverflowPointer { - head_page_id: 0, - logical_len: 0, - flags: 0, + let root_page = encode_root_header( + db.config().page_size, + RootHeader { + schema_cookie: self.catalog.schema_cookie, + payload_checksum: checksum, + pointer, }, - checksum: 0, - row_count: 0, - tail: OverflowTailInfo::default(), - pk_index_root: previous_state.pk_index_root, - }); - } - - let mut persisted_chunks = Vec::with_capacity(encoded_chunks.len()); - for chunk in encoded_chunks { - let pointer = write_overflow(store, &chunk.payload, CompressionMode::Never)?; - persisted_chunks.push(PersistedTableChunkState { + ); + db.write_page_owned(page::CATALOG_ROOT_PAGE_ID, root_page)?; + self.dirty_tables_mut().clear(); + self.paged_mutations.clear(); + self.root_state = Some(RootHeader { + schema_cookie: self.catalog.schema_cookie, + payload_checksum: checksum, pointer, - checksum: chunk.checksum, - row_count: chunk.row_count, - tombstoned_row_ids: Vec::new(), - overlay_pointer: None, - overlay_checksum: None, }); - } - let manifest = PersistedPagedTableManifest { - chunks: persisted_chunks, - }; - let manifest_payload = encode_paged_table_manifest_payload(&manifest)?; - let checksum = crc32c_parts(&[manifest_payload.as_slice()]); - let pointer = write_overflow(store, &manifest_payload, CompressionMode::Never)? - .with_table_paged_manifest(true); - let tail = read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default(); - if previous_state.pointer.head_page_id != 0 { - free_persisted_table_bytes(store, previous_state)?; - } - Ok(PersistedTableState { - pointer, - checksum, - row_count, - tail, - pk_index_root: previous_state.pk_index_root, - }) -} - -fn rewrite_paged_table_from_resident( - store: &mut S, - previous_state: PersistedTableState, - data: &TableData, - page_size: u32, -) -> Result { - if previous_state.pointer.head_page_id == 0 || !previous_state.pointer.is_table_paged_manifest() - { - let encoded_chunks = encode_paged_table_chunks(data, page_size)?; - return persist_paged_table(store, previous_state, &encoded_chunks, data.rows.len()); - } - - let manifest_payload = read_overflow(store, previous_state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != previous_state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); - } - let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - let mut current_rows_by_id = Int64Map::default(); - for row in data.visible_rows() { - current_rows_by_id.insert(row.row_id, row); - } - - let mut seen_old_row_ids = std::collections::BTreeSet::new(); - let mut new_chunks = Vec::with_capacity(manifest.chunks.len()); - let mut replaced_chunk_pointers = Vec::new(); - let mut changed = false; - - for chunk in manifest.chunks { - let payload = read_overflow(store, chunk.pointer)?; - if crc32c_parts(&[payload.as_slice()]) != chunk.checksum { - return Err(DbError::corruption("paged table chunk checksum mismatch")); + if schema_cookie_changed { + db.set_schema_cookie(self.catalog.schema_cookie)?; } - let previous_rows = decode_table_payload_rows(payload.as_slice())?; - let mut current_chunk_rows = Vec::with_capacity(previous_rows.len()); - let mut chunk_changed = false; + Ok(()) + } - for previous_row in &previous_rows { - seen_old_row_ids.insert(previous_row.row_id); - if let Some(current_row) = current_rows_by_id.get(&previous_row.row_id).copied() { - if current_row.values != previous_row.values { - chunk_changed = true; + pub(crate) fn has_checkpoint_compaction_candidates( + &self, + store: &S, + _config: &crate::config::DbConfig, + ) -> Result { + for state in self.persisted_tables.values() { + if state.pointer.head_page_id == 0 { + continue; + } + if state.pointer.is_table_paged_manifest() { + if paged_table_state_needs_checkpoint_compaction(store, *state)? { + return Ok(true); } - current_chunk_rows.push(current_row.clone()); - } else { - chunk_changed = true; + continue; + } + if state.pk_index_root.is_none() + && !state.pointer.is_compressed() + && usize::try_from(state.pointer.logical_len) + .ok() + .is_some_and(|len| len >= AUTO_MIN_PAYLOAD_BYTES) + { + return Ok(true); } } + Ok(self.root_state.is_some_and(|root| { + root.pointer.head_page_id != 0 + && !root.pointer.is_compressed() + && usize::try_from(root.pointer.logical_len) + .ok() + .is_some_and(|len| len >= AUTO_MIN_PAYLOAD_BYTES) + })) + } - if !chunk_changed { - new_chunks.push(chunk); - continue; - } - - changed = true; - replaced_chunk_pointers.push(chunk.pointer); - for encoded_chunk in encode_paged_table_chunks_from_rows(¤t_chunk_rows, page_size)? { - let pointer = write_overflow(store, &encoded_chunk.payload, CompressionMode::Never)?; - new_chunks.push(PersistedTableChunkState { - pointer, - checksum: encoded_chunk.checksum, - row_count: encoded_chunk.row_count, - tombstoned_row_ids: Vec::new(), - overlay_pointer: None, - overlay_checksum: None, - }); + pub(super) fn planner_catalog(&self) -> CatalogState { + let mut catalog = self.catalog.as_ref().clone(); + for (name, table) in self.temp_tables.iter() { + catalog.views.remove(name); + catalog.tables.insert(name.clone(), table.clone()); + catalog + .indexes + .retain(|_, index| !identifiers_equal(&index.table_name, name)); + catalog + .triggers + .retain(|_, trigger| !identifiers_equal(&trigger.target_name, name)); + catalog.table_stats.remove(name); } - } - - let appended_rows = data - .visible_rows() - .filter(|row| !seen_old_row_ids.contains(&row.row_id)) - .cloned() - .collect::>(); - if !appended_rows.is_empty() { - changed = true; - for encoded_chunk in encode_paged_table_chunks_from_rows(&appended_rows, page_size)? { - let pointer = write_overflow(store, &encoded_chunk.payload, CompressionMode::Never)?; - new_chunks.push(PersistedTableChunkState { - pointer, - checksum: encoded_chunk.checksum, - row_count: encoded_chunk.row_count, - tombstoned_row_ids: Vec::new(), - overlay_pointer: None, - overlay_checksum: None, - }); + for (name, view) in self.temp_views.iter() { + catalog.tables.remove(name); + catalog.views.insert(name.clone(), view.clone()); + catalog + .triggers + .retain(|_, trigger| !identifiers_equal(&trigger.target_name, name)); } + catalog } - if !changed { - return Ok(previous_state); - } - - if new_chunks.is_empty() { - free_persisted_table_bytes(store, previous_state)?; - return Ok(PersistedTableState { - pointer: OverflowPointer { - head_page_id: 0, - logical_len: 0, - flags: 0, - }, - checksum: 0, - row_count: 0, - tail: OverflowTailInfo::default(), - pk_index_root: previous_state.pk_index_root, - }); + pub(super) fn temp_relation_exists(&self, name: &str) -> bool { + self.temp_table_schema(name).is_some() || self.temp_view(name).is_some() } - let updated_manifest_payload = - encode_paged_table_manifest_payload(&PersistedPagedTableManifest { chunks: new_chunks })?; - let checksum = crc32c_parts(&[updated_manifest_payload.as_slice()]); - let pointer = rewrite_overflow( - store, - previous_state.pointer.with_table_paged_manifest(false), - &updated_manifest_payload, - CompressionMode::Never, - )? - .with_table_paged_manifest(true); - let tail = read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default(); - - for replaced_pointer in replaced_chunk_pointers { - if replaced_pointer.head_page_id != 0 { - free_overflow(store, replaced_pointer.head_page_id)?; + pub(super) fn temp_table_schema(&self, name: &str) -> Option<&TableSchema> { + match compat_schema_qualified_name(name) { + (Some(CompatSchemaQualifier::Main), _) => None, + (_, object) => map_get_ci(&self.temp_tables, object), } } - Ok(PersistedTableState { - pointer, - checksum, - row_count: data.row_count(), - tail, - pk_index_root: previous_state.pk_index_root, - }) -} - -/// Scan a table payload's row ids without decoding row values. Returns the set -/// of row ids stored in the payload (excluding any tombstoned/overlaid rows -/// the caller already handles). Used to decide whether a chunk contains deleted -/// rows without paying the cost of decoding every row's values. -fn scan_table_payload_row_ids(payload: &[u8]) -> Result> { - if payload.len() < TABLE_PAYLOAD_MAGIC.len() + 4 { - return Ok(BTreeSet::new()); - } - let mut cursor = Cursor::new(payload); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - let mut row_ids = BTreeSet::new(); - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - cursor.read_slice(row_bytes_len)?; - if is_tombstone { - continue; + pub(super) fn temp_table_schema_mut(&mut self, name: &str) -> Option<&mut TableSchema> { + let (qualifier, object) = compat_schema_qualified_name(name); + if qualifier == Some(CompatSchemaQualifier::Main) { + return None; } - row_ids.insert(row_id); + map_get_ci_mut(self.temp_tables_mut(), object) } - Ok(row_ids) -} -/// Delete-only variant of [`rewrite_paged_table_from_resident`]. When the -/// transaction only deleted rows (no updates, no appends), the surviving rows -/// are a strict subset of the previous on-disk rows. Chunks that contain no -/// deleted row id are byte-for-byte unchanged and can be reused verbatim -/// without decoding their row values; only chunks that actually hold deleted -/// rows are re-encoded. This avoids decoding every row's values during a bulk -/// delete on a paged table, which previously dominated commit wall time. -fn rewrite_paged_table_from_resident_delete_only( - store: &mut S, - previous_state: PersistedTableState, - data: &TableData, - page_size: u32, - deleted_row_ids: &BTreeSet, -) -> Result { - if previous_state.pointer.head_page_id == 0 || !previous_state.pointer.is_table_paged_manifest() - { - let encoded_chunks = encode_paged_table_chunks(data, page_size)?; - return persist_paged_table(store, previous_state, &encoded_chunks, data.rows.len()); + pub(super) fn temp_table_data(&self, name: &str) -> Option<&TableData> { + match compat_schema_qualified_name(name) { + (Some(CompatSchemaQualifier::Main), _) => None, + (_, object) => map_get_ci(&self.temp_table_data, object).map(|arc| arc.as_ref()), + } } - let manifest_payload = read_overflow(store, previous_state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != previous_state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); + pub(super) fn temp_table_data_mut(&mut self, name: &str) -> Option<&mut TableData> { + let (qualifier, object) = compat_schema_qualified_name(name); + if qualifier == Some(CompatSchemaQualifier::Main) { + return None; + } + self.entry_temp_table_data_mut(object) } - let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - // Index the surviving rows by id so we can re-encode affected chunks from - // the resident data without re-reading them from disk. - let mut current_rows_by_id = Int64Map::default(); - for row in data.visible_rows() { - current_rows_by_id.insert(row.row_id, row); + pub(super) fn temp_view(&self, name: &str) -> Option<&ViewSchema> { + match compat_schema_qualified_name(name) { + (Some(CompatSchemaQualifier::Main), _) => None, + (_, object) => map_get_ci(&self.temp_views, object), + } } - let mut new_chunks = Vec::with_capacity(manifest.chunks.len()); - let mut replaced_chunk_pointers = Vec::new(); - let mut changed = false; - - for chunk in manifest.chunks { - let payload = read_overflow(store, chunk.pointer)?; - if crc32c_parts(&[payload.as_slice()]) != chunk.checksum { - return Err(DbError::corruption("paged table chunk checksum mismatch")); + pub(super) fn visible_view( + &self, + name: &str, + _scope: NameResolutionScope, + ) -> Option<&ViewSchema> { + let (qualifier, object) = compat_schema_qualified_name(name); + if qualifier == Some(CompatSchemaQualifier::Main) { + return self.catalog.view(object); } - // Fast path: scan row ids only (skip value decode) to determine whether - // this chunk contains any deleted row. If not, reuse the chunk as-is. - let chunk_row_ids = scan_table_payload_row_ids(payload.as_slice())?; - let chunk_has_deletes = chunk_row_ids.iter().any(|id| deleted_row_ids.contains(id)); - if !chunk_has_deletes { - new_chunks.push(chunk); - continue; + if let Some(view) = self.temp_view(name) { + return Some(view); } - - changed = true; - replaced_chunk_pointers.push(chunk.pointer); - let previous_rows = decode_table_payload_rows(payload.as_slice())?; - let mut current_chunk_rows = Vec::with_capacity(previous_rows.len()); - for previous_row in &previous_rows { - if let Some(current_row) = current_rows_by_id.get(&previous_row.row_id).copied() { - current_chunk_rows.push(current_row.clone()); - } + if self.temp_table_schema(name).is_some() { + return None; } - for encoded_chunk in encode_paged_table_chunks_from_rows(¤t_chunk_rows, page_size)? { - let pointer = write_overflow(store, &encoded_chunk.payload, CompressionMode::Never)?; - new_chunks.push(PersistedTableChunkState { - pointer, - checksum: encoded_chunk.checksum, - row_count: encoded_chunk.row_count, - tombstoned_row_ids: Vec::new(), - overlay_pointer: None, - overlay_checksum: None, - }); + if qualifier == Some(CompatSchemaQualifier::Temp) { + None + } else { + self.catalog.view(object) } } - if !changed { - return Ok(previous_state); + pub(super) fn visible_table_is_temporary(&self, name: &str) -> bool { + !self.temp_tables.is_empty() && self.temp_table_schema(name).is_some() } - if new_chunks.is_empty() { - free_persisted_table_bytes(store, previous_state)?; - return Ok(PersistedTableState { - pointer: OverflowPointer { - head_page_id: 0, - logical_len: 0, - flags: 0, - }, - checksum: 0, - row_count: 0, - tail: OverflowTailInfo::default(), - pk_index_root: previous_state.pk_index_root, - }); + pub(super) fn table_schema_in_scope( + &self, + name: &str, + _scope: NameResolutionScope, + ) -> Option<&TableSchema> { + let (qualifier, object) = compat_schema_qualified_name(name); + if qualifier == Some(CompatSchemaQualifier::Main) { + return self.catalog.table(object); + } + if self.temp_view(name).is_some() { + return None; + } + if let Some(table) = self.temp_table_schema(name) { + return Some(table); + } + if qualifier == Some(CompatSchemaQualifier::Temp) { + None + } else { + self.catalog.table(object) + } } - let updated_manifest_payload = - encode_paged_table_manifest_payload(&PersistedPagedTableManifest { chunks: new_chunks })?; - let checksum = crc32c_parts(&[updated_manifest_payload.as_slice()]); - let pointer = rewrite_overflow( - store, - previous_state.pointer.with_table_paged_manifest(false), - &updated_manifest_payload, - CompressionMode::Never, - )? - .with_table_paged_manifest(true); - let tail = read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default(); + pub(super) fn table_schema(&self, name: &str) -> Option<&TableSchema> { + self.table_schema_in_scope(name, NameResolutionScope::Session) + } - for replaced_pointer in replaced_chunk_pointers { - if replaced_pointer.head_page_id != 0 { - free_overflow(store, replaced_pointer.head_page_id)?; + pub(super) fn catalog_table_mut(&mut self, name: &str) -> Option<&mut TableSchema> { + let (qualifier, object) = compat_schema_qualified_name(name); + if qualifier == Some(CompatSchemaQualifier::Temp) { + return None; } + let catalog = self.catalog_mut(); + map_get_ci_mut(&mut catalog.tables, object) } - Ok(PersistedTableState { - pointer, - checksum, - row_count: data.row_count(), - tail, - pk_index_root: previous_state.pk_index_root, - }) -} - -fn rewrite_paged_table_from_manifest( - store: &mut S, - previous_state: PersistedTableState, - manifest: &TablePageManifest, -) -> Result<(PersistedTableState, Vec)> { - if manifest.chunks.is_empty() { - if previous_state.pointer.head_page_id != 0 { - free_persisted_table_bytes(store, previous_state)?; + pub(super) fn canonical_catalog_table_name(&self, name: &str) -> Option { + let (qualifier, object) = compat_schema_qualified_name(name); + if qualifier == Some(CompatSchemaQualifier::Temp) { + return None; } - return Ok(( - PersistedTableState { - pointer: OverflowPointer { - head_page_id: 0, - logical_len: 0, - flags: 0, - }, - checksum: 0, - row_count: 0, - tail: OverflowTailInfo::default(), - pk_index_root: previous_state.pk_index_root, - }, - Vec::new(), - )); + self.catalog.table(object).map(|table| table.name.clone()) } - let previous_chunks = if previous_state.pointer.head_page_id != 0 - && previous_state.pointer.is_table_paged_manifest() - { - let manifest_payload = read_overflow(store, previous_state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != previous_state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); + pub(super) fn table_data_in_scope( + &self, + name: &str, + _scope: NameResolutionScope, + ) -> Option<&TableData> { + let (qualifier, object) = compat_schema_qualified_name(name); + if qualifier == Some(CompatSchemaQualifier::Main) { + let table_name = self.catalog.table(object)?.name.clone(); + return self + .tables + .get(&table_name) + .map(TableRowSource::resident_data); } - decode_paged_table_manifest_payload(&manifest_payload)?.chunks - } else { - Vec::new() - }; - let mut previous_payloads = None; - let mut reused_previous = vec![false; previous_chunks.len()]; - let mut replaced_overlay_pointers = Vec::new(); - - let mut new_chunks = Vec::with_capacity(manifest.chunks.len()); - let mut persisted_chunks = Vec::with_capacity(manifest.chunks.len()); - - for (current_index, current_chunk) in manifest.chunks.iter().enumerate() { - if let Some(chunk_state) = previous_chunks.get(current_index) { - if persisted_chunk_metadata_matches_current(chunk_state, current_chunk) { - reused_previous[current_index] = true; - persisted_chunks.push(TablePageManifestChunk { - pointer: chunk_state.pointer, - checksum: chunk_state.checksum, - row_count: chunk_state.row_count, - payload: Arc::clone(¤t_chunk.payload), - tombstoned_row_ids: Arc::clone(¤t_chunk.tombstoned_row_ids), - overlay_pointer: chunk_state.overlay_pointer, - overlay_checksum: chunk_state.overlay_checksum, - overlay_payload: current_chunk.overlay_payload.clone(), - }); - new_chunks.push(chunk_state.clone()); - continue; - } - if chunk_state.pointer.head_page_id != 0 - && chunk_state.pointer == current_chunk.pointer - && chunk_state.checksum == current_chunk.checksum - { - reused_previous[current_index] = true; - let current_overlay_checksum = current_chunk - .overlay_payload - .as_ref() - .map(|payload| crc32c_parts(&[payload.as_slice()])); - let (overlay_pointer, overlay_checksum) = match ( - ¤t_chunk.overlay_payload, - current_chunk.overlay_pointer, - current_chunk.overlay_checksum, - ) { - (Some(_), Some(pointer), Some(checksum)) - if Some(pointer) == chunk_state.overlay_pointer - && Some(checksum) == chunk_state.overlay_checksum => - { - (Some(pointer), Some(checksum)) - } - (Some(overlay_payload), _, _) => { - let pointer = write_overflow( - store, - overlay_payload.as_slice(), - CompressionMode::Never, - )?; - let checksum = current_overlay_checksum.ok_or_else(|| { - DbError::internal("overlay checksum missing for paged table chunk") - })?; - (Some(pointer), Some(checksum)) - } - (None, _, _) => (None, None), - }; - if let Some(previous_overlay_pointer) = chunk_state.overlay_pointer { - if Some(previous_overlay_pointer) != overlay_pointer - && previous_overlay_pointer.head_page_id != 0 - { - replaced_overlay_pointers.push(previous_overlay_pointer); - } - } - let visible = table_page_manifest_chunk_visible_row_count(current_chunk)?; - new_chunks.push(PersistedTableChunkState { - pointer: chunk_state.pointer, - checksum: chunk_state.checksum, - row_count: visible, - tombstoned_row_ids: current_chunk.tombstoned_row_ids.iter().copied().collect(), - overlay_pointer, - overlay_checksum, - }); - persisted_chunks.push(TablePageManifestChunk { - pointer: chunk_state.pointer, - checksum: chunk_state.checksum, - row_count: visible, - payload: Arc::clone(¤t_chunk.payload), - tombstoned_row_ids: Arc::clone(¤t_chunk.tombstoned_row_ids), - overlay_pointer, - overlay_checksum, - overlay_payload: current_chunk.overlay_payload.clone(), - }); - continue; - } + if self.temp_view(name).is_some() { + return None; + } + if let Some(table) = self.temp_table_schema(name) { + return self.temp_table_data(&table.name); + } + if qualifier == Some(CompatSchemaQualifier::Temp) { + None + } else { + let table_name = self.catalog.table(object)?.name.clone(); + self.tables + .get(&table_name) + .map(TableRowSource::resident_data) } + } - if previous_payloads.is_none() { - if previous_state.pointer.head_page_id != 0 - && previous_state.pointer.is_table_paged_manifest() - { - previous_payloads = Some(read_paged_table_chunk_payloads(store, previous_state)?); - } else { - previous_payloads = Some(Vec::new()); - } + pub(super) fn table_row_source(&self, name: &str) -> Option<&TableRowSource> { + let (qualifier, object) = compat_schema_qualified_name(name); + if qualifier == Some(CompatSchemaQualifier::Temp) { + return None; } - let reusable_previous = previous_payloads - .as_ref() - .expect("previous payloads loaded"); - let checksum = crc32c_parts(&[current_chunk.payload.as_slice()]); - let current_overlay_checksum = current_chunk - .overlay_payload - .as_ref() - .map(|payload| crc32c_parts(&[payload.as_slice()])); - let reused_index = reusable_previous - .iter() - .enumerate() - .find_map(|(index, payload)| { - let chunk_state = previous_chunks.get(index)?; - let overlay_match = match ( - &payload.overlay_payload, - ¤t_chunk.overlay_payload, - chunk_state.overlay_checksum, - current_overlay_checksum, - ) { - (None, None, None, None) => true, - (Some(previous), Some(current), Some(previous_checksum), Some(checksum)) => { - previous_checksum == checksum && previous.as_slice() == current.as_slice() - } - _ => false, - }; - (!reused_previous[index] - && chunk_state.checksum == checksum - && payload.payload.as_slice() == current_chunk.payload.as_slice() - && chunk_state.row_count == current_chunk.row_count - && chunk_state.tombstoned_row_ids.len() - == current_chunk.tombstoned_row_ids.len() - && chunk_state - .tombstoned_row_ids - .iter() - .all(|id| current_chunk.tombstoned_row_ids.contains(id)) - && overlay_match) - .then_some(index) - }); - if let Some(index) = reused_index { - reused_previous[index] = true; - let chunk_state = previous_chunks[index].clone(); - persisted_chunks.push(TablePageManifestChunk { - pointer: chunk_state.pointer, - checksum: chunk_state.checksum, - row_count: chunk_state.row_count, - payload: Arc::clone(¤t_chunk.payload), - tombstoned_row_ids: Arc::clone(¤t_chunk.tombstoned_row_ids), - overlay_pointer: chunk_state.overlay_pointer, - overlay_checksum: chunk_state.overlay_checksum, - overlay_payload: current_chunk.overlay_payload.clone(), - }); - new_chunks.push(chunk_state); - continue; + if self.temp_view(name).is_some() { + return None; } - - let pointer = write_overflow(store, ¤t_chunk.payload, CompressionMode::Never)?; - let (overlay_pointer, overlay_checksum) = - if let Some(overlay_payload) = ¤t_chunk.overlay_payload { - let overlay_pointer = - write_overflow(store, overlay_payload.as_slice(), CompressionMode::Never)?; - let overlay_checksum = crc32c_parts(&[overlay_payload.as_slice()]); - (Some(overlay_pointer), Some(overlay_checksum)) - } else { - (None, None) - }; - let visible = table_page_manifest_chunk_visible_row_count(current_chunk)?; - new_chunks.push(PersistedTableChunkState { - pointer, - checksum, - row_count: visible, - tombstoned_row_ids: current_chunk.tombstoned_row_ids.iter().copied().collect(), - overlay_pointer, - overlay_checksum, - }); - persisted_chunks.push(TablePageManifestChunk { - pointer, - checksum, - row_count: visible, - payload: Arc::clone(¤t_chunk.payload), - tombstoned_row_ids: Arc::clone(¤t_chunk.tombstoned_row_ids), - overlay_pointer, - overlay_checksum, - overlay_payload: current_chunk.overlay_payload.clone(), - }); + let table_name = self.catalog.table(object)?.name.clone(); + self.tables.get(&table_name) } - if new_chunks == previous_chunks { - return Ok((previous_state, persisted_chunks)); + pub(super) fn prepared_insert_target_loaded(&self, table_name: &str) -> bool { + self.tables.contains_key(table_name) || self.temp_tables.contains_key(table_name) } - let updated_manifest_payload = - encode_paged_table_manifest_payload(&PersistedPagedTableManifest { chunks: new_chunks })?; - let checksum = crc32c_parts(&[updated_manifest_payload.as_slice()]); - let pointer = rewrite_overflow( - store, - previous_state.pointer.with_table_paged_manifest(false), - &updated_manifest_payload, - CompressionMode::Never, - )? - .with_table_paged_manifest(true); - let tail = read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default(); - - for (index, chunk_state) in previous_chunks.iter().enumerate() { - if reused_previous[index] || chunk_state.pointer.head_page_id == 0 { - continue; + fn visible_table_row_source_in_scope( + &self, + name: &str, + _scope: NameResolutionScope, + ) -> Option> { + let (qualifier, object) = compat_schema_qualified_name(name); + if qualifier == Some(CompatSchemaQualifier::Main) { + let table_name = self.catalog.table(object)?.name.clone(); + return self + .tables + .get(&table_name) + .map(VisibleTableRowSource::Base); } - free_overflow(store, chunk_state.pointer.head_page_id)?; - if let Some(overlay_pointer) = chunk_state.overlay_pointer { - if overlay_pointer.head_page_id != 0 { - free_overflow(store, overlay_pointer.head_page_id)?; - } + if self.temp_view(name).is_some() { + return None; } - } - for overlay_pointer in replaced_overlay_pointers { - if overlay_pointer.head_page_id != 0 { - free_overflow(store, overlay_pointer.head_page_id)?; + if let Some(table) = self.temp_table_schema(name) { + return self + .temp_table_data(&table.name) + .map(VisibleTableRowSource::Temp); + } + if qualifier == Some(CompatSchemaQualifier::Temp) { + None + } else { + let table_name = self.catalog.table(object)?.name.clone(); + self.tables + .get(&table_name) + .map(VisibleTableRowSource::Base) } } - Ok(( - PersistedTableState { - pointer, - checksum, - row_count: manifest.row_count(), - tail, - pk_index_root: previous_state.pk_index_root, - }, - persisted_chunks, - )) -} + pub(crate) fn visible_table_row_source(&self, name: &str) -> Option> { + self.visible_table_row_source_in_scope(name, NameResolutionScope::Session) + } -fn build_persistent_pk_index_root(db: &crate::db::Db, payload: &[u8]) -> Result> { - let entries = build_row_locator_entries(payload)?; - let mut tree = Btree::new(DbTxnPageStore { db }); - tree.replace_entries(entries)?; - let (_store, root_page_id) = tree.into_parts(); - Ok(root_page_id) -} + pub(super) fn table_data(&self, name: &str) -> Option<&TableData> { + self.table_data_in_scope(name, NameResolutionScope::Session) + } -fn build_persistent_pk_index_root_from_chunk_payloads( - db: &crate::db::Db, - chunk_payloads: &[TablePageManifestChunk], -) -> Result> { - let entries = build_paged_row_locator_entries_from_chunk_payloads(chunk_payloads)?; - let mut tree = Btree::new(DbTxnPageStore { db }); - tree.replace_entries(entries)?; - let (_store, root_page_id) = tree.into_parts(); - Ok(root_page_id) -} + pub(super) fn persisted_table_state(&self, name: &str) -> Option { + let table_name = self.catalog.table(name)?.name.clone(); + self.persisted_tables.get(&table_name).copied() + } -fn replace_table_pk_index_root( - runtime: &mut EngineRuntime, - db: &crate::db::Db, - table_name: &str, - new_pk_index_root: Option, -) -> Result<()> { - let previous_pk_index_root = runtime - .persisted_tables - .get(table_name) - .and_then(|state| state.pk_index_root) - .or_else(|| { - runtime - .catalog - .tables - .get(table_name) - .and_then(|table| table.pk_index_root) - }); - match previous_pk_index_root { - Some(previous_pk_index_root) if Some(previous_pk_index_root) != new_pk_index_root => { - let mut store = DbTxnPageStore { db }; - free_table_btree(&mut store, Some(previous_pk_index_root))?; + pub(super) fn table_data_mut_in_scope( + &mut self, + name: &str, + _scope: NameResolutionScope, + ) -> Option<&mut TableData> { + let (qualifier, object) = compat_schema_qualified_name(name); + if qualifier == Some(CompatSchemaQualifier::Main) { + return self.entry_table_data_mut(object); + } + if self.temp_view(name).is_some() { + return None; + } + if self.temp_table_schema(name).is_some() { + return self.temp_table_data_mut(name); + } + if qualifier == Some(CompatSchemaQualifier::Temp) { + None + } else { + self.entry_table_data_mut(object) } - _ => {} - } - let table = runtime - .catalog_mut() - .tables - .get_mut(table_name) - .ok_or_else(|| DbError::internal(format!("table schema for {table_name} is missing")))?; - table.pk_index_root = new_pk_index_root; - if let Some(state) = runtime.persisted_tables_mut().get_mut(table_name) { - state.pk_index_root = new_pk_index_root; } - Ok(()) -} -/// Build a new payload by splicing only the modified rows into the cached -/// previous payload. Unchanged row bytes are copied verbatim from `old`, -/// saving the per-row serialisation cost for the common single-row UPDATE. -/// Result of a splice operation, containing the new payload and dirty byte -/// range metadata. -struct SpliceResult { - payload: Vec, - /// Byte offset of the first modified byte in the OLD payload. - first_dirty_byte: usize, - /// Exclusive byte offset of the first byte after the changed range in the - /// OLD payload, when conservative behavior is used this may be payload - /// length. - last_dirty_byte: usize, - /// Whether the updated payload preserves row offsets and can reuse - /// the previous persistent PK locator root. - pk_locator_preserved: bool, -} + pub(super) fn table_data_mut(&mut self, name: &str) -> Option<&mut TableData> { + self.table_data_mut_in_scope(name, NameResolutionScope::Session) + } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -struct SpliceDirtyRange { - first_dirty_byte: usize, - last_dirty_byte: usize, -} + pub(super) fn replace_table_row_source( + &mut self, + name: &str, + row_source: TableRowSource, + ) -> Result<()> { + let Some(table_name) = self.canonical_catalog_table_name(name) else { + return Err(DbError::internal(format!( + "table row source for {name} is missing" + ))); + }; + self.tables_mut().insert(table_name, row_source); + Ok(()) + } -fn single_dirty_range(range: Range) -> Vec> { - std::iter::once(range).collect() -} + pub(crate) fn has_redeferable_persisted_tables(&self, names: &[&str]) -> bool { + names.iter().any(|name| { + let Some(table_name) = self.canonical_catalog_table_name(name) else { + return false; + }; + self.persisted_tables + .get(&table_name) + .is_some_and(|state| state.pointer.is_table_paged_manifest()) + && self.tables.contains_key(&table_name) + }) + } -fn splice_updated_rows_payload_in_place( - payload: &mut [u8], - data: &TableData, - dirty_indices: &[usize], -) -> Result> { - const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; + pub(crate) fn rebuild_stale_indexes(&mut self, page_size: u32) -> Result<()> { + if self + .catalog + .indexes + .iter() + .all(|(name, index)| index.fresh && self.indexes.contains_key(name)) + { + return Ok(()); + } - if payload.len() < HEADER_LEN || payload[..8] != *TABLE_PAYLOAD_MAGIC { - return Ok(None); - } - let old_row_count = - u32::from_le_bytes(payload[8..12].try_into().expect("row-count header length")) as usize; - if old_row_count != data.rows.len() { - return Ok(None); + // ADR 0143 Phase B: under per-table deferred materialization, an + // index whose target table has not yet been loaded must not be + // rebuilt — its rows are not in memory. If the index was still + // resident from before the table was deferred, it remains valid; + // otherwise the index is rebuilt when the table itself is + // materialized and no longer appears in `deferred_tables`. + let names = self + .catalog + .indexes + .iter() + .filter(|(name, index)| !index.fresh || !self.indexes.contains_key(*name)) + .filter(|(_, index)| { + !self + .deferred_tables + .iter() + .any(|table_name| identifiers_equal(table_name, &index.table_name)) + }) + .map(|(name, _)| name.clone()) + .collect::>(); + for name in names { + self.rebuild_index(&name, page_size)?; + } + Ok(()) } - let mut sorted_dirty: Vec = dirty_indices.to_vec(); - sorted_dirty.sort_unstable(); - sorted_dirty.dedup(); - if sorted_dirty.is_empty() { - return Ok(Some(SpliceDirtyRange { - first_dirty_byte: payload.len(), - last_dirty_byte: payload.len(), - })); + pub(super) fn mark_table_dirty(&mut self, table_name: &str) { + if self.visible_table_is_temporary(table_name) { + return; + } + let Some(table_name) = self.canonical_catalog_table_name(table_name) else { + return; + }; + self.catalog_mut().table_stats.remove(&table_name); + self.paged_mutations.remove(&table_name); + self.dirty_tables_mut().insert(table_name); } - let mut row_spans: Vec<(usize, usize)> = Vec::with_capacity(sorted_dirty.len()); - let mut scan_offset = HEADER_LEN; - let mut dirty_cursor = 0; - let mut row_idx = 0; - while dirty_cursor < sorted_dirty.len() && scan_offset + 12 <= payload.len() { - if row_idx >= old_row_count { - break; - } - let row_id = i64::from_le_bytes( - payload[scan_offset..scan_offset + 8] - .try_into() - .expect("row id length"), - ); - let row_data_len = u32::from_le_bytes( - payload[scan_offset + 8..scan_offset + 12] - .try_into() - .expect("row data len"), - ) as usize; - let row_end = scan_offset.saturating_add(12).saturating_add(row_data_len); - if row_end > payload.len() { - return Ok(None); + pub(super) fn mark_table_row_dirty( + &mut self, + table_name: &str, + _row_index: usize, + row_id: i64, + values: &[Value], + ) { + if self.visible_table_is_temporary(table_name) { + return; } - if row_idx == sorted_dirty[dirty_cursor] { - let Some(row) = data.rows.get(row_idx) else { - return Ok(None); - }; - if row.row_id != row_id { - return Ok(None); - } - row_spans.push((scan_offset, row_end)); - dirty_cursor += 1; - if dirty_cursor == sorted_dirty.len() { - break; - } + let Some(table_name) = self.canonical_catalog_table_name(table_name) else { + return; + }; + self.catalog_mut().table_stats.remove(&table_name); + if self.dirty_tables.contains(&table_name) + && !self.paged_mutations.contains_key(&table_name) + { + return; } - scan_offset = row_end; - row_idx += 1; - } - if row_spans.len() != sorted_dirty.len() { - return Ok(None); + self.dirty_tables_mut().insert(table_name.clone()); + self.paged_mutations + .entry(table_name) + .or_default() + .updated_rows + .insert(row_id, values.to_vec()); } - let mut encoded_rows: Vec> = Vec::with_capacity(sorted_dirty.len()); - let mut encoded_row = Vec::with_capacity(128); - for (span_idx, &dirty_row) in sorted_dirty.iter().enumerate() { - let Some(row) = data.rows.get(dirty_row) else { - return Ok(None); + pub(super) fn mark_table_row_dirty_with_original_values( + &mut self, + table_name: &str, + _row_index: usize, + row_id: i64, + original_values: &[Value], + values: &[Value], + ) { + if self.visible_table_is_temporary(table_name) { + return; + } + let Some(table_name) = self.canonical_catalog_table_name(table_name) else { + return; }; - let (span_start, span_end) = row_spans[span_idx]; - let old_row_body_len = span_end.saturating_sub(span_start).saturating_sub(12); - encoded_row.clear(); - Row::encode_values_into(&row.values, &mut encoded_row)?; - if encoded_row.len() > old_row_body_len { - return Ok(None); + self.catalog_mut().table_stats.remove(&table_name); + if self.dirty_tables.contains(&table_name) + && !self.paged_mutations.contains_key(&table_name) + { + return; } - encoded_rows.push(encoded_row.clone()); - } - for (span_idx, encoded_row) in encoded_rows.iter().enumerate() { - let (span_start, span_end) = row_spans[span_idx]; - let body_start = span_start.saturating_add(12); - let body_written_end = body_start.saturating_add(encoded_row.len()); - payload[body_start..body_written_end].copy_from_slice(encoded_row); - payload[body_written_end..span_end].fill(0); - } - - Ok(Some(SpliceDirtyRange { - first_dirty_byte: row_spans - .first() - .map_or(0, |span| span.0.saturating_add(12)), - last_dirty_byte: row_spans.last().map_or(payload.len(), |span| span.1), - })) -} + let restores_original = self + .paged_mutations + .get(&table_name) + .and_then(|delta| delta.original_rows.get(&row_id)) + .is_some_and(|original| original == values); -/// ADR 0200: mark the given row ids as deleted in place by setting the -/// tombstone flag on each slot's `row_body_len` field. The body bytes are left -/// untouched, so only four bytes per deleted row change and the payload length -/// is unchanged. This collapses a scattered delete from "rewrite every byte -/// after the first deletion" down to "patch one length field per deleted row". -/// -/// Returns the dirty byte ranges to persist, or `None` when the payload is not -/// a recognizable resident table payload or a targeted row id is absent / already -/// tombstoned — in which case the caller falls back to the splice / full -/// re-encode path. -fn tombstone_deleted_rows_payload_in_place( - payload: &mut [u8], - deleted_row_ids: &BTreeSet, -) -> Result>>> { - const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; + if restores_original || values == original_values { + if let Some(delta) = self.paged_mutations.get_mut(&table_name) { + delta.updated_rows.remove(&row_id); + delta.original_rows.remove(&row_id); + if delta.updated_rows.is_empty() + && delta.deleted_rows.is_empty() + && delta.append_count == 0 + { + self.paged_mutations.remove(&table_name); + self.dirty_tables_mut().remove(&table_name); + } + } + return; + } - if deleted_row_ids.is_empty() { - return Ok(Some(Vec::new())); - } - if payload.len() < HEADER_LEN || payload[..8] != *TABLE_PAYLOAD_MAGIC { - return Ok(None); + self.dirty_tables_mut().insert(table_name.clone()); + let delta = self.paged_mutations.entry(table_name.clone()).or_default(); + delta + .original_rows + .entry(row_id) + .or_insert_with(|| original_values.to_vec()); + delta.updated_rows.insert(row_id, values.to_vec()); } - let row_count = - u32::from_le_bytes(payload[8..12].try_into().expect("row-count header length")) as usize; - let mut remaining = deleted_row_ids.len(); - let mut dirty_ranges = Vec::with_capacity(deleted_row_ids.len()); - let mut offset = HEADER_LEN; - let mut scanned_rows = 0usize; - while remaining > 0 && offset + 12 <= payload.len() { - if scanned_rows >= row_count { - break; + pub(super) fn mark_table_row_deleted(&mut self, table_name: &str, row_id: i64) { + if self.visible_table_is_temporary(table_name) { + return; } - let row_id = i64::from_le_bytes( - payload[offset..offset + 8] - .try_into() - .expect("row id length"), - ); - let len_field_offset = offset + 8; - let raw_len = u32::from_le_bytes( - payload[len_field_offset..len_field_offset + 4] - .try_into() - .expect("row data len"), - ); - let (is_tombstone, body_len) = split_table_payload_row_len(raw_len); - let Some(row_end) = len_field_offset - .checked_add(4) - .and_then(|value| value.checked_add(body_len)) - else { - return Ok(None); + let Some(table_name) = self.canonical_catalog_table_name(table_name) else { + return; }; - if row_end > payload.len() { - return Ok(None); - } - if !is_tombstone && deleted_row_ids.contains(&row_id) { - let flagged = raw_len | TABLE_PAYLOAD_ROW_TOMBSTONE_FLAG; - payload[len_field_offset..len_field_offset + 4].copy_from_slice(&flagged.to_le_bytes()); - dirty_ranges.push(len_field_offset..len_field_offset + 4); - remaining -= 1; + self.catalog_mut().table_stats.remove(&table_name); + if self.dirty_tables.contains(&table_name) + && !self.paged_mutations.contains_key(&table_name) + { + return; } - offset = row_end; - scanned_rows += 1; - } - - if remaining > 0 { - // A targeted row id was not found as a live slot. Fall back so the - // caller re-encodes from the authoritative resident rows. - return Ok(None); - } - Ok(Some(dirty_ranges)) -} - -fn tombstone_deleted_rows_payload_by_locator( - payload: &mut [u8], - deleted_row_ids: &BTreeSet, - locators: &Int64Map, -) -> Result>>> { - const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; - - if deleted_row_ids.is_empty() { - return Ok(Some(Vec::new())); - } - if payload.len() < HEADER_LEN || payload[..8] != *TABLE_PAYLOAD_MAGIC { - return Ok(None); + self.dirty_tables_mut().insert(table_name.clone()); + self.paged_mutations + .entry(table_name) + .or_default() + .deleted_rows + .insert(row_id); } - let mut dirty_ranges = Vec::with_capacity(deleted_row_ids.len()); - for row_id in deleted_row_ids { - let Some(&len_field_offset) = locators.get(row_id) else { - return Ok(None); - }; - let len_field_offset = len_field_offset as usize; - if len_field_offset < 8 || len_field_offset + 4 > payload.len() { - return Ok(None); - } - let row_id_offset = len_field_offset - 8; - let actual_row_id = i64::from_le_bytes( - payload[row_id_offset..row_id_offset + 8] - .try_into() - .expect("row id length"), - ); - if actual_row_id != *row_id { - return Ok(None); + pub(super) fn mark_table_rows_deleted(&mut self, table_name: &str, row_ids: &BTreeSet) { + if row_ids.is_empty() || self.visible_table_is_temporary(table_name) { + return; } - let raw_len = u32::from_le_bytes( - payload[len_field_offset..len_field_offset + 4] - .try_into() - .expect("row data len"), - ); - let (is_tombstone, body_len) = split_table_payload_row_len(raw_len); - let Some(row_end) = len_field_offset - .checked_add(4) - .and_then(|value| value.checked_add(body_len)) - else { - return Ok(None); + let Some(table_name) = self.canonical_catalog_table_name(table_name) else { + return; }; - if row_end > payload.len() || is_tombstone { - return Ok(None); + self.catalog_mut().table_stats.remove(&table_name); + if self.dirty_tables.contains(&table_name) + && !self.paged_mutations.contains_key(&table_name) + { + return; + } + self.dirty_tables_mut().insert(table_name.clone()); + let deleted_rows = &mut self + .paged_mutations + .entry(table_name) + .or_default() + .deleted_rows; + if deleted_rows.is_empty() { + *deleted_rows = row_ids.clone(); + } else { + deleted_rows.extend(row_ids.iter().copied()); } - - let flagged = raw_len | TABLE_PAYLOAD_ROW_TOMBSTONE_FLAG; - payload[len_field_offset..len_field_offset + 4].copy_from_slice(&flagged.to_le_bytes()); - dirty_ranges.push(len_field_offset..len_field_offset + 4); - } - Ok(Some(dirty_ranges)) -} - -fn tombstone_deleted_rows_cached_payload_by_locator( - payload: &mut [u8], - deleted_row_ids: &BTreeSet, - locators: &Int64Map, - previous_checksum: u32, -) -> Result>, u32)>> { - const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; - - if deleted_row_ids.is_empty() { - return Ok(Some((Vec::new(), previous_checksum))); - } - if payload.len() < HEADER_LEN || payload[..8] != *TABLE_PAYLOAD_MAGIC { - return Ok(None); } - let mut patches = Vec::with_capacity(deleted_row_ids.len()); - let mut span_start = usize::MAX; - let mut span_end = 0usize; - for row_id in deleted_row_ids { - let Some(&len_field_offset) = locators.get(row_id) else { - return Ok(None); - }; - let len_field_offset = len_field_offset as usize; - if len_field_offset < 8 || len_field_offset + 4 > payload.len() { - return Ok(None); + pub(super) fn mark_table_row_appended(&mut self, table_name: &str) { + if self.visible_table_is_temporary(table_name) { + return; } - let row_id_offset = len_field_offset - 8; - let actual_row_id = i64::from_le_bytes( - payload[row_id_offset..row_id_offset + 8] - .try_into() - .expect("row id length"), - ); - if actual_row_id != *row_id { - return Ok(None); + if let Some(delta) = self.paged_mutations.get_mut(table_name) { + delta.append_count += 1; + return; } - let raw_len = u32::from_le_bytes( - payload[len_field_offset..len_field_offset + 4] - .try_into() - .expect("row data len"), - ); - let (is_tombstone, body_len) = split_table_payload_row_len(raw_len); - let Some(row_end) = len_field_offset - .checked_add(4) - .and_then(|value| value.checked_add(body_len)) - else { - return Ok(None); + if self.dirty_tables.contains(table_name) { + return; + } + let Some(table_name) = self.canonical_catalog_table_name(table_name) else { + return; }; - if row_end > payload.len() || is_tombstone { - return Ok(None); + self.catalog_mut().table_stats.remove(&table_name); + if let Some(delta) = self.paged_mutations.get_mut(table_name.as_str()) { + delta.append_count += 1; + return; + } + if self.dirty_tables.contains(table_name.as_str()) { + return; } - let new_bytes = (raw_len | TABLE_PAYLOAD_ROW_TOMBSTONE_FLAG).to_le_bytes(); - span_start = span_start.min(len_field_offset); - span_end = span_end.max(len_field_offset + 4); - patches.push((len_field_offset, new_bytes)); + self.dirty_tables_mut().insert(table_name.clone()); + self.paged_mutations + .entry(table_name) + .or_default() + .append_count += 1; } - if patches.is_empty() { - return Ok(Some((Vec::new(), previous_checksum))); - } - let old_span = payload[span_start..span_end].to_vec(); - let mut dirty_ranges = Vec::with_capacity(patches.len()); - for (offset, new_bytes) in patches { - if payload[offset..offset + 4] != new_bytes { - payload[offset..offset + 4].copy_from_slice(&new_bytes); - dirty_ranges.push(offset..offset + 4); - } + pub(super) fn mark_all_tables_dirty(&mut self) { + let table_names = self.catalog.tables.keys().cloned().collect::>(); + self.dirty_tables_mut().extend(table_names); + self.paged_mutations.clear(); } - let checksum = crc32c_patch_bytes( - previous_checksum, - payload.len(), - span_start, - &old_span, - &payload[span_start..span_end], - ) - .ok_or_else(|| DbError::internal("resident tombstone checksum patch failed"))?; - Ok(Some((dirty_ranges, checksum))) -} - -fn truncate_tail_deleted_rows_payload( - payload: &mut Vec, - deleted_row_ids: &BTreeSet, - live_row_count: usize, -) -> Result>>> { - const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; - if deleted_row_ids.is_empty() { - return Ok(Some(Vec::new())); - } - if payload.len() < HEADER_LEN || payload[..8] != *TABLE_PAYLOAD_MAGIC { - return Ok(None); - } - let physical_row_count = - u32::from_le_bytes(payload[8..12].try_into().expect("row-count header length")) as usize; - if physical_row_count != live_row_count.saturating_add(deleted_row_ids.len()) { - return Ok(None); + pub(crate) fn execute_statement( + &mut self, + statement: &Statement, + params: &[Value], + page_size: u32, + ) -> Result { + match statement { + Statement::Query(_) | Statement::Explain(_) => { + self.execute_read_statement(statement, params, page_size) + } + Statement::Insert(statement) => { + let result = self.execute_insert(statement, params, page_size)?; + Ok(result) + } + Statement::Update(statement) => { + let result = self.execute_update(statement, params, page_size)?; + Ok(result) + } + Statement::Delete(statement) => { + let result = self.execute_delete(statement, params, page_size)?; + Ok(result) + } + Statement::Analyze { table_name } => { + self.execute_analyze(table_name.as_deref())?; + Ok(QueryResult::with_affected_rows(0)) + } + Statement::CreateTable(statement) => { + self.execute_create_table(statement)?; + if !statement.temporary { + self.rebuild_indexes(page_size)?; + } + Ok(QueryResult::with_affected_rows(0)) + } + Statement::CreateSchema { + name, + if_not_exists, + } => { + self.execute_create_schema(name, *if_not_exists)?; + Ok(QueryResult::with_affected_rows(0)) + } + Statement::CreateTableAs(statement) => { + let result = self.execute_create_table_as(statement, params, page_size)?; + if !statement.temporary { + self.rebuild_indexes(page_size)?; + } + Ok(result) + } + Statement::CreateIndex(statement) => { + if let Some(index_name) = self.execute_create_index(statement, page_size)? { + self.rebuild_index(&index_name, page_size)?; + } + Ok(QueryResult::with_affected_rows(0)) + } + Statement::AlterIndexRebuild { name } => { + self.rebuild_index(name, page_size)?; + Ok(QueryResult::with_affected_rows(0)) + } + Statement::AlterIndexVerify { name } => { + self.verify_index(name, page_size)?; + Ok(QueryResult::with_affected_rows(0)) + } + Statement::CreateView(statement) => { + self.execute_create_view(statement)?; + if !statement.temporary { + self.rebuild_indexes(page_size)?; + } + Ok(QueryResult::with_affected_rows(0)) + } + Statement::CreateTrigger(statement) => { + self.execute_create_trigger(statement)?; + self.rebuild_indexes(page_size)?; + Ok(QueryResult::with_affected_rows(0)) + } + Statement::DropTable { name, if_exists } => { + let temporary = self.temp_table_schema(name).is_some(); + self.execute_drop_table(name, *if_exists, page_size)?; + if !temporary { + self.rebuild_indexes(page_size)?; + } + Ok(QueryResult::with_affected_rows(0)) + } + Statement::DropIndex { name, if_exists } => { + self.execute_drop_index(name, *if_exists)?; + self.rebuild_indexes(page_size)?; + Ok(QueryResult::with_affected_rows(0)) + } + Statement::DropView { name, if_exists } => { + let temporary = self.temp_view(name).is_some(); + self.execute_drop_view(name, *if_exists)?; + if !temporary { + self.rebuild_indexes(page_size)?; + } + Ok(QueryResult::with_affected_rows(0)) + } + Statement::DropTrigger { + name, + table_name, + if_exists, + } => { + self.execute_drop_trigger(name, table_name, *if_exists)?; + self.rebuild_indexes(page_size)?; + Ok(QueryResult::with_affected_rows(0)) + } + Statement::AlterViewRename { + view_name, + new_name, + } => { + self.execute_alter_view_rename(view_name, new_name)?; + self.rebuild_indexes(page_size)?; + Ok(QueryResult::with_affected_rows(0)) + } + Statement::AlterTable { + table_name, + actions, + } => { + self.execute_alter_table(table_name, actions, params, page_size)?; + Ok(QueryResult::with_affected_rows(0)) + } + Statement::TruncateTable { + table_name, + identity, + cascade, + } => { + self.execute_truncate_table( + table_name, + *identity == TruncateIdentityMode::Restart, + *cascade, + page_size, + )?; + Ok(QueryResult::with_affected_rows(0)) + } + } } - let mut offset = HEADER_LEN; - let mut first_deleted_offset = None; - let mut deleted_seen = 0usize; - for _ in 0..physical_row_count { - if offset + 12 > payload.len() { - return Ok(None); + fn execute_create_table_as( + &mut self, + statement: &CreateTableAsStatement, + params: &[Value], + page_size: u32, + ) -> Result { + let (qualifier, object_name) = compat_schema_qualified_name(&statement.table_name); + if statement.temporary && qualifier == Some(CompatSchemaQualifier::Main) { + return Err(DbError::sql( + "temporary tables cannot be created in the main schema", + )); } - let row_start = offset; - let row_id = i64::from_le_bytes( - payload[offset..offset + 8] - .try_into() - .expect("row id length"), - ); - let len_field_offset = offset + 8; - let raw_len = u32::from_le_bytes( - payload[len_field_offset..len_field_offset + 4] - .try_into() - .expect("row data len"), - ); - let (is_tombstone, body_len) = split_table_payload_row_len(raw_len); - if is_tombstone { - return Ok(None); + let temporary = statement.temporary || qualifier == Some(CompatSchemaQualifier::Temp); + let table_name = object_name.to_string(); + if temporary { + if self.temp_relation_exists(&table_name) { + if statement.if_not_exists && self.temp_table_schema(&table_name).is_some() { + return Ok(QueryResult::with_affected_rows(0)); + } + return Err(DbError::sql(format!( + "object {} already exists", + table_name + ))); + } + } else if self.catalog.contains_object(&table_name) { + if statement.if_not_exists && self.catalog.table(&table_name).is_some() { + return Ok(QueryResult::with_affected_rows(0)); + } + return Err(DbError::sql(format!( + "object {} already exists", + table_name + ))); } - let Some(row_end) = len_field_offset - .checked_add(4) - .and_then(|value| value.checked_add(body_len)) - else { - return Ok(None); + + let mut source = self.evaluate_query(&statement.query, params, &BTreeMap::new())?; + let target_columns = if statement.column_names.is_empty() { + source + .columns + .iter() + .enumerate() + .map(|(index, binding)| { + if binding.name.is_empty() { + format!("column{}", index + 1) + } else { + binding.name.clone() + } + }) + .collect::>() + } else { + if statement.column_names.len() != source.columns.len() { + return Err(DbError::sql(format!( + "CREATE TABLE AS expected {} column names but query produced {} columns", + statement.column_names.len(), + source.columns.len() + ))); + } + statement.column_names.clone() + }; + + let columns = target_columns + .iter() + .enumerate() + .map(|(index, name)| ColumnDefinition { + name: name.clone(), + column_type: infer_column_type_for_ctas(&source.rows, index), + spatial_type: None, + enum_type: None, + nullable: true, + default: None, + generated: None, + generated_stored: true, + primary_key: false, + unique: false, + checks: Vec::new(), + references: None, + }) + .collect::>(); + let create_statement = CreateTableStatement { + table_name: table_name.clone(), + temporary, + if_not_exists: false, + columns, + constraints: Vec::new(), }; - if row_end > payload.len() { - return Ok(None); + self.execute_create_table(&create_statement)?; + if !statement.with_data { + return Ok(QueryResult::with_affected_rows(0)); } - if deleted_row_ids.contains(&row_id) { - if first_deleted_offset.is_none() { - first_deleted_offset = Some(row_start); + + let temporary = self.visible_table_is_temporary(&table_name); + let mut affected_rows = 0_u64; + for source_row in source.take_rows() { + let candidate = { + let mut staged_table = self + .table_schema(&table_name) + .cloned() + .ok_or_else(|| DbError::sql(format!("unknown table {}", table_name)))?; + let candidate = dml::build_insert_row_values( + self, + &mut staged_table, + &target_columns, + source_row, + params, + )?; + if temporary { + self.temp_table_schema_mut(&table_name) + .ok_or_else(|| DbError::sql(format!("unknown table {}", table_name)))? + .next_row_id = staged_table.next_row_id; + } else { + self.catalog_mut() + .tables + .get_mut(&table_name) + .ok_or_else(|| DbError::sql(format!("unknown table {}", table_name)))? + .next_row_id = staged_table.next_row_id; + } + candidate + }; + self.validate_row(&table_name, &candidate, None, params)?; + let row_id = { + let table = self + .table_schema(&table_name) + .ok_or_else(|| DbError::sql(format!("unknown table {}", table_name)))?; + dml::primary_row_id(table, &candidate) + .unwrap_or_else(|| dml::next_row_id(self, &table_name)) + }; + let stored_row = StoredRow { + row_id, + values: candidate, + }; + let index_updates = + self.prepare_insert_index_updates(&table_name, &stored_row, page_size)?; + self.table_data_mut(&table_name) + .ok_or_else(|| { + DbError::internal(format!("table data for {table_name} is missing")) + })? + .push_row(stored_row); + self.apply_insert_index_updates(index_updates)?; + if !temporary { + self.mark_table_dirty(&table_name); } - deleted_seen += 1; - } else if first_deleted_offset.is_some() { - return Ok(None); + affected_rows += 1; } - offset = row_end; - } - if offset != payload.len() || deleted_seen != deleted_row_ids.len() { - return Ok(None); - } - let Some(truncate_at) = first_deleted_offset else { - return Ok(None); - }; - - payload[8..12].copy_from_slice( - &u32::try_from(live_row_count) - .map_err(|_| DbError::constraint("table row count exceeds u32"))? - .to_le_bytes(), - ); - payload.truncate(truncate_at); - let mut dirty_ranges = Vec::with_capacity(2); - dirty_ranges.push(8..12); - if !payload.is_empty() { - let tail_start = payload.len().saturating_sub(1); - dirty_ranges.push(tail_start..payload.len()); + Ok(QueryResult::with_affected_rows(affected_rows)) } - Ok(Some(dirty_ranges)) -} -fn tombstone_deleted_rows_overflow_by_locator( - store: &mut S, - previous_state: PersistedTableState, - chain_cache: &OverflowChainCache, - deleted_row_ids: &BTreeSet, - locators: &Int64Map, - live_row_count: usize, -) -> Result> { - if deleted_row_ids.is_empty() { - return Ok(Some((previous_state, chain_cache.clone()))); - } - let pointer = previous_state.pointer; - if pointer.head_page_id == 0 - || pointer.logical_len == 0 - || pointer.is_compressed() - || pointer.is_table_paged_manifest() - { - return Ok(None); - } - if locators.is_empty() { - return Ok(None); - } - let dead_after = locators.len().saturating_sub(live_row_count); - if live_row_count == 0 || dead_after > live_row_count { - return Ok(None); - } + pub(crate) fn execute_read_statement( + &self, + statement: &Statement, + params: &[Value], + _page_size: u32, + ) -> Result { + self.clear_fts_eval_context()?; + match statement { + Statement::Query(query) => { + if self.security_rules_active()? { + return self + .evaluate_query(query, params, &BTreeMap::new()) + .map(dataset_to_result); + } + if let Some(result) = Self::try_execute_simple_integer_series_query(query) { + return Ok(result); + } + if let Some(result) = self.try_execute_simple_count_query(query, params)? { + return Ok(result); + } + if let Some(result) = self.try_execute_simple_min_max_query(query)? { + return Ok(result); + } + if let Some(result) = + self.try_execute_simple_indexed_projection_query(query, params)? + { + return Ok(result); + } + if let Some(result) = + self.try_execute_simple_distinct_filtered_projection_query(query, params)? + { + return Ok(result); + } + if let Some(result) = + self.try_execute_simple_distinct_projection_query(query, params)? + { + return Ok(result); + } + if let Some(result) = + self.try_execute_simple_filtered_projection_query(query, params)? + { + return Ok(result); + } + if let Some(result) = + self.try_execute_simple_expression_projection_query(query, params)? + { + return Ok(result); + } + if let Some(result) = + self.try_execute_simple_table_projection_query(query, params)? + { + return Ok(result); + } + if let Some(result) = + self.try_execute_left_join_status_aggregate_query(query, params)? + { + return Ok(result); + } + if let Some(result) = self.try_execute_crm_revenue_raw_aggregate_query(query)? { + return Ok(result); + } + if let Some(result) = self.try_execute_left_join_aggregate_query(query, params)? { + return Ok(result); + } + if let Some(result) = self.try_execute_simple_grouped_count_query(query, params)? { + return Ok(result); + } + if let Some(result) = + self.try_execute_simple_grouped_numeric_aggregate_query(query, params)? + { + return Ok(result); + } + if let Some(result) = + self.try_execute_three_table_genre_popularity_query(query, params)? + { + return Ok(result); + } + if let Some(result) = self.try_execute_movie_tag_search_query(query, params)? { + return Ok(result); + } + if let Some(result) = self.try_execute_movie_watchlist_query(query, params)? { + return Ok(result); + } + if let Some(result) = + self.try_execute_movie_top_rated_by_year_query(query, params)? + { + return Ok(result); + } + if let Some(result) = self.try_execute_movie_busiest_people_query(query, params)? { + return Ok(result); + } + if let Some(result) = self.try_execute_showdown_window_query(query)? { + return Ok(result); + } + if let Some(result) = + self.try_execute_showdown_directors_cte_query(query, params)? + { + return Ok(result); + } + if let Some(result) = self.try_execute_general_grouped_query(query, params)? { + return Ok(result); + } + if let Some(result) = + self.try_execute_indexed_join_grouped_count_query(query, params)? + { + return Ok(result); + } + if let Some(result) = + self.try_execute_simple_view_projection_limit_query(query, params)? + { + return Ok(result); + } + if let Some(result) = + self.try_execute_indexed_join_limit_projection_query(query, params)? + { + return Ok(result); + } + if let Some(result) = self.try_execute_benchmark_history_query(query, params)? { + return Ok(result); + } + if let Some(result) = self.try_execute_benchmark_report_query(query, params)? { + return Ok(result); + } + if let Some(result) = + self.try_execute_simple_indexed_join_projection_query(query, params)? + { + return Ok(result); + } + if let Some(result) = + self.try_execute_three_table_indexed_join_projection_query(query, params)? + { + return Ok(result); + } + if let Some(result) = self.try_execute_base_table_join(query, params)? { + return Ok(result); + } + self.evaluate_query(query, params, &BTreeMap::new()) + .map(dataset_to_result) + } + Statement::Explain(explain) => { + let mut planner_catalog = self.planner_catalog(); + if self.security_rules_active()? { + for index in planner_catalog.indexes.values_mut() { + if index.kind == IndexKind::Btree { + index.fresh = false; + } + } + } + match explain.statement.as_ref() { + Statement::Update(update) => { + if explain.analyze { + return Err(DbError::sql( + "EXPLAIN ANALYZE is not supported for UPDATE".to_string(), + )); + } + if self + .visible_view(&update.table_name, NameResolutionScope::Session) + .is_some() + { + return Err(DbError::sql(format!( + "EXPLAIN UPDATE is not supported for view {}", + update.table_name + ))); + } - let logical_len = pointer.logical_len as usize; - let mut patch_bytes = Vec::with_capacity(deleted_row_ids.len()); - for row_id in deleted_row_ids { - let Some(&len_field_offset) = locators.get(row_id) else { - return Ok(None); - }; - let len_field_offset = len_field_offset as usize; - if len_field_offset < 8 || len_field_offset + 4 > logical_len { - return Ok(None); - } + let mut lines = vec![format!("Mutation: UPDATE {}", update.table_name)]; + for (index, assignment) in update.assignments.iter().enumerate() { + lines.push(format!( + "Assignment {}: {} = {}", + index + 1, + assignment.column_name, + assignment.expr.to_sql() + )); + } + match &update.filter { + Some(filter) => lines.push(format!("Filter: {}", filter.to_sql())), + None => lines.push("Filter: ".to_string()), + } + let table = self.table_schema(&update.table_name).ok_or_else(|| { + DbError::sql(format!("unknown table {}", update.table_name)) + })?; + let candidate_rows = dml::matching_row_ids( + self, + &update.table_name, + &update.table_name, + table, + update.filter.as_ref(), + params, + )? + .len(); + lines.push(format!("Candidate rows: {candidate_rows}")); + lines.push(format!( + "Returning: {}", + if update.returning.is_empty() { + "OFF" + } else { + "ON" + } + )); - let mut row_id_bytes = [0_u8; 8]; - if !read_overflow_cached_logical_bytes( - store, - pointer, - &chain_cache.page_ids, - len_field_offset - 8, - &mut row_id_bytes, - )? { - return Ok(None); - } - let actual_row_id = i64::from_le_bytes(row_id_bytes); - if actual_row_id != *row_id { - return Ok(None); + Ok(QueryResult::with_explain(lines)) + } + _ => { + let mut lines = planner::plan_statement( + &Statement::Explain(explain.clone()), + &planner_catalog, + )? + .render(); + if explain.analyze { + lines.insert(0, "ANALYZE true".to_string()); + let started = Instant::now(); + let actual_rows = match explain.statement.as_ref() { + Statement::Query(query) => self + .evaluate_query(query, params, &BTreeMap::new())? + .rows + .len(), + other => { + return Err(DbError::sql(format!( + "EXPLAIN ANALYZE is not supported for {other:?}" + ))) + } + }; + lines.push(format!("Actual Rows: {actual_rows}")); + lines.push(format!( + "Actual Time: {:.3} ms", + started.elapsed().as_secs_f64() * 1_000.0 + )); + } + Ok(QueryResult::with_explain(lines)) + } + } + } + other => Err(DbError::internal(format!( + "read-only execution received mutating statement {other:?}" + ))), } + } - let mut len_bytes = [0_u8; 4]; - if !read_overflow_cached_logical_bytes( - store, - pointer, - &chain_cache.page_ids, - len_field_offset, - &mut len_bytes, - )? { - return Ok(None); - } - let raw_len = u32::from_le_bytes(len_bytes); - let (is_tombstone, body_len) = split_table_payload_row_len(raw_len); - let Some(row_end) = len_field_offset - .checked_add(4) - .and_then(|value| value.checked_add(body_len)) - else { - return Ok(None); + fn execute_analyze(&mut self, table_name: Option<&str>) -> Result<()> { + let target_tables = if let Some(table_name) = table_name { + if self.visible_table_is_temporary(table_name) { + return Err(DbError::sql( + "ANALYZE is not supported for temporary tables", + )); + } + if self + .visible_view(table_name, NameResolutionScope::Session) + .is_some() + && self.catalog.table(table_name).is_none() + { + return Err(DbError::sql(format!("unknown table {table_name}"))); + } + let table = self + .catalog + .table(table_name) + .ok_or_else(|| DbError::sql(format!("unknown table {table_name}")))?; + vec![table.name.clone()] + } else { + self.catalog.tables.keys().cloned().collect::>() }; - if row_end > logical_len || is_tombstone { - return Ok(None); + for table_name in target_tables { + self.refresh_table_stats(&table_name)?; } - - patch_bytes.push(( - len_field_offset, - (raw_len | TABLE_PAYLOAD_ROW_TOMBSTONE_FLAG).to_le_bytes(), - )); + Ok(()) } - let patches = patch_bytes - .iter() - .map(|(offset, bytes)| OverflowBytePatch { - offset: *offset, - bytes: bytes.as_slice(), - }) - .collect::>(); - let (pointer, new_chain_cache, tail, checksum) = - rewrite_overflow_cached_with_sparse_byte_patches( - store, - pointer, - previous_state.checksum, - &chain_cache.page_ids, - &patches, - )?; - Ok(Some(( - PersistedTableState { - pointer, - checksum, - row_count: live_row_count, - tail, - pk_index_root: previous_state.pk_index_root, - }, - new_chain_cache, - ))) -} - -fn read_overflow_cached_logical_bytes( - store: &S, - pointer: OverflowPointer, - cached_page_ids: &[PageId], - offset: usize, - out: &mut [u8], -) -> Result { - if out.is_empty() { - return Ok(true); - } - if pointer.is_compressed() || pointer.is_table_paged_manifest() { - return Ok(false); - } - let logical_len = pointer.logical_len as usize; - let Some(end) = offset.checked_add(out.len()) else { - return Ok(false); - }; - if end > logical_len { - return Ok(false); - } + fn refresh_table_stats(&mut self, table_name: &str) -> Result<()> { + let row_count = self + .table_row_source(table_name) + .map(TableRowSource::row_count) + .or_else(|| self.table_data(table_name).map(|table| table.rows.len())) + .map(i64::try_from) + .transpose() + .map_err(|_| { + DbError::sql(format!( + "table {table_name} exceeds ANALYZE row-count limits" + )) + })? + .unwrap_or(0); + self.catalog_mut() + .table_stats + .insert(table_name.to_string(), TableStats { row_count }); - let page_size = store.page_size() as usize; - if page_size <= OVERFLOW_HEADER_SIZE { - return Err(DbError::internal("page size too small for overflow pages")); - } - let chunk_capacity = page_size - OVERFLOW_HEADER_SIZE; - let mut read = 0usize; - while read < out.len() { - let logical_offset = offset + read; - let page_index = logical_offset / chunk_capacity; - let Some(&page_id) = cached_page_ids.get(page_index) else { - return Ok(false); - }; - let page_payload_offset = page_index.saturating_mul(chunk_capacity); - let local_offset = logical_offset.saturating_sub(page_payload_offset); - let page = store.read_page(page_id)?; - if page.len() < OVERFLOW_HEADER_SIZE { - return Err(DbError::corruption("overflow page shorter than header")); - } - let chunk_len = - u32::from_le_bytes(page[4..8].try_into().expect("header chunk len")) as usize; - let chunk_end = OVERFLOW_HEADER_SIZE.saturating_add(chunk_len); - if chunk_end > page.len() { - return Err(DbError::corruption( - "overflow chunk length exceeds page payload", - )); - } - if local_offset >= chunk_len { - return Ok(false); + let index_names = self + .catalog + .indexes + .values() + .filter(|index| identifiers_equal(&index.table_name, table_name)) + .map(|index| index.name.clone()) + .collect::>(); + for index_name in index_names { + self.catalog_mut().index_stats.remove(&index_name); + let Some(index) = self.catalog.index(&index_name).cloned() else { + continue; + }; + let (entry_count, distinct_key_count) = match self.index(&index.name) { + Some(RuntimeIndex::Btree { keys, .. }) => { + let entry_count = i64::try_from(keys.total_row_id_count()).map_err(|_| { + DbError::sql(format!( + "index {} exceeds ANALYZE entry-count limits", + index.name + )) + })?; + let distinct_key_count = + i64::try_from(keys.distinct_key_count()).map_err(|_| { + DbError::sql(format!( + "index {} exceeds ANALYZE distinct-count limits", + index.name + )) + })?; + (entry_count, distinct_key_count) + } + Some(RuntimeIndex::Spatial { index: spatial }) => { + let entry_count = i64::try_from(spatial.len()).map_err(|_| { + DbError::sql(format!( + "index {} exceeds ANALYZE entry-count limits", + index.name + )) + })?; + (entry_count, entry_count) + } + Some(RuntimeIndex::Trigram { .. }) | None => continue, + Some(RuntimeIndex::FullText { index: fulltext }) => { + let entry_count = i64::try_from(fulltext.entry_count()).map_err(|_| { + DbError::sql(format!( + "index {} exceeds ANALYZE entry-count limits", + index.name + )) + })?; + let distinct_key_count = + i64::try_from(fulltext.term_count()).map_err(|_| { + DbError::sql(format!( + "index {} exceeds ANALYZE distinct-count limits", + index.name + )) + })?; + (entry_count, distinct_key_count) + } + }; + self.catalog_mut().index_stats.insert( + index.name.clone(), + IndexStats { + entry_count, + distinct_key_count, + }, + ); } - let take = (out.len() - read).min(chunk_len - local_offset); - out[read..read + take].copy_from_slice( - &page[OVERFLOW_HEADER_SIZE + local_offset..OVERFLOW_HEADER_SIZE + local_offset + take], - ); - read += take; + Ok(()) } - Ok(true) -} -fn build_resident_tombstone_locators_from_payload(payload: &[u8]) -> Result> { - const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; - - if payload.is_empty() { - return Ok(Int64Map::with_hasher(Int64HashBuilder::default())); - } - if payload.len() < HEADER_LEN || payload[..8] != *TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = - u32::from_le_bytes(payload[8..12].try_into().expect("row-count header length")) as usize; - let mut locators = Int64Map::with_capacity_and_hasher(row_count, Int64HashBuilder::default()); - let mut offset = HEADER_LEN; - for _ in 0..row_count { - if offset + 12 > payload.len() { - return Err(DbError::corruption("truncated table payload row header")); - } - let row_id = i64::from_le_bytes( - payload[offset..offset + 8] - .try_into() - .expect("row id length"), - ); - let len_field_offset = offset + 8; - let raw_len = u32::from_le_bytes( - payload[len_field_offset..len_field_offset + 4] - .try_into() - .expect("row data len"), - ); - let (_, body_len) = split_table_payload_row_len(raw_len); - let row_end = len_field_offset - .checked_add(4) - .and_then(|value| value.checked_add(body_len)) - .ok_or_else(|| DbError::corruption("table payload row length overflow"))?; - if row_end > payload.len() { - return Err(DbError::corruption("truncated table payload row body")); + fn execute_validated_simple_row_id_projection_at_snapshot( + &self, + request: ValidatedSimpleRowIdProjectionRequest<'_>, + ) -> Result> { + let table_schema = request.table_schema; + let canonical_table_name = table_schema.name.as_str(); + if let Some(result) = self.try_execute_validated_resident_simple_row_id_projection( + table_schema, + request.projection_indexes, + Arc::clone(&request.column_names), + request.lookup_row_id, + )? { + return Ok(Some(result)); } - locators.insert( - row_id, - u32::try_from(len_field_offset) - .map_err(|_| DbError::constraint("resident tombstone locator exceeds u32"))?, - ); - offset = row_end; - } - Ok(locators) -} - -fn splice_deleted_rows_payload_in_place( - payload: &mut Vec, - data: &TableData, - deleted_row_ids: &BTreeSet, -) -> Result>>> { - const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; - if deleted_row_ids.is_empty() { - return Ok(Some(single_dirty_range(payload.len()..payload.len()))); - } - if payload.len() < HEADER_LEN || payload[..8] != *TABLE_PAYLOAD_MAGIC { - return Ok(None); - } - let old_row_count = - u32::from_le_bytes(payload[8..12].try_into().expect("row-count header length")) as usize; - if old_row_count != data.rows.len().saturating_add(deleted_row_ids.len()) { - return Ok(None); + if !self.has_deferred_tables() + || !self + .deferred_table_names() + .any(|candidate| identifiers_equal(candidate, canonical_table_name)) + { + return Ok(None); + } + let Some(state) = self.persisted_table_state(canonical_table_name) else { + return Ok(None); + }; + let paged_locator_cache = self + .catalog + .table(canonical_table_name) + .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) + .map(|cache| cache.as_ref()); + let store = SnapshotPageStore { + pager: request.pager, + wal: request.wal, + snapshot_lsn: request.snapshot_lsn, + }; + let rows = read_deferred_projected_values_by_id( + &store, + state, + table_schema, + request.lookup_row_id, + request.use_persistent_pk_index, + paged_locator_cache, + request.projection_indexes, + )? + .map(|values| vec![QueryRow::new(values)]) + .unwrap_or_default(); + Ok(Some(QueryResult::with_shared_columns( + Arc::clone(&request.column_names), + rows, + ))) } - let mut deleted_spans: Vec<(usize, usize)> = Vec::with_capacity(deleted_row_ids.len()); - let mut remaining = deleted_row_ids.len(); - let mut scan_offset = HEADER_LEN; - let mut scanned_rows = 0usize; - while remaining > 0 && scan_offset + 12 <= payload.len() { - if scanned_rows >= old_row_count { - break; - } - let row_id = i64::from_le_bytes( - payload[scan_offset..scan_offset + 8] - .try_into() - .expect("row id length"), - ); - let row_data_len = u32::from_le_bytes( - payload[scan_offset + 8..scan_offset + 12] - .try_into() - .expect("row data len"), - ) as usize; - let row_end = scan_offset.saturating_add(12).saturating_add(row_data_len); - if row_end > payload.len() { + #[allow(clippy::too_many_arguments)] + fn stream_deferred_view_linear_three_table_rows_from_root( + &self, + store: &S, + table_row_readers: &[DeferredViewTableRowReader<'_>], + join_steps: &[DeferredViewJoinStep], + join_keys: &[&RuntimeBtreeKeys], + key_projection_indexes: &[Option], + table_projections: &[DeferredViewTableProjection], + root_row: &StoredRow, + use_persistent_pk_index: bool, + chunk_payload_cache: &mut HashMap>>, + visit: &mut F, + ) -> Result> + where + F: FnMut(&StoredRow, &StoredRow, StoredRow) -> Result, + { + if table_row_readers.len() != 3 + || join_steps.len() != 2 + || table_projections.len() != 3 + || join_keys.len() != 2 + || key_projection_indexes.len() != 2 + { return Ok(None); } - if deleted_row_ids.contains(&row_id) { - deleted_spans.push((scan_offset, row_end)); - remaining -= 1; + let step0 = &join_steps[0]; + let step1 = &join_steps[1]; + if step0.previous_table_index != 0 + || step0.current_table_index != 1 + || step1.previous_table_index != 1 + || step1.current_table_index != 2 + { + return Ok(None); } - scan_offset = row_end; - scanned_rows += 1; - } + let [keys0, keys1] = join_keys else { + return Ok(None); + }; + let [key0_projection_index, key1_projection_index] = key_projection_indexes else { + return Ok(None); + }; + let key0_row_ids = match *key0_projection_index { + Some(key0_projection_index) => { + let Some(key0_value) = root_row.values.get(key0_projection_index) else { + return Err(DbError::internal( + "deferred view linear join projection row is shorter than planned schema", + )); + }; + if matches!(key0_value, Value::Null) { + return Ok(Some(false)); + } + keys0.row_ids_for_value_set(key0_value)? + } + None => keys0.row_ids_for_row_id(root_row.row_id), + }; - if remaining > 0 || deleted_spans.len() != deleted_row_ids.len() { - return Ok(None); + let stopped = key0_row_ids + .visit_until(|row1_id| { + let Some(row1) = table_row_readers[1].read_projected_with_chunk_cache( + store, + row1_id, + use_persistent_pk_index, + &table_projections[1].projection_indexes, + chunk_payload_cache, + )? + else { + return Ok(false); + }; + let key1_row_ids = match *key1_projection_index { + Some(key1_projection_index) => { + let Some(key1_value) = row1.values.get(key1_projection_index) else { + return Err(DbError::internal( + "deferred view linear join projection row is shorter than planned schema", + )); + }; + if matches!(key1_value, Value::Null) { + return Ok(false); + } + keys1.row_ids_for_value_set(key1_value)? + } + None => keys1.row_ids_for_row_id(row1.row_id), + }; + key1_row_ids + .visit_until(|row2_id| { + let Some(row2) = table_row_readers[2].read_projected_with_chunk_cache( + store, + row2_id, + use_persistent_pk_index, + &table_projections[2].projection_indexes, + chunk_payload_cache, + )? + else { + return Ok(false); + }; + visit(root_row, &row1, row2) + }) + })?; + Ok(Some(stopped)) } - let original_len = payload.len(); - let first_deleted_byte = deleted_spans.first().map_or(HEADER_LEN, |span| span.0); - payload[8..12].copy_from_slice( - &u32::try_from(data.rows.len()) - .map_err(|_| DbError::constraint("table row count exceeds u32"))? - .to_le_bytes(), - ); + pub(crate) fn evaluate_query( + &self, + query: &Query, + params: &[Value], + inherited_ctes: &BTreeMap, + ) -> Result { + let mut ctes = inherited_ctes.clone(); + let recursive_ctes = validate_recursive_ctes(query)?; + for cte in &query.ctes { + let dataset = if recursive_ctes.contains(&cte.name) { + self.evaluate_recursive_cte(cte, params, &ctes)? + } else { + prepare_cte_dataset(cte, self.evaluate_query(&cte.query, params, &ctes)?)? + }; + ctes.insert(cte.name.clone(), dataset); + } + + if let Some(dataset) = + self.try_execute_simple_union_range_projection_query(query, params, &ctes)? + { + return Ok(dataset); + } - let mut copy_from = HEADER_LEN; - let mut write_at = HEADER_LEN; - for (span_start, span_end) in deleted_spans { - if copy_from < span_start { - if copy_from != write_at { - payload.copy_within(copy_from..span_start, write_at); + let mut sorted_during_select = false; + let mut dataset = match &query.body { + QueryBody::Select(select) => { + if let Some(dataset) = self.try_fulltext_bm25_top_k_select( + select, + &query.order_by, + query.limit.as_ref(), + query.offset.as_ref(), + params, + &ctes, + )? { + return Ok(dataset); + } + if select_requires_grouped_evaluation(self, select)? { + self.evaluate_select(select, params, &ctes)? + } else { + let projection_order_by = + projection_order_by_plan(&query.order_by, &select.projection); + let mut source = self.build_select_dataset(select, params, &ctes)?; + if !query.order_by.is_empty() && projection_order_by.is_none() { + self.sort_dataset(&mut source, &query.order_by, params, &ctes)?; + sorted_during_select = true; + } + let mut projected = + self.project_dataset(&source, &select.projection, params, &ctes, None)?; + if let Some(order_by_plan) = projection_order_by.as_deref() { + sort_dataset_by_projection_order( + Some(self), + &mut projected, + order_by_plan, + )?; + sorted_during_select = true; + } + projected + } + } + _ => self.evaluate_query_body(&query.body, params, &ctes)?, + }; + if let QueryBody::Select(select) = &query.body { + if select.distinct { + if !query.order_by.is_empty() && !sorted_during_select { + self.sort_dataset(&mut dataset, &query.order_by, params, &ctes)?; + sorted_during_select = true; + } + dataset = self.apply_select_distinct(select, dataset, params, &ctes)?; } - write_at += span_start - copy_from; } - copy_from = span_end; - } - if copy_from < original_len { - let tail_len = original_len - copy_from; - if copy_from != write_at { - payload.copy_within(copy_from..original_len, write_at); + if !query.order_by.is_empty() && !sorted_during_select { + self.sort_dataset(&mut dataset, &query.order_by, params, &ctes)?; } - write_at += tail_len; - } - payload.truncate(write_at); - - let mut ranges = single_dirty_range(8..HEADER_LEN); - if !payload.is_empty() { - let tail_dirty_start = first_deleted_byte.saturating_sub(1).min(payload.len()); - if tail_dirty_start < payload.len() { - ranges.push(tail_dirty_start..payload.len()); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &ctes)) + .transpose()? + .unwrap_or(0); + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &ctes)) + .transpose()?; + if offset > 0 || limit.is_some() { + let start = usize::try_from(offset.max(0)).unwrap_or(usize::MAX); + let rows = if start >= dataset.rows.len() { + Vec::new() + } else { + let iter = dataset.take_rows().into_iter().skip(start); + match limit { + Some(limit) => iter + .take(usize::try_from(limit.max(0)).unwrap_or(0)) + .collect(), + None => iter.collect(), + } + }; + dataset.set_rows(rows); } - } - Ok(Some(ranges)) -} - -fn splice_updated_rows_payload( - old: &[u8], - data: &TableData, - dirty_indices: &[usize], -) -> Result { - const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; - - if old.len() < HEADER_LEN || old[..8] != *TABLE_PAYLOAD_MAGIC { - let payload = encode_table_payload(data)?; - let payload_len = payload.len(); - return Ok(SpliceResult { - payload, - first_dirty_byte: 0, - last_dirty_byte: payload_len, - pk_locator_preserved: false, - }); - } - let old_row_count = - u32::from_le_bytes(old[8..12].try_into().expect("row-count header length")) as usize; - if old_row_count != data.rows.len() { - // Row count changed (e.g. concurrent insert/delete after the cache - // was stored) — fall back to full encode for safety. - let payload = encode_table_payload(data)?; - let payload_len = payload.len(); - return Ok(SpliceResult { - payload, - first_dirty_byte: 0, - last_dirty_byte: payload_len, - pk_locator_preserved: false, - }); + Ok(dataset) } - // Fast path: only a handful of rows changed. Scan the old payload to - // locate byte ranges of each dirty row, then splice new encodings in. - // - // Row wire format: - // row_id (8 bytes, i64 LE) - // row_data_len (4 bytes, u32 LE) - // row_data (row_data_len bytes) - // - // We need the byte offset of each dirty row (and the one after it) so - // we can copy unchanged prefix / suffix regions. - - // Sort dirty indices so we splice left-to-right. - let mut sorted_dirty: Vec = dirty_indices.to_vec(); - sorted_dirty.sort_unstable(); - sorted_dirty.dedup(); - - // Scan the old payload to locate dirty row byte ranges. - // row_spans[i] = (start, end) byte offsets in `old` for dirty row i. - let mut row_spans: Vec<(usize, usize)> = Vec::with_capacity(sorted_dirty.len()); - let mut scan_offset = HEADER_LEN; - let mut dirty_cursor = 0; - let mut row_idx = 0; - while dirty_cursor < sorted_dirty.len() && scan_offset + 12 <= old.len() { - if row_idx >= old_row_count { - break; - } - let rd_len = u32::from_le_bytes( - old[scan_offset + 8..scan_offset + 12] - .try_into() - .expect("row data len"), - ) as usize; - let row_end = scan_offset + 12 + rd_len; - if row_end > old.len() { - let payload = encode_table_payload(data)?; - let payload_len = payload.len(); - return Ok(SpliceResult { - payload, - first_dirty_byte: 0, - last_dirty_byte: payload_len, - pk_locator_preserved: false, - }); + fn evaluate_recursive_cte( + &self, + cte: &CommonTableExpr, + params: &[Value], + inherited_ctes: &BTreeMap, + ) -> Result { + if let Some(dataset) = Self::try_evaluate_simple_integer_series_cte(cte) { + return Ok(dataset); } - if row_idx == sorted_dirty[dirty_cursor] { - row_spans.push((scan_offset, row_end)); - dirty_cursor += 1; - if dirty_cursor == sorted_dirty.len() { - break; - } + if !cte.query.order_by.is_empty() || cte.query.limit.is_some() || cte.query.offset.is_some() + { + return Err(DbError::sql(format!( + "recursive CTE {} does not support ORDER BY, LIMIT, or OFFSET at the CTE level", + cte.name + ))); } - scan_offset = row_end; - row_idx += 1; - } - if row_spans.len() != sorted_dirty.len() { - // Could not find all dirty rows in the old payload; fall back. - let payload = encode_table_payload(data)?; - let payload_len = payload.len(); - return Ok(SpliceResult { - payload, - first_dirty_byte: 0, - last_dirty_byte: payload_len, - pk_locator_preserved: false, - }); - } + let QueryBody::SetOperation { + op: crate::sql::ast::SetOperation::Union, + all, + left, + right, + } = &cte.query.body + else { + return Err(DbError::sql(format!( + "recursive CTE {} must use UNION or UNION ALL between anchor and recursive terms", + cte.name + ))); + }; - if row_spans.is_empty() { - return Ok(SpliceResult { - payload: old.to_vec(), - first_dirty_byte: 0, - last_dirty_byte: old.len(), - pk_locator_preserved: false, - }); - } + let anchor_references = query_body_table_reference_count(left, &cte.name); + if anchor_references != 0 { + return Err(DbError::sql(format!( + "recursive CTE {} anchor term must not reference itself", + cte.name + ))); + } - let mut can_preserve_body = true; - let mut encoded_rows: Vec> = Vec::with_capacity(sorted_dirty.len()); - let mut encoded_row = Vec::with_capacity(128); - for (span_idx, &dirty_row) in sorted_dirty.iter().enumerate() { - if dirty_row >= data.rows.len() { - let payload = encode_table_payload(data)?; - let payload_len = payload.len(); - return Ok(SpliceResult { - payload, - first_dirty_byte: 0, - last_dirty_byte: payload_len, - pk_locator_preserved: false, - }); + let recursive_references = query_body_table_reference_count(right, &cte.name); + if recursive_references != 1 { + return Err(DbError::sql(format!( + "recursive CTE {} recursive term must reference itself exactly once", + cte.name + ))); } - let (span_start, span_end) = row_spans[span_idx]; - let old_row_body_len = span_end.saturating_sub(span_start).saturating_sub(12); + validate_recursive_term(right, &cte.name)?; - let row = &data.rows[dirty_row]; - encoded_row.clear(); - Row::encode_values_into(&row.values, &mut encoded_row)?; - if encoded_row.len() > old_row_body_len { - can_preserve_body = false; + let mut anchor = self.evaluate_query_body(left, params, inherited_ctes)?; + if !all { + let rows = anchor.take_rows(); + anchor.set_rows(deduplicate_rows(rows)?); } - encoded_rows.push(encoded_row.clone()); - } + let mut result = prepare_cte_dataset(cte, anchor)?; + let mut working = result.clone(); + let mut seen = if *all { + None + } else { + Some( + result + .rows + .iter() + .map(|row| row_identity(row)) + .collect::>>()?, + ) + }; - let mut output = Vec::with_capacity(if can_preserve_body { - old.len() - } else { - old.len().saturating_add(sorted_dirty.len() * 32) - }); - output.extend_from_slice(&old[..8]); // magic - encode_u32(&mut output, data.rows.len() as u32); + for _ in 0..RECURSIVE_CTE_MAX_ITERATIONS { + if working.rows.is_empty() { + return Ok(result); + } - let first_dirty_byte = if can_preserve_body { - row_spans.first().map_or(0, |s| s.0.saturating_add(12)) - } else { - row_spans.first().map_or(0, |s| s.0) - }; - let last_dirty_byte = if can_preserve_body { - row_spans.last().map_or(first_dirty_byte, |s| s.1) - } else { - old.len() - }; + let mut recursive_ctes = inherited_ctes.clone(); + recursive_ctes.insert(cte.name.clone(), working.clone()); - let mut copy_from = HEADER_LEN; - for (span_idx, &dirty_row) in sorted_dirty.iter().enumerate() { - let (span_start, span_end) = row_spans[span_idx]; - let old_row_body_len = span_end.saturating_sub(span_start).saturating_sub(12); - let encoded_row = &encoded_rows[span_idx]; - - // Copy unchanged bytes before this dirty row. - if copy_from < span_start { - output.extend_from_slice(&old[copy_from..span_start]); - } - - // Encode the updated row. - let row = &data.rows[dirty_row]; - encode_i64(&mut output, row.row_id); - if can_preserve_body { - let row_body_len = u32::try_from(old_row_body_len) - .map_err(|_| DbError::constraint("table row body length exceeds u32"))?; - output.extend_from_slice(&row_body_len.to_le_bytes()); - output.extend_from_slice(encoded_row); - output.extend(std::iter::repeat_n( - 0u8, - old_row_body_len.saturating_sub(encoded_row.len()), - )); - } else { - encode_u32( - &mut output, - u32::try_from(encoded_row.len()) - .map_err(|_| DbError::constraint("table row body length exceeds u32"))?, - ); - output.extend_from_slice(encoded_row); - } + let recursive_rows = prepare_cte_dataset( + cte, + self.evaluate_query_body(right, params, &recursive_ctes)?, + )?; + if recursive_rows.columns.len() != result.columns.len() { + return Err(DbError::sql(format!( + "recursive CTE {} produced {} columns in its recursive term but {} in its anchor term", + cte.name, + recursive_rows.columns.len(), + result.columns.len() + ))); + } - copy_from = span_end; - } - // Copy any remaining unchanged tail. - if copy_from < old.len() { - output.extend_from_slice(&old[copy_from..]); - } + let next_rows = if let Some(seen) = &mut seen { + let mut rows = Vec::new(); + for row in recursive_rows.into_rows() { + let identity = row_identity(&row)?; + if seen.insert(identity) { + rows.push(row); + } + } + rows + } else { + recursive_rows.into_rows() + }; - Ok(SpliceResult { - payload: output, - first_dirty_byte, - last_dirty_byte, - pk_locator_preserved: can_preserve_body, - }) -} + if next_rows.is_empty() { + return Ok(result); + } -fn splice_deleted_rows_payload( - old: &[u8], - data: &TableData, - deleted_row_ids: &BTreeSet, -) -> Result { - const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; + result.rows_mut().extend(next_rows.clone()); + working.set_rows(next_rows); + } - if deleted_row_ids.is_empty() { - return Ok(SpliceResult { - payload: old.to_vec(), - first_dirty_byte: old.len(), - last_dirty_byte: old.len(), - pk_locator_preserved: false, - }); - } - if old.len() < HEADER_LEN || old[..8] != *TABLE_PAYLOAD_MAGIC { - let payload = encode_table_payload(data)?; - let payload_len = payload.len(); - return Ok(SpliceResult { - payload, - first_dirty_byte: 0, - last_dirty_byte: payload_len, - pk_locator_preserved: false, - }); - } - let old_row_count = - u32::from_le_bytes(old[8..12].try_into().expect("row-count header length")) as usize; - if old_row_count != data.rows.len().saturating_add(deleted_row_ids.len()) { - let payload = encode_table_payload(data)?; - let payload_len = payload.len(); - return Ok(SpliceResult { - payload, - first_dirty_byte: 0, - last_dirty_byte: payload_len, - pk_locator_preserved: false, - }); + Err(DbError::sql(format!( + "recursive CTE {} exceeded the {} iteration limit", + cte.name, RECURSIVE_CTE_MAX_ITERATIONS + ))) } - let mut deleted_spans: Vec<(usize, usize)> = Vec::with_capacity(deleted_row_ids.len()); - let mut remaining = deleted_row_ids.len(); - let mut scan_offset = HEADER_LEN; - while remaining > 0 && scan_offset + 12 <= old.len() { - let row_id = i64::from_le_bytes( - old[scan_offset..scan_offset + 8] - .try_into() - .expect("row id length"), - ); - let row_data_len = u32::from_le_bytes( - old[scan_offset + 8..scan_offset + 12] - .try_into() - .expect("row data len"), - ) as usize; - let row_end = scan_offset.saturating_add(12).saturating_add(row_data_len); - if row_end > old.len() { - let payload = encode_table_payload(data)?; - let payload_len = payload.len(); - return Ok(SpliceResult { - payload, - first_dirty_byte: 0, - last_dirty_byte: payload_len, - pk_locator_preserved: false, - }); - } - if deleted_row_ids.contains(&row_id) { - deleted_spans.push((scan_offset, row_end)); - remaining -= 1; + fn try_evaluate_simple_integer_series_cte(cte: &CommonTableExpr) -> Option { + let (column_name, start, step, upper_exclusive) = Self::simple_integer_series_bounds(cte)?; + let mut rows = Vec::new(); + let mut value = start; + rows.push(vec![Value::Int64(value)]); + while value < upper_exclusive { + if rows.len() >= RECURSIVE_CTE_MAX_ITERATIONS { + return None; + } + value = value.checked_add(step)?; + rows.push(vec![Value::Int64(value)]); } - scan_offset = row_end; - } - if remaining > 0 || deleted_spans.len() != deleted_row_ids.len() { - let payload = encode_table_payload(data)?; - let payload_len = payload.len(); - return Ok(SpliceResult { - payload, - first_dirty_byte: 0, - last_dirty_byte: payload_len, - pk_locator_preserved: false, - }); + Some(Dataset::with_rows( + vec![ColumnBinding::visible(Some(cte.name.clone()), column_name)], + rows, + )) } - let first_dirty_byte = deleted_spans.first().map_or(0, |span| span.0); - let mut output = Vec::with_capacity(old.len()); - output.extend_from_slice(&old[..TABLE_PAYLOAD_MAGIC.len()]); - encode_u32(&mut output, data.rows.len() as u32); - - let mut copy_from = HEADER_LEN; - for (span_start, span_end) in deleted_spans { - if copy_from < span_start { - output.extend_from_slice(&old[copy_from..span_start]); + fn evaluate_query_with_outer( + &self, + query: &Query, + params: &[Value], + inherited_ctes: &BTreeMap, + outer_dataset: &Dataset, + outer_row: &[Value], + ) -> Result { + if !query_references_outer_scope(query, outer_dataset) { + return self.evaluate_query(query, params, inherited_ctes); + } + if query.recursive { + return Err(DbError::sql( + "WITH RECURSIVE is not supported in correlated subqueries yet", + )); } - copy_from = span_end; - } - if copy_from < old.len() { - output.extend_from_slice(&old[copy_from..]); - } - - let last_dirty_byte = output.len(); - Ok(SpliceResult { - payload: output, - first_dirty_byte, - last_dirty_byte, - pk_locator_preserved: false, - }) -} - -fn encode_appended_table_rows(data: &TableData, existing_count: usize) -> Result> { - if existing_count > data.rows.len() { - return Err(DbError::internal( - "append-only table payload rewrite saw fewer rows than the previous persisted payload", - )); - } - if existing_count == data.rows.len() { - return Ok(Vec::new()); - } - let mut appended = Vec::with_capacity((data.rows.len() - existing_count) * 32); - let mut encoded_row = Vec::with_capacity(64); - for row in data.rows.iter().skip(existing_count) { - encode_i64(&mut appended, row.row_id); - Row::encode_values_into(&row.values, &mut encoded_row)?; - let row_body_len = encoded_row - .len() - .saturating_add(TABLE_PAYLOAD_ROW_BODY_PADDING_BYTES); - encode_u32( - &mut appended, - u32::try_from(row_body_len) - .map_err(|_| DbError::constraint("table row body length exceeds u32"))?, - ); - appended.extend_from_slice(&encoded_row); - appended.extend(std::iter::repeat_n( - 0u8, - row_body_len.saturating_sub(encoded_row.len()), - )); - encoded_row.clear(); - } - Ok(appended) -} + let mut ctes = inherited_ctes.clone(); + for cte in &query.ctes { + let mut dataset = self.evaluate_query_with_outer( + &cte.query, + params, + &ctes, + outer_dataset, + outer_row, + )?; + if !cte.column_names.is_empty() { + if cte.column_names.len() != dataset.columns.len() { + return Err(DbError::sql(format!( + "CTE {} expected {} columns but produced {}", + cte.name, + cte.column_names.len(), + dataset.columns.len() + ))); + } + for (binding, name) in dataset.columns.iter_mut().zip(&cte.column_names) { + binding.name = name.clone(); + binding.table = Some(cte.name.clone()); + } + } + ctes.insert(cte.name.clone(), dataset); + } -fn append_encoded_rows_to_table_payload( - mut previous: Vec, - row_count: usize, - appended_rows: &[u8], -) -> Result> { - if appended_rows.is_empty() { - return Ok(previous); - } - let count_offset = TABLE_PAYLOAD_MAGIC.len(); - if previous.len() < count_offset + 4 { - return Err(DbError::corruption("table payload header is truncated")); - } - if previous[..count_offset] != *TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); + let mut sorted_during_select = false; + let mut dataset = match &query.body { + QueryBody::Select(select) => { + if let Some(dataset) = self.try_fulltext_bm25_top_k_select( + select, + &query.order_by, + query.limit.as_ref(), + query.offset.as_ref(), + params, + &ctes, + )? { + return Ok(dataset); + } + if select_requires_grouped_evaluation(self, select)? { + self.evaluate_select_with_outer( + select, + params, + &ctes, + outer_dataset, + outer_row, + )? + } else { + let projection_order_by = + projection_order_by_plan(&query.order_by, &select.projection); + let mut source = self.build_select_dataset_with_outer( + select, + params, + &ctes, + outer_dataset, + outer_row, + )?; + if !query.order_by.is_empty() && projection_order_by.is_none() { + self.sort_dataset(&mut source, &query.order_by, params, &ctes)?; + sorted_during_select = true; + } + let mut projected = + self.project_dataset(&source, &select.projection, params, &ctes, None)?; + if let Some(order_by_plan) = projection_order_by.as_deref() { + sort_dataset_by_projection_order( + Some(self), + &mut projected, + order_by_plan, + )?; + sorted_during_select = true; + } + projected + } + } + _ => self.evaluate_query_body_with_outer( + &query.body, + params, + &ctes, + outer_dataset, + outer_row, + )?, + }; + if let QueryBody::Select(select) = &query.body { + if select.distinct { + if !query.order_by.is_empty() && !sorted_during_select { + self.sort_dataset(&mut dataset, &query.order_by, params, &ctes)?; + sorted_during_select = true; + } + dataset = self.apply_select_distinct(select, dataset, params, &ctes)?; + } + } + if !query.order_by.is_empty() && !sorted_during_select { + self.sort_dataset(&mut dataset, &query.order_by, params, &ctes)?; + } + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &ctes)) + .transpose()? + .unwrap_or(0); + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &ctes)) + .transpose()?; + if offset > 0 || limit.is_some() { + let start = usize::try_from(offset.max(0)).unwrap_or(usize::MAX); + let rows = if start >= dataset.rows.len() { + Vec::new() + } else { + let iter = dataset.take_rows().into_iter().skip(start); + match limit { + Some(limit) => iter + .take(usize::try_from(limit.max(0)).unwrap_or(0)) + .collect(), + None => iter.collect(), + } + }; + dataset.set_rows(rows); + } + Ok(dataset) } - previous[count_offset..count_offset + 4].copy_from_slice( - &u32::try_from(row_count) - .map_err(|_| DbError::constraint("table row count exceeds u32"))? - .to_le_bytes(), - ); - previous.extend_from_slice(appended_rows); - Ok(previous) -} - -fn append_table_payload(mut previous: Vec, data: &TableData) -> Result> { - if data.rows.is_empty() { - return Ok(Vec::new()); - } - if data.has_tombstoned_rows() { - return encode_table_payload(data); - } - if previous.is_empty() { - return encode_table_payload(data); - } - let count_offset = TABLE_PAYLOAD_MAGIC.len(); - if previous.len() < count_offset + 4 { - return Err(DbError::corruption("table payload header is truncated")); - } - if previous[..count_offset] != *TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); + fn evaluate_query_body( + &self, + body: &QueryBody, + params: &[Value], + ctes: &BTreeMap, + ) -> Result { + match body { + QueryBody::Select(select) => self.evaluate_select(select, params, ctes), + QueryBody::Values(rows) => self.evaluate_values_body(rows, params, ctes), + QueryBody::SetOperation { + op, + all, + left, + right, + } => { + let left = self.evaluate_query_body(left, params, ctes)?; + let right = self.evaluate_query_body(right, params, ctes)?; + self.evaluate_set_operation(*op, *all, left, right) + } + } } - let existing_count = u32::from_le_bytes( - previous[count_offset..count_offset + 4] - .try_into() - .expect("row-count header length"), - ) as usize; - let appended_rows = encode_appended_table_rows(data, existing_count)?; - if appended_rows.is_empty() { - return Ok(previous); + fn evaluate_query_body_with_outer( + &self, + body: &QueryBody, + params: &[Value], + ctes: &BTreeMap, + outer_dataset: &Dataset, + outer_row: &[Value], + ) -> Result { + match body { + QueryBody::Select(select) => { + self.evaluate_select_with_outer(select, params, ctes, outer_dataset, outer_row) + } + QueryBody::Values(rows) => { + self.evaluate_values_body_with_outer(rows, params, ctes, outer_dataset, outer_row) + } + QueryBody::SetOperation { + op, + all, + left, + right, + } => { + let left = self.evaluate_query_body_with_outer( + left, + params, + ctes, + outer_dataset, + outer_row, + )?; + let right = self.evaluate_query_body_with_outer( + right, + params, + ctes, + outer_dataset, + outer_row, + )?; + self.evaluate_set_operation(*op, *all, left, right) + } + } } - previous[count_offset..count_offset + 4].copy_from_slice( - &u32::try_from(data.row_count()) - .map_err(|_| DbError::constraint("table row count exceeds u32"))? - .to_le_bytes(), - ); - previous.extend_from_slice(&appended_rows); - Ok(previous) -} - -#[cfg(test)] -fn decode_table_payload(bytes: &[u8]) -> Result { - if bytes.is_empty() { - return Ok(TableData::default()); - } - let mut cursor = Cursor::new(bytes); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - let mut data = TableData::default(); - data.reserve_rows(row_count); - let mut slots = 0usize; - while cursor.offset < cursor.bytes.len() { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes = cursor.read_slice(row_bytes_len)?; - slots += 1; - if is_tombstone { - continue; + fn evaluate_select( + &self, + select: &Select, + params: &[Value], + ctes: &BTreeMap, + ) -> Result { + let dataset = self.build_select_dataset(select, params, ctes)?; + if select_requires_grouped_evaluation(self, select)? { + self.evaluate_grouped_select(select, dataset, params, ctes) + } else { + self.project_dataset(&dataset, &select.projection, params, ctes, None) } - let row = Row::decode(row_bytes)?; - data.push_row(StoredRow { - row_id, - values: row.into_values(), - }); } - if slots < row_count { - return Err(DbError::corruption( - "table payload row count exceeded decoded row content", - )); + + fn evaluate_select_with_outer( + &self, + select: &Select, + params: &[Value], + ctes: &BTreeMap, + outer_dataset: &Dataset, + outer_row: &[Value], + ) -> Result { + let dataset = + self.build_select_dataset_with_outer(select, params, ctes, outer_dataset, outer_row)?; + if select_requires_grouped_evaluation(self, select)? { + self.evaluate_grouped_select(select, dataset, params, ctes) + } else { + self.project_dataset(&dataset, &select.projection, params, ctes, None) + } } - Ok(data) -} -fn encode_u32(output: &mut Vec, value: u32) { - output.extend_from_slice(&value.to_le_bytes()); -} + fn build_select_dataset( + &self, + select: &Select, + params: &[Value], + ctes: &BTreeMap, + ) -> Result { + let has_lateral = select.from.iter().any(from_item_contains_lateral); + let mut dataset = if !has_lateral { + if let Some(dataset) = self.try_view_filter_pushdown(select, params, ctes)? { + dataset + } else if let Some(dataset) = self.try_indexed_scan(select, params, ctes)? { + dataset + } else if let Some(dataset) = self.try_spatial_join(select, params, ctes)? { + dataset + } else if let Some(dataset) = self.try_indexed_join(select, params, ctes)? { + dataset + } else if let Some(dataset) = + self.try_indexed_prefiltered_inner_join_tree(select, params, ctes)? + { + dataset + } else { + self.evaluate_from_clause(&select.from, params, ctes, &Dataset::empty(), &[])? + } + } else { + self.evaluate_from_clause(&select.from, params, ctes, &Dataset::empty(), &[])? + }; -fn encode_u64(output: &mut Vec, value: u64) { - output.extend_from_slice(&value.to_le_bytes()); -} + if let Some(filter) = &select.filter { + let filter_dataset = Dataset::with_rows(dataset.columns.clone(), Vec::new()); + let mut filtered = Vec::with_capacity(dataset.rows.len()); + for row in dataset.take_rows() { + if matches!( + self.eval_expr(filter, &filter_dataset, &row, params, ctes, None)?, + Value::Bool(true) + ) { + filtered.push(row); + } + } + dataset.set_rows(filtered); + } -fn encode_i64(output: &mut Vec, value: i64) { - output.extend_from_slice(&value.to_le_bytes()); -} + Ok(dataset) + } -fn encode_string(output: &mut Vec, value: &str) -> Result<()> { - encode_u32( - output, - u32::try_from(value.len()).map_err(|_| DbError::constraint("string length exceeds u32"))?, - ); - output.extend_from_slice(value.as_bytes()); - Ok(()) -} + fn build_select_dataset_with_outer( + &self, + select: &Select, + params: &[Value], + ctes: &BTreeMap, + outer_dataset: &Dataset, + outer_row: &[Value], + ) -> Result { + let mut dataset = + self.evaluate_from_clause(&select.from, params, ctes, outer_dataset, outer_row)?; -fn encode_optional_string(output: &mut Vec, value: Option<&str>) -> Result<()> { - output.push(u8::from(value.is_some())); - if let Some(value) = value { - encode_string(output, value)?; + dataset = augment_dataset_with_outer_scope(dataset, outer_dataset, outer_row); + if let Some(filter) = &select.filter { + let filter_dataset = Dataset::with_rows(dataset.columns.clone(), Vec::new()); + let mut filtered = Vec::with_capacity(dataset.rows.len()); + for row in dataset.take_rows() { + if matches!( + self.eval_expr(filter, &filter_dataset, &row, params, ctes, None)?, + Value::Bool(true) + ) { + filtered.push(row); + } + } + dataset.set_rows(filtered); + } + Ok(dataset) } - Ok(()) -} -fn encode_strings(output: &mut Vec, values: &[String]) -> Result<()> { - encode_u32( - output, - u32::try_from(values.len()) - .map_err(|_| DbError::constraint("string list length exceeds u32"))?, - ); - for value in values { - encode_string(output, value)?; + fn evaluate_from_clause( + &self, + from: &[FromItem], + params: &[Value], + ctes: &BTreeMap, + scope_dataset: &Dataset, + scope_row: &[Value], + ) -> Result { + if from.is_empty() { + return Ok(Dataset::with_rows(Vec::new(), vec![Vec::new()])); + } + let mut iter = from.iter(); + let cross_constraint = JoinConstraint::On(Expr::Literal(Value::Bool(true))); + // Invariant: iterator is non-empty due to the guard above. + let mut current = self.evaluate_from_item_in_scope( + iter.next().expect("first FROM item"), + params, + ctes, + scope_dataset, + scope_row, + )?; + for item in iter { + current = if from_item_is_lateral(item) { + self.evaluate_join_with_lateral_right( + current, + item, + JoinKind::Inner, + &cross_constraint, + params, + ctes, + scope_dataset, + scope_row, + )? + } else { + let right = + self.evaluate_from_item_in_scope(item, params, ctes, scope_dataset, scope_row)?; + nested_loop_join( + current, + right, + JoinKind::Inner, + &cross_constraint, + self, + params, + ctes, + )? + }; + } + Ok(current) } - Ok(()) -} - -fn encode_bytes(output: &mut Vec, bytes: &[u8]) -> Result<()> { - encode_u32( - output, - u32::try_from(bytes.len()) - .map_err(|_| DbError::constraint("byte vector length exceeds u32"))?, - ); - output.extend_from_slice(bytes); - Ok(()) } -fn encode_foreign_key( - output: &mut Vec, - foreign_key: &crate::catalog::ForeignKeyConstraint, -) -> Result<()> { - encode_optional_string(output, foreign_key.name.as_deref())?; - encode_strings(output, &foreign_key.columns)?; - encode_string(output, &foreign_key.referenced_table)?; - encode_strings(output, &foreign_key.referenced_columns)?; - output.push(foreign_key.on_delete as u8); - output.push(foreign_key.on_update as u8); - Ok(()) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct RootHeader { + schema_cookie: u32, + payload_checksum: u32, + pointer: OverflowPointer, } -fn encode_generated_columns_section( - output: &mut Vec, - tables: &BTreeMap, -) -> Result<()> { - let generated_columns = tables - .values() - .flat_map(|table| { - table.columns.iter().filter_map(move |column| { - column.generated_sql.as_ref().map(|generated_sql| { - ( - table.name.as_str(), - column.name.as_str(), - generated_sql.as_str(), - column.generated_stored, - ) - }) - }) - }) - .collect::>(); - output.extend_from_slice(GENERATED_COLUMNS_SECTION_MAGIC); - output.push(1); - encode_u32( - output, - u32::try_from(generated_columns.len()) - .map_err(|_| DbError::constraint("generated column count exceeds u32"))?, - ); - for (table_name, column_name, generated_sql, generated_stored) in generated_columns { - encode_string(output, table_name)?; - encode_string(output, column_name)?; - encode_string(output, generated_sql)?; - output.push(u8::from(generated_stored)); - } - Ok(()) +struct SimpleIndexedProjectionPlan<'a> { + table_name: &'a str, + table_schema: &'a TableSchema, + filter_column: &'a str, + lookup_value: Value, + extra_lookup_terms: Vec<(&'a str, Value)>, + projection_indexes: Vec, + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, } -fn encode_spatial_columns_section( - output: &mut Vec, - tables: &BTreeMap, -) -> Result<()> { - let spatial_columns = tables - .values() - .flat_map(|table| { - table.columns.iter().filter_map(move |column| { - column - .spatial_type - .map(|spatial_type| (table.name.as_str(), column.name.as_str(), spatial_type)) - }) - }) - .collect::>(); - output.extend_from_slice(SPATIAL_COLUMNS_SECTION_MAGIC); - output.push(1); - encode_u32( - output, - u32::try_from(spatial_columns.len()) - .map_err(|_| DbError::constraint("spatial column count exceeds u32"))?, - ); - for (table_name, column_name, spatial_type) in spatial_columns { - encode_string(output, table_name)?; - encode_string(output, column_name)?; - output.push(encode_spatial_subtype_tag(spatial_type.subtype)); - output.push(encode_spatial_dimensions_tag(spatial_type.dimensions)); - let srid = u32::try_from(spatial_type.srid) - .map_err(|_| DbError::constraint("spatial SRID must be non-negative"))?; - encode_u32(output, srid); - } - Ok(()) +struct SimpleUnionRangeProjectionSide { + table_name: String, + alias: Option, + projection_indexes: Vec, + column_names: Vec, + filter_column_index: usize, + lower_bound: Option, + upper_bound: Option, } -fn encode_enum_columns_section( - output: &mut Vec, - tables: &BTreeMap, -) -> Result<()> { - let enum_columns = tables - .values() - .flat_map(|table| { - table.columns.iter().filter_map(move |column| { - column - .enum_type - .as_ref() - .map(|enum_type| (table.name.as_str(), column.name.as_str(), enum_type)) - }) - }) - .collect::>(); - output.extend_from_slice(ENUM_COLUMNS_SECTION_MAGIC); - output.push(1); - encode_u32( - output, - u32::try_from(enum_columns.len()) - .map_err(|_| DbError::constraint("enum column count exceeds u32"))?, - ); - for (table_name, column_name, enum_type) in enum_columns { - encode_string(output, table_name)?; - encode_string(output, column_name)?; - encode_u64(output, enum_type.type_id); - encode_u32( - output, - u32::try_from(enum_type.labels.len()) - .map_err(|_| DbError::constraint("enum label count exceeds u32"))?, - ); - for label in &enum_type.labels { - encode_u64(output, label.id); - encode_string(output, &label.label)?; - } - } - Ok(()) +struct LeftJoinStatusAggregatePlan<'a> { + parent_table_name: &'a str, + parent_join_index: usize, + child_table_name: &'a str, + child_join_index: usize, + child_status_index: usize, + child_id_index: usize, + child_index_name: Option, + group_column_indexes: Vec, + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, } -fn encode_index_include_columns_section( - output: &mut Vec, - indexes: &BTreeMap, +fn add_status_aggregate_child_row( + counts: &mut LeftJoinStatusCounts, + child_values: &[Value], + plan: &LeftJoinStatusAggregatePlan<'_>, ) -> Result<()> { - let include_entries = indexes - .iter() - .filter(|(_, index)| !index.include_columns.is_empty()) - .collect::>(); - output.extend_from_slice(INDEX_INCLUDE_COLUMNS_SECTION_MAGIC); - output.push(1); - encode_u32( - output, - u32::try_from(include_entries.len()) - .map_err(|_| DbError::constraint("index include entry count exceeds u32"))?, - ); - for (index_name, index) in include_entries { - encode_string(output, index_name)?; - encode_strings(output, &index.include_columns)?; - } - Ok(()) + let Some(child_status) = child_values.get(plan.child_status_index) else { + return Err(DbError::internal("child join row is shorter than schema")); + }; + let Some(child_id) = child_values.get(plan.child_id_index) else { + return Err(DbError::internal("child join row is shorter than schema")); + }; + counts.add_child(child_status, child_id) } -fn encode_full_text_options_section( - output: &mut Vec, - indexes: &BTreeMap, -) -> Result<()> { - let entries = indexes - .iter() - .filter_map(|(name, index)| index.full_text.as_ref().map(|config| (name, config))) - .collect::>(); - output.extend_from_slice(FULL_TEXT_OPTIONS_SECTION_MAGIC); - output.push(1); - encode_u32( - output, - u32::try_from(entries.len()) - .map_err(|_| DbError::constraint("fulltext option entry count exceeds u32"))?, - ); - for (index_name, config) in entries { - encode_string(output, index_name)?; - let bytes = config - .to_json() - .map_err(|error| DbError::internal(error.message))?; - encode_bytes(output, &bytes)?; - } - Ok(()) +#[derive(Clone, Copy, Debug, Default)] +struct LeftJoinStatusCounts { + open_count: i64, + in_progress_count: i64, + resolved_count: i64, + closed_count: i64, + total_count: i64, } -fn encode_schemas_section( - output: &mut Vec, - schemas: &BTreeMap, -) -> Result<()> { - output.extend_from_slice(SCHEMAS_SECTION_MAGIC); - output.push(1); - encode_u32( - output, - u32::try_from(schemas.len()) - .map_err(|_| DbError::constraint("schema count exceeds u32"))?, - ); - for schema in schemas.values() { - encode_string(output, &schema.name)?; +impl LeftJoinStatusCounts { + fn bump(value: &mut i64) -> Result<()> { + *value = value + .checked_add(1) + .ok_or_else(|| DbError::sql("aggregate count exceeds INT64 limits"))?; + Ok(()) } - Ok(()) -} -fn encode_pk_index_roots_section( - output: &mut Vec, - tables: &BTreeMap, - mut offsets: Option<&mut BTreeMap>, -) -> Result<()> { - output.extend_from_slice(PK_INDEX_ROOTS_SECTION_MAGIC); - output.push(1); - encode_u32( - output, - u32::try_from(tables.len()) - .map_err(|_| DbError::constraint("pk index root entry count exceeds u32"))?, - ); - for table in tables.values() { - encode_string(output, &table.name)?; - if let Some(offsets) = offsets.as_deref_mut() { - offsets.insert(table.name.clone(), output.len()); + fn add_child(&mut self, status: &Value, id: &Value) -> Result<()> { + if let Value::Text(status) = status { + match status.as_str() { + "open" => Self::bump(&mut self.open_count)?, + "in_progress" => Self::bump(&mut self.in_progress_count)?, + "resolved" => Self::bump(&mut self.resolved_count)?, + "closed" => Self::bump(&mut self.closed_count)?, + _ => {} + } } - encode_u32(output, table.pk_index_root.unwrap_or(0)); - } - Ok(()) -} - -fn decode_schemas_section( - cursor: &mut Cursor<'_>, - schemas: &mut BTreeMap, -) -> Result<()> { - let section_is_present = cursor - .bytes - .get(cursor.offset..cursor.offset + SCHEMAS_SECTION_MAGIC.len()) - .is_some_and(|magic| magic == SCHEMAS_SECTION_MAGIC); - if !section_is_present { - return Ok(()); - } - cursor.offset += SCHEMAS_SECTION_MAGIC.len(); - let version = cursor.read_u8()?; - if version != 1 { - return Err(DbError::corruption(format!( - "unknown schemas section version {version}" - ))); - } - let schema_count = cursor.read_u32()?; - for _ in 0..schema_count { - let name = cursor.read_string()?; - schemas.insert(name.clone(), SchemaInfo { name }); + if !matches!(id, Value::Null) { + Self::bump(&mut self.total_count)?; + } + Ok(()) } - Ok(()) } -fn decode_index_include_columns_section( - cursor: &mut Cursor<'_>, - indexes: &mut BTreeMap, -) -> Result<()> { - let section_is_present = cursor - .bytes - .get(cursor.offset..cursor.offset + INDEX_INCLUDE_COLUMNS_SECTION_MAGIC.len()) - .is_some_and(|magic| magic == INDEX_INCLUDE_COLUMNS_SECTION_MAGIC); - if !section_is_present { - return Ok(()); - } - cursor.offset += INDEX_INCLUDE_COLUMNS_SECTION_MAGIC.len(); - let version = cursor.read_u8()?; - if version != 1 { - return Err(DbError::corruption(format!( - "unknown index include columns section version {version}" - ))); - } - let entry_count = cursor.read_u32()?; - for _ in 0..entry_count { - let index_name = cursor.read_string()?; - let include_columns = cursor.read_strings()?; - let index = indexes.get_mut(&index_name).ok_or_else(|| { - DbError::corruption(format!( - "index include metadata referenced unknown index {index_name}" - )) - })?; - index.include_columns = include_columns; - } - Ok(()) +#[derive(Clone, Copy, Debug)] +enum IndexedJoinAggregateKind { + CountRows, + CountNonNull(usize), + CountDistinct(usize), + Sum(usize), + Avg(usize), + Min(usize), + Max(usize), } -fn decode_full_text_options_section( - cursor: &mut Cursor<'_>, - indexes: &mut BTreeMap, -) -> Result<()> { - let section_is_present = cursor - .bytes - .get(cursor.offset..cursor.offset + FULL_TEXT_OPTIONS_SECTION_MAGIC.len()) - .is_some_and(|magic| magic == FULL_TEXT_OPTIONS_SECTION_MAGIC); - if !section_is_present { - return Ok(()); - } - cursor.offset += FULL_TEXT_OPTIONS_SECTION_MAGIC.len(); - let version = cursor.read_u8()?; - if version != 1 { - return Err(DbError::corruption(format!( - "unknown fulltext options section version {version}" - ))); - } - let entry_count = cursor.read_u32()?; - for _ in 0..entry_count { - let index_name = cursor.read_string()?; - let config_bytes_len = cursor.read_u32()? as usize; - let config_bytes = cursor.read_slice(config_bytes_len)?; - let config = AnalyzerConfig::from_json(config_bytes) - .map_err(|error| DbError::corruption(error.message))?; - let index = indexes.get_mut(&index_name).ok_or_else(|| { - DbError::corruption(format!( - "fulltext options metadata referenced unknown index {index_name}" - )) - })?; - if index.kind != IndexKind::FullText { - return Err(DbError::corruption(format!( - "fulltext options metadata referenced non-fulltext index {index_name}" - ))); - } - index.full_text = Some(config); - } - Ok(()) +#[allow(dead_code)] +pub(crate) struct LeftJoinAggregatePlan<'a> { + parent_table_name: &'a str, + parent_join_index: usize, + child_table_name: &'a str, + child_join_index: usize, + child_index_name: Option, + group_column_indexes: Vec, + aggregate_kinds: Vec, + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, + include_empty_parent: bool, } -#[cfg(test)] -fn drop_index_include_columns_section(payload: &[u8]) -> Result> { - let start = payload - .windows(INDEX_INCLUDE_COLUMNS_SECTION_MAGIC.len()) - .position(|window| window == INDEX_INCLUDE_COLUMNS_SECTION_MAGIC) - .ok_or_else(|| DbError::internal("index include columns section not found"))?; - let mut cursor = Cursor::new( - payload - .get(start + INDEX_INCLUDE_COLUMNS_SECTION_MAGIC.len()..) - .ok_or_else(|| DbError::internal("index include columns section header truncated"))?, - ); - let _version = cursor.read_u8()?; - let entry_count = cursor.read_u32()?; - for _ in 0..entry_count { - let _index_name = cursor.read_string()?; - let _include_columns = cursor.read_strings()?; - } - let section_len = INDEX_INCLUDE_COLUMNS_SECTION_MAGIC.len() + cursor.offset; - let end = start - .checked_add(section_len) - .ok_or_else(|| DbError::internal("index include section length overflow"))?; - let mut output = Vec::with_capacity(payload.len().saturating_sub(section_len)); - output.extend_from_slice( - payload - .get(..start) - .ok_or_else(|| DbError::internal("invalid include section start"))?, - ); - output.extend_from_slice( - payload - .get(end..) - .ok_or_else(|| DbError::internal("invalid include section end"))?, - ); - Ok(output) +struct ThreeTableGenrePopularityPlan<'a> { + genre_table_name: &'a str, + genre_id_index: usize, + genre_name_index: usize, + bridge_table_name: &'a str, + bridge_movie_id_index: usize, + bridge_genre_index_name: String, + movie_table_name: &'a str, + movie_rating_index: usize, + movie_index_name: Option, + movie_id_is_rowid_alias: bool, + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, } -fn decode_generated_columns_section( - cursor: &mut Cursor<'_>, - tables: &mut BTreeMap, -) -> Result<()> { - let section_is_versioned = cursor - .bytes - .get(cursor.offset..cursor.offset + GENERATED_COLUMNS_SECTION_MAGIC.len()) - .is_some_and(|magic| magic == GENERATED_COLUMNS_SECTION_MAGIC); - if section_is_versioned { - cursor.offset += GENERATED_COLUMNS_SECTION_MAGIC.len(); - let version = cursor.read_u8()?; - if version != 1 { - return Err(DbError::corruption(format!( - "unknown generated columns section version {version}" - ))); - } - } - let generated_column_count = cursor.read_u32()?; - for _ in 0..generated_column_count { - let table_name = cursor.read_string()?; - let column_name = cursor.read_string()?; - let generated_sql = cursor.read_string()?; - let generated_stored = if section_is_versioned { - cursor.read_bool()? - } else { - true - }; - let table = tables.get_mut(&table_name).ok_or_else(|| { - DbError::corruption(format!( - "generated column metadata referenced unknown table {table_name}" - )) - })?; - let column = table - .columns - .iter_mut() - .find(|column| identifiers_equal(&column.name, &column_name)) - .ok_or_else(|| { - DbError::corruption(format!( - "generated column metadata referenced unknown column {}.{}", - table_name, column_name - )) - })?; - column.generated_sql = Some(generated_sql); - column.generated_stored = generated_stored; - } - Ok(()) +struct MovieTagSearchPlan<'a> { + tag_table_name: &'a str, + tag_id_index: usize, + tag_name_index_name: String, + tag_name_value: Value, + bridge_table_name: &'a str, + bridge_movie_id_index: usize, + bridge_tag_index_name: String, + movie_table_name: &'a str, + movie_index_name: Option, + movie_id_is_rowid_alias: bool, + projection_indexes: Vec, + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, } -fn decode_spatial_columns_section( - cursor: &mut Cursor<'_>, - tables: &mut BTreeMap, -) -> Result<()> { - let section_is_present = cursor - .bytes - .get(cursor.offset..cursor.offset + SPATIAL_COLUMNS_SECTION_MAGIC.len()) - .is_some_and(|magic| magic == SPATIAL_COLUMNS_SECTION_MAGIC); - if !section_is_present { - return Ok(()); - } - cursor.offset += SPATIAL_COLUMNS_SECTION_MAGIC.len(); - let version = cursor.read_u8()?; - if version != 1 { - return Err(DbError::corruption(format!( - "unknown spatial columns section version {version}" - ))); - } - let entry_count = cursor.read_u32()?; - for _ in 0..entry_count { - let table_name = cursor.read_string()?; - let column_name = cursor.read_string()?; - let subtype = decode_spatial_subtype_tag(cursor.read_u8()?)?; - let dimensions = decode_spatial_dimensions_tag(cursor.read_u8()?)?; - let srid = i32::try_from(cursor.read_u32()?) - .map_err(|_| DbError::corruption("spatial SRID exceeds i32"))?; - let table = tables.get_mut(&table_name).ok_or_else(|| { - DbError::corruption(format!( - "spatial column metadata referenced unknown table {table_name}" - )) - })?; - let column = table - .columns - .iter_mut() - .find(|column| identifiers_equal(&column.name, &column_name)) - .ok_or_else(|| { - DbError::corruption(format!( - "spatial column metadata referenced unknown column {}.{}", - table_name, column_name - )) - })?; - column.spatial_type = Some(crate::catalog::SpatialTypeInfo { - subtype, - dimensions, - srid, - }); - } - Ok(()) +struct MovieWatchlistPlan<'a> { + watchlist_table_name: &'a str, + watchlist_movie_id_index: usize, + watchlist_priority_index: usize, + watchlist_user_index_name: String, + user_handle_value: Value, + movie_table_name: &'a str, + movie_id_index: usize, + movie_title_index: usize, + movie_index_name: Option, + movie_id_is_rowid_alias: bool, + review_table_name: &'a str, + review_score_index: usize, + review_movie_index_name: String, + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, } -fn decode_enum_columns_section( - cursor: &mut Cursor<'_>, - tables: &mut BTreeMap, -) -> Result<()> { - let section_is_present = cursor - .bytes - .get(cursor.offset..cursor.offset + ENUM_COLUMNS_SECTION_MAGIC.len()) - .is_some_and(|magic| magic == ENUM_COLUMNS_SECTION_MAGIC); - if !section_is_present { - return Ok(()); - } - cursor.offset += ENUM_COLUMNS_SECTION_MAGIC.len(); - let version = cursor.read_u8()?; - if version != 1 { - return Err(DbError::corruption(format!( - "unknown enum columns section version {version}" - ))); - } - let entry_count = cursor.read_u32()?; - for _ in 0..entry_count { - let table_name = cursor.read_string()?; - let column_name = cursor.read_string()?; - let type_id = cursor.read_u64()?; - let label_count = cursor.read_u32()?; - let mut labels = Vec::with_capacity(label_count as usize); - for _ in 0..label_count { - labels.push(EnumLabel { - id: cursor.read_u64()?, - label: cursor.read_string()?, - }); - } - let table = tables.get_mut(&table_name).ok_or_else(|| { - DbError::corruption(format!( - "enum column metadata referenced unknown table {table_name}" - )) - })?; - let column = table - .columns - .iter_mut() - .find(|column| identifiers_equal(&column.name, &column_name)) - .ok_or_else(|| { - DbError::corruption(format!( - "enum column metadata referenced unknown column {}.{}", - table_name, column_name - )) - })?; - column.enum_type = Some(EnumTypeInfo { type_id, labels }); - } - Ok(()) +struct MovieTopRatedByYearPlan<'a> { + movie_table_name: &'a str, + movie_id_index: usize, + movie_release_year_index: usize, + movie_release_year_index_name: Option, + movie_projection_indexes: Vec, + release_year_value: Value, + review_table_name: &'a str, + review_score_index: usize, + review_movie_index_name: String, + min_review_count: i64, + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, } -fn decode_pk_index_roots_section( - cursor: &mut Cursor<'_>, - tables: &mut BTreeMap, -) -> Result<()> { - let section_is_present = cursor - .bytes - .get(cursor.offset..cursor.offset + PK_INDEX_ROOTS_SECTION_MAGIC.len()) - .is_some_and(|magic| magic == PK_INDEX_ROOTS_SECTION_MAGIC); - if !section_is_present { - return Ok(()); - } - cursor.offset += PK_INDEX_ROOTS_SECTION_MAGIC.len(); - let version = cursor.read_u8()?; - if version != 1 { - return Err(DbError::corruption(format!( - "unknown pk index roots section version {version}" - ))); - } - let entry_count = cursor.read_u32()?; - for _ in 0..entry_count { - let table_name = cursor.read_string()?; - let pk_index_root = match cursor.read_u32()? { - 0 => None, - page_id => Some(page_id), - }; - let table = tables.get_mut(&table_name).ok_or_else(|| { - DbError::corruption(format!( - "pk index root metadata referenced unknown table {table_name}" - )) - })?; - table.pk_index_root = pk_index_root; - } - Ok(()) +struct MovieBusiestPeoplePlan<'a> { + people_table_name: &'a str, + people_projection_indexes: Vec, + people_index_name: Option, + people_id_is_rowid_alias: bool, + roles_person_index_name: String, + column_names: Vec, + limit: Option, + offset: usize, } -fn encode_spatial_subtype_tag(subtype: crate::catalog::SpatialSubtype) -> u8 { - match subtype { - crate::catalog::SpatialSubtype::Any => 0, - crate::catalog::SpatialSubtype::Point => 1, - crate::catalog::SpatialSubtype::LineString => 2, - crate::catalog::SpatialSubtype::Polygon => 3, - crate::catalog::SpatialSubtype::MultiPoint => 4, - crate::catalog::SpatialSubtype::MultiLineString => 5, - crate::catalog::SpatialSubtype::MultiPolygon => 6, - } -} - -fn decode_spatial_subtype_tag(tag: u8) -> Result { - match tag { - 0 => Ok(crate::catalog::SpatialSubtype::Any), - 1 => Ok(crate::catalog::SpatialSubtype::Point), - 2 => Ok(crate::catalog::SpatialSubtype::LineString), - 3 => Ok(crate::catalog::SpatialSubtype::Polygon), - 4 => Ok(crate::catalog::SpatialSubtype::MultiPoint), - 5 => Ok(crate::catalog::SpatialSubtype::MultiLineString), - 6 => Ok(crate::catalog::SpatialSubtype::MultiPolygon), - _ => Err(DbError::corruption("unknown spatial subtype tag")), - } -} - -fn encode_spatial_dimensions_tag(dimensions: crate::catalog::SpatialDimensions) -> u8 { - match dimensions { - crate::catalog::SpatialDimensions::Any => 0, - crate::catalog::SpatialDimensions::Xy => 1, - crate::catalog::SpatialDimensions::Xyz => 2, - crate::catalog::SpatialDimensions::Xym => 3, - crate::catalog::SpatialDimensions::Xyzm => 4, - } -} - -fn decode_spatial_dimensions_tag(tag: u8) -> Result { - match tag { - 0 => Ok(crate::catalog::SpatialDimensions::Any), - 1 => Ok(crate::catalog::SpatialDimensions::Xy), - 2 => Ok(crate::catalog::SpatialDimensions::Xyz), - 3 => Ok(crate::catalog::SpatialDimensions::Xym), - 4 => Ok(crate::catalog::SpatialDimensions::Xyzm), - _ => Err(DbError::corruption("unknown spatial dimensions tag")), - } -} - -fn encode_column_type(column_type: crate::catalog::ColumnType) -> u8 { - match column_type { - crate::catalog::ColumnType::Int64 => 0, - crate::catalog::ColumnType::Float64 => 1, - crate::catalog::ColumnType::Text => 2, - crate::catalog::ColumnType::Bool => 3, - crate::catalog::ColumnType::Blob => 4, - crate::catalog::ColumnType::Decimal => 5, - crate::catalog::ColumnType::Uuid => 6, - crate::catalog::ColumnType::Timestamp => 7, - crate::catalog::ColumnType::Geometry => 8, - crate::catalog::ColumnType::Geography => 9, - crate::catalog::ColumnType::Enum => 10, - crate::catalog::ColumnType::IpAddr => 11, - crate::catalog::ColumnType::Cidr => 12, - crate::catalog::ColumnType::Date => 13, - crate::catalog::ColumnType::Time => 14, - crate::catalog::ColumnType::TimestampTz => 15, - crate::catalog::ColumnType::Interval => 16, - crate::catalog::ColumnType::MacAddr => 17, - } -} - -fn decode_column_type(tag: u8) -> Result { - match tag { - 0 => Ok(crate::catalog::ColumnType::Int64), - 1 => Ok(crate::catalog::ColumnType::Float64), - 2 => Ok(crate::catalog::ColumnType::Text), - 3 => Ok(crate::catalog::ColumnType::Bool), - 4 => Ok(crate::catalog::ColumnType::Blob), - 5 => Ok(crate::catalog::ColumnType::Decimal), - 6 => Ok(crate::catalog::ColumnType::Uuid), - 7 => Ok(crate::catalog::ColumnType::Timestamp), - 8 => Ok(crate::catalog::ColumnType::Geometry), - 9 => Ok(crate::catalog::ColumnType::Geography), - 10 => Ok(crate::catalog::ColumnType::Enum), - 11 => Ok(crate::catalog::ColumnType::IpAddr), - 12 => Ok(crate::catalog::ColumnType::Cidr), - 13 => Ok(crate::catalog::ColumnType::Date), - 14 => Ok(crate::catalog::ColumnType::Time), - 15 => Ok(crate::catalog::ColumnType::TimestampTz), - 16 => Ok(crate::catalog::ColumnType::Interval), - 17 => Ok(crate::catalog::ColumnType::MacAddr), - _ => Err(DbError::corruption("unknown column type tag")), - } -} - -fn decode_index_kind(tag: u8) -> Result { - match tag { - 0 => Ok(crate::catalog::IndexKind::Btree), - 1 => Ok(crate::catalog::IndexKind::Trigram), - 2 => Ok(crate::catalog::IndexKind::Spatial), - 3 => Ok(crate::catalog::IndexKind::FullText), - _ => Err(DbError::corruption("unknown index kind tag")), - } -} - -fn decode_trigger_kind(tag: u8) -> Result { - match tag { - 0 => Ok(crate::catalog::TriggerKind::After), - 1 => Ok(crate::catalog::TriggerKind::InsteadOf), - _ => Err(DbError::corruption("unknown trigger kind tag")), - } -} - -fn decode_trigger_event(tag: u8) -> Result { - match tag { - 0 => Ok(crate::catalog::TriggerEvent::Insert), - 1 => Ok(crate::catalog::TriggerEvent::Update), - 2 => Ok(crate::catalog::TriggerEvent::Delete), - _ => Err(DbError::corruption("unknown trigger event tag")), - } -} - -fn decode_fk_action(tag: u8) -> Result { - match tag { - 0 => Ok(crate::catalog::ForeignKeyAction::NoAction), - 1 => Ok(crate::catalog::ForeignKeyAction::Restrict), - 2 => Ok(crate::catalog::ForeignKeyAction::Cascade), - 3 => Ok(crate::catalog::ForeignKeyAction::SetNull), - _ => Err(DbError::corruption("unknown foreign-key action tag")), - } -} - -fn decode_foreign_key(cursor: &mut Cursor<'_>) -> Result { - Ok(crate::catalog::ForeignKeyConstraint { - name: cursor.read_optional_string()?, - columns: cursor.read_strings()?, - referenced_table: cursor.read_string()?, - referenced_columns: cursor.read_strings()?, - on_delete: decode_fk_action(cursor.read_u8()?)?, - on_update: decode_fk_action(cursor.read_u8()?)?, - }) +struct MovieBusiestPeopleCount { + person_key: RuntimeBtreeKey, + role_count: i64, } -struct Cursor<'a> { - bytes: &'a [u8], +struct DirectorsCtePlan<'a> { + roles_table_name: &'a str, + role_person_id_index: usize, + role_movie_id_index: usize, + role_job_index: usize, + director_job: String, + movie_table_name: &'a str, + movie_title_index: usize, + movie_rating_index: usize, + movie_index_name: Option, + movie_id_is_rowid_alias: bool, + min_films: i64, + title_separator: String, + column_names: Vec, + order_by: Option>, + limit: Option, offset: usize, } -impl<'a> Cursor<'a> { - fn new(bytes: &'a [u8]) -> Self { - Self { bytes, offset: 0 } - } +struct DirectedMoviesCtePlan<'a> { + roles_table_name: &'a str, + role_person_id_index: usize, + role_movie_id_index: usize, + role_job_index: usize, + director_job: String, + movie_table_name: &'a str, + movie_title_index: usize, + movie_rating_index: usize, + movie_index_name: Option, + movie_id_is_rowid_alias: bool, +} - fn read_slice(&mut self, len: usize) -> Result<&'a [u8]> { - let end = self - .offset - .checked_add(len) - .ok_or_else(|| DbError::corruption("cursor overflow"))?; - let bytes = self - .bytes - .get(self.offset..end) - .ok_or_else(|| DbError::corruption("truncated catalog state"))?; - self.offset = end; - Ok(bytes) - } +struct DirectorsTopDirsCtePlan { + min_films: i64, +} - fn read_u8(&mut self) -> Result { - let value = *self - .bytes - .get(self.offset) - .ok_or_else(|| DbError::corruption("truncated catalog state"))?; - self.offset += 1; - Ok(value) - } +type DirectorsFinalSelectAnalysis = ( + Vec, + Option>, + Option, + usize, + String, +); - fn read_bool(&mut self) -> Result { - Ok(self.read_u8()? != 0) - } +struct DirectorsCteAccumulator { + person_id: Value, + films: i64, + rating_sum: f64, + rating_count: i64, + titles: Vec, +} - fn read_u32(&mut self) -> Result { - let bytes = self.read_slice(4)?; - Ok(u32::from_le_bytes(bytes.try_into().expect("u32"))) +impl DirectorsCteAccumulator { + fn new(person_id: Value) -> Self { + Self { + person_id, + films: 0, + rating_sum: 0.0, + rating_count: 0, + titles: Vec::new(), + } } - fn read_u64(&mut self) -> Result { - let bytes = self.read_slice(8)?; - Ok(u64::from_le_bytes(bytes.try_into().expect("u64"))) + fn add_movie(&mut self, movie_values: &[Value], title_index: usize, rating_index: usize) { + self.films = self.films.saturating_add(1); + if let Some(rating) = movie_values + .get(rating_index) + .and_then(indexed_join_aggregate_as_f64) + { + self.rating_sum += rating; + self.rating_count = self.rating_count.saturating_add(1); + } + if let Some(Value::Text(title)) = movie_values.get(title_index) { + self.titles.push(title.clone()); + } } +} - fn read_i64(&mut self) -> Result { - let bytes = self.read_slice(8)?; - Ok(i64::from_le_bytes(bytes.try_into().expect("i64"))) - } +struct IndexedJoinAggregateState { + accumulators: Vec, +} - fn read_string(&mut self) -> Result { - let len = self.read_u32()? as usize; - let bytes = self.read_slice(len)?; - std::str::from_utf8(bytes) - .map(|s| s.to_owned()) - .map_err(|error| { - DbError::corruption(format!("catalog state string is not valid UTF-8: {error}")) +enum IndexedJoinAccumulator { + CountRows { count: i64 }, + CountNonNull { col: usize, count: i64 }, + CountDistinct { col: usize, seen: BTreeSet> }, + Sum { col: usize, sum: f64, count: i64 }, + Avg { col: usize, sum: f64, count: i64 }, + Min { col: usize, value: Option }, + Max { col: usize, value: Option }, +} + +impl IndexedJoinAggregateState { + fn new(kinds: &[IndexedJoinAggregateKind]) -> Self { + let accumulators = kinds + .iter() + .map(|kind| match kind { + IndexedJoinAggregateKind::CountRows => { + IndexedJoinAccumulator::CountRows { count: 0 } + } + IndexedJoinAggregateKind::CountNonNull(col) => { + IndexedJoinAccumulator::CountNonNull { + col: *col, + count: 0, + } + } + IndexedJoinAggregateKind::CountDistinct(col) => { + IndexedJoinAccumulator::CountDistinct { + col: *col, + seen: BTreeSet::new(), + } + } + IndexedJoinAggregateKind::Sum(col) => IndexedJoinAccumulator::Sum { + col: *col, + sum: 0.0, + count: 0, + }, + IndexedJoinAggregateKind::Avg(col) => IndexedJoinAccumulator::Avg { + col: *col, + sum: 0.0, + count: 0, + }, + IndexedJoinAggregateKind::Min(col) => IndexedJoinAccumulator::Min { + col: *col, + value: None, + }, + IndexedJoinAggregateKind::Max(col) => IndexedJoinAccumulator::Max { + col: *col, + value: None, + }, }) + .collect(); + Self { accumulators } } - fn read_optional_string(&mut self) -> Result> { - if self.read_bool()? { - Ok(Some(self.read_string()?)) - } else { - Ok(None) + fn accumulate(&mut self, child_values: &[Value]) -> Result<()> { + for acc in &mut self.accumulators { + match acc { + IndexedJoinAccumulator::CountRows { count } => { + *count = count.saturating_add(1); + } + IndexedJoinAccumulator::CountNonNull { col, count } => { + if let Some(value) = child_values.get(*col) { + if !matches!(value, Value::Null) { + *count = count.saturating_add(1); + } + } + } + IndexedJoinAccumulator::CountDistinct { col, seen } => { + if let Some(value) = child_values.get(*col) { + if !matches!(value, Value::Null) { + seen.insert(row_identity(std::slice::from_ref(value))?); + } + } + } + IndexedJoinAccumulator::Sum { col, sum, count } => { + if let Some(value) = child_values.get(*col) { + if let Some(f) = indexed_join_aggregate_as_f64(value) { + *sum += f; + *count = count.saturating_add(1); + } + } + } + IndexedJoinAccumulator::Avg { col, sum, count } => { + if let Some(value) = child_values.get(*col) { + if let Some(f) = indexed_join_aggregate_as_f64(value) { + *sum += f; + *count = count.saturating_add(1); + } + } + } + IndexedJoinAccumulator::Min { col, value } => { + if let Some(v) = child_values.get(*col) { + if !matches!(v, Value::Null) { + match value { + None => *value = Some(v.clone()), + Some(curr) => { + if compare_values_no_error(v, curr) + == Some(std::cmp::Ordering::Less) + { + *value = Some(v.clone()); + } + } + } + } + } + } + IndexedJoinAccumulator::Max { col, value } => { + if let Some(v) = child_values.get(*col) { + if !matches!(v, Value::Null) { + match value { + None => *value = Some(v.clone()), + Some(curr) => { + if compare_values_no_error(v, curr) + == Some(std::cmp::Ordering::Greater) + { + *value = Some(v.clone()); + } + } + } + } + } + } + } } + Ok(()) } - fn read_strings(&mut self) -> Result> { - let len = self.read_u32()? as usize; - (0..len).map(|_| self.read_string()).collect() - } -} - -fn dataset_to_result(dataset: Dataset) -> QueryResult { - let Dataset { columns, rows } = dataset; - QueryResult::with_rows( - columns.into_iter().map(|binding| binding.name).collect(), - Arc::unwrap_or_clone(rows) - .into_iter() - .map(QueryRow::new) - .collect(), - ) -} - -fn crm_column_index(table: &TableSchema, column: &str, column_type: ColumnType) -> Option { - let index = schema_column_index(table, column)?; - if table.columns.get(index)?.column_type == column_type { - Some(index) - } else { - None + fn finalize_into(self, output: &mut Vec) { + for acc in self.accumulators { + match acc { + IndexedJoinAccumulator::CountRows { count } => { + output.push(Value::Int64(count)); + } + IndexedJoinAccumulator::CountNonNull { count, .. } => { + output.push(Value::Int64(count)); + } + IndexedJoinAccumulator::CountDistinct { seen, .. } => { + output.push(Value::Int64(seen.len() as i64)); + } + IndexedJoinAccumulator::Sum { sum, count, .. } => { + if count == 0 { + output.push(Value::Null); + } else { + output.push(Value::Float64(sum)); + } + } + IndexedJoinAccumulator::Avg { sum, count, .. } => { + if count == 0 { + output.push(Value::Null); + } else { + output.push(Value::Float64(sum / count as f64)); + } + } + IndexedJoinAccumulator::Min { value, .. } => { + output.push(value.unwrap_or(Value::Null)); + } + IndexedJoinAccumulator::Max { value, .. } => { + output.push(value.unwrap_or(Value::Null)); + } + } + } } } -fn crm_i64_cell(value: Option<&Value>, table: &str, column: &str) -> Result> { +fn indexed_join_aggregate_as_f64(value: &Value) -> Option { match value { - Some(Value::Int64(value)) => Ok(Some(*value)), - Some(Value::Null) => Ok(None), - Some(other) => Err(DbError::sql(format!( - "{table}.{column} expected INT64 but found {other:?}" - ))), - None => Err(DbError::internal(format!( - "{table}.{column} is missing from row" - ))), + Value::Int64(v) => Some(*v as f64), + Value::Float64(v) => Some(*v), + Value::Decimal { scaled, scale } => { + let scaled_u = if *scaled >= 0 { + *scaled as u64 + } else { + return None; + }; + let divisor = 10u64.checked_pow(*scale as u32).unwrap_or(1); + Some(scaled_u as f64 / divisor as f64) + } + _ => None, } } -fn crm_text_cell(value: Option<&Value>, table: &str, column: &str) -> Result> { - match value { - Some(Value::Text(value)) => Ok(Some(value.clone())), - Some(Value::Null) => Ok(None), - Some(other) => Err(DbError::sql(format!( - "{table}.{column} expected TEXT but found {other:?}" - ))), - None => Err(DbError::internal(format!( - "{table}.{column} is missing from row" - ))), - } +fn compare_values_no_error(a: &Value, b: &Value) -> Option { + crate::exec::expressions::compare_values(a, b).ok() } -fn crm_revenue_from_covering_dense( - covering: &RuntimeCoveringPayloads, - company_id_offset: usize, - total_offset: usize, - deleted: &BTreeSet, - company_names: &BTreeMap, -) -> Result>> { - let Some(max_company_id) = company_names.keys().copied().max() else { - return Ok(Some(BTreeMap::new())); - }; - if !(0..=1_000_000).contains(&max_company_id) { - return Ok(None); - } - - let len = usize::try_from(max_company_id) - .ok() - .and_then(|value| value.checked_add(1)) - .ok_or_else(|| DbError::constraint("company id exceeded addressable summary range"))?; - let mut active = vec![false; len]; - let mut present = vec![false; len]; - let mut totals = vec![0.0_f64; len]; - for company_id in company_names.keys().copied() { - let Ok(index) = usize::try_from(company_id) else { - return Ok(None); - }; - active[index] = true; - } +enum SimpleIndexedProjectionRowIds<'a> { + Borrowed(RuntimeRowIdSet<'a>), + Owned(Vec), +} - if deleted.is_empty() { - for values in covering.rows.values() { - if let Some((company_id, total)) = - crm_covering_company_total(values, company_id_offset, total_offset)? - { - let Ok(index) = usize::try_from(company_id) else { - continue; - }; - if index < active.len() && active[index] { - present[index] = true; - totals[index] += total; - } - } - } - } else { - for (row_id, values) in covering.rows.iter() { - if deleted.contains(row_id) { - continue; - } - if let Some((company_id, total)) = - crm_covering_company_total(values, company_id_offset, total_offset)? - { - let Ok(index) = usize::try_from(company_id) else { - continue; - }; - if index < active.len() && active[index] { - present[index] = true; - totals[index] += total; - } - } +impl SimpleIndexedProjectionRowIds<'_> { + fn len(&self) -> usize { + match self { + Self::Borrowed(row_ids) => row_ids.len(), + Self::Owned(row_ids) => row_ids.len(), } } - let mut revenues = BTreeMap::new(); - for company_id in company_names.keys().copied() { - let index = usize::try_from(company_id) - .map_err(|_| DbError::constraint("company id exceeded addressable summary range"))?; - if present.get(index).copied().unwrap_or(false) { - revenues.insert(company_id, totals.get(index).copied().unwrap_or(0.0)); - } + fn into_vec(self) -> Vec { + let mut row_ids = Vec::with_capacity(self.len()); + self.for_each(|row_id| row_ids.push(row_id)); + row_ids } - Ok(Some(revenues)) -} -fn crm_revenue_from_covering_sparse( - covering: &RuntimeCoveringPayloads, - company_id_offset: usize, - total_offset: usize, - deleted: &BTreeSet, - company_names: &BTreeMap, -) -> Result> { - let mut revenues = BTreeMap::new(); - for (row_id, values) in covering.rows.iter() { - if deleted.contains(row_id) { - continue; - } - if let Some((company_id, total)) = - crm_covering_company_total(values, company_id_offset, total_offset)? - { - if company_names.contains_key(&company_id) { - *revenues.entry(company_id).or_insert(0.0) += total; - } + fn into_sorted_vec(self, descending: bool) -> Vec { + let mut row_ids = self.into_vec(); + row_ids.sort_unstable(); + if descending { + row_ids.reverse(); } + row_ids } - Ok(revenues) -} -fn crm_revenue_from_invoice_rows( - invoices_source: VisibleTableRowSource<'_>, - company_id_index: usize, - total_index: usize, - company_names: &BTreeMap, -) -> Result> { - let mut invoice_company_ids = BTreeMap::new(); - invoices_source.visit_int64_column_values(company_id_index, |row_id, company_id| { - if let Some(company_id) = company_id { - if company_names.contains_key(&company_id) { - invoice_company_ids.insert(row_id, company_id); + fn for_each(self, mut f: impl FnMut(i64)) { + match self { + Self::Borrowed(row_ids) => row_ids.for_each(f), + Self::Owned(row_ids) => { + for row_id in row_ids { + f(row_id); + } } } - Ok(()) - })?; - let mut revenues = BTreeMap::new(); - invoices_source.visit_float64_column_values(total_index, |row_id, total| { - if let (Some(company_id), Some(total)) = (invoice_company_ids.get(&row_id), total) { - *revenues.entry(*company_id).or_insert(0.0_f64) += total; - } - Ok(()) - })?; - Ok(revenues) -} - -fn crm_covering_company_total( - values: &[Value], - company_id_offset: usize, - total_offset: usize, -) -> Result> { - let Some(company_id) = values.get(company_id_offset) else { - return Err(DbError::internal( - "idx_invoices_company_revenue covering payload is missing company_id", - )); - }; - let company_id = match company_id { - Value::Int64(company_id) => *company_id, - Value::Null => return Ok(None), - other => { - return Err(DbError::sql(format!( - "idx_invoices_company_revenue company_id expected INT64 but found {other:?}" - ))) - } - }; - let Some(total) = values.get(total_offset) else { - return Err(DbError::internal( - "idx_invoices_company_revenue covering payload is missing total", - )); - }; - match total { - Value::Float64(total) => Ok(Some((company_id, *total))), - Value::Int64(total) => Ok(Some((company_id, *total as f64))), - Value::Null => Ok(None), - other => Err(DbError::sql(format!( - "idx_invoices_company_revenue total expected FLOAT64 but found {other:?}" - ))), } } -fn crm_table(item: &FromItem, table_name: &str, expected_alias: &str) -> bool { - match item { - FromItem::Table { name, alias } => { - identifiers_equal(name, table_name) - && alias - .as_deref() - .is_none_or(|candidate| identifiers_equal(candidate, expected_alias)) - } - _ => false, - } +#[derive(Clone, Debug)] +struct ActiveRowPolicy { + name: String, + expr: Expr, } -fn crm_join_on_columns( - constraint: &JoinConstraint, - left_tables: &[&str], - left_column: &str, - right_tables: &[&str], - right_column: &str, -) -> bool { - let JoinConstraint::On(Expr::Binary { - left, - op: BinaryOp::Eq, - right, - }) = constraint - else { - return false; - }; - - (crm_column(left, left_tables, left_column) && crm_column(right, right_tables, right_column)) - || (crm_column(left, right_tables, right_column) - && crm_column(right, left_tables, left_column)) +#[derive(Clone, Debug)] +struct ActiveColumnMask { + table_name: String, + column_name: String, + expr: Expr, } -fn crm_count_distinct_users(expr: &Expr) -> bool { - let Expr::Aggregate { - name, - args, - distinct, - star, - order_by, - within_group, - } = expr - else { - return false; - }; - - identifiers_equal(name, "count") - && *distinct - && !*star - && order_by.is_empty() - && !*within_group - && args.len() == 1 - && crm_column(&args[0], &["u", "users"], "id") +struct SimpleCountQueryPlan<'a> { + table_name: &'a str, + table_ref: &'a str, + filter: Option<&'a Expr>, + column_name: String, } -fn crm_coalesced_invoice_total_sum(expr: &Expr) -> bool { - let Expr::Function { name, args } = expr else { - return false; - }; - - identifiers_equal(name, "coalesce") - && args.len() == 2 - && crm_invoice_total_sum(&args[0]) - && crm_zero_literal(&args[1]) +struct SimpleMinMaxQueryPlan<'a> { + table_name: &'a str, + column_index: usize, + is_max: bool, + column_name: String, } -fn crm_invoice_total_sum(expr: &Expr) -> bool { - let Expr::Aggregate { - name, - args, - distinct, - star, - order_by, - within_group, - } = expr - else { - return false; - }; - - identifiers_equal(name, "sum") - && !*distinct - && !*star - && order_by.is_empty() - && !*within_group - && args.len() == 1 - && crm_column(&args[0], &["i", "invoices"], "total") +pub(crate) struct SimpleGroupedCountPlan<'a> { + table_name: &'a str, + group_exprs: &'a [Expr], + group_eval_bindings: Vec, + filter_expr: Option, + column_names: Vec, + projection_exprs: Option>, + raw_projection_bindings: Option>, + having: Option, + having_bindings: Vec, + order_by: Option>, + limit: Option, + offset: usize, } -fn crm_zero_literal(expr: &Expr) -> bool { - match expr { - Expr::Literal(Value::Int64(value)) => *value == 0, - Expr::Literal(Value::Float64(value)) => *value == 0.0, - Expr::Literal(Value::Decimal { scaled, .. }) => *scaled == 0, - _ => false, - } +struct SimpleGroupedNumericAggregatePlan<'a> { + table_name: &'a str, + group_exprs: &'a [Expr], + group_eval_bindings: Vec, + filter_expr: Option, + column_names: Vec, + aggregate_bindings: Vec, + projection_exprs: Option>, + raw_projection_bindings: Option>, + having: Option, + having_bindings: Vec, + order_by: Option>, + limit: Option, + offset: usize, } -fn crm_column(expr: &Expr, tables: &[&str], column: &str) -> bool { - match expr { - Expr::Column { - table, - column: candidate, - } => { - identifiers_equal(candidate, column) - && table.as_deref().is_some_and(|candidate_table| { - tables - .iter() - .any(|table| identifiers_equal(candidate_table, table)) - }) - } - _ => false, - } +struct GeneralGroupedSingleTablePlan<'a> { + table_name: &'a str, + table_alias: Option<&'a str>, + group_by: &'a [Expr], + filter: Option<&'a Expr>, + projection: &'a [SelectItem], + having: Option<&'a Expr>, + order_by: &'a [crate::sql::ast::OrderBy], + distinct: bool, + limit: Option<&'a Expr>, + offset: Option<&'a Expr>, } -pub(crate) fn projection_has_aggregate_items(items: &[SelectItem]) -> bool { - items.iter().any(|item| match item { - SelectItem::Expr { expr, .. } => expr_contains_aggregate(expr), - SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => false, - }) +#[derive(Clone, Copy)] +struct WindowEvalContext<'a> { + dataset: &'a Dataset, + params: &'a [Value], + ctes: &'a BTreeMap, } -fn select_requires_grouped_evaluation(runtime: &EngineRuntime, select: &Select) -> Result { - if !select.group_by.is_empty() || projection_has_aggregate_items(&select.projection) { - return Ok(true); - } - if select.having.as_ref().is_some_and(expr_contains_aggregate) { - return Ok(true); - } - projection_has_runtime_extension_aggregate_items(runtime, &select.projection).and_then( - |has_projection_aggregate| { - if has_projection_aggregate { - return Ok(true); - } - select - .having - .as_ref() - .map(|expr| expr_contains_runtime_extension_aggregate(runtime, expr)) - .transpose() - .map(Option::unwrap_or_default) - }, - ) +struct WindowSortedRow { + row_index: usize, + order_keys: Vec, } -fn projection_has_runtime_extension_aggregate_items( - runtime: &EngineRuntime, - items: &[SelectItem], -) -> Result { - for item in items { - if let SelectItem::Expr { expr, .. } = item { - if expr_contains_runtime_extension_aggregate(runtime, expr)? { - return Ok(true); - } - } - } - Ok(false) +struct ReviewRankingFastRow { + row_id: i64, + movie_id: i64, + score: i64, + author: Value, } -fn simple_btree_lookup(filter: &Expr) -> Option<(Option<&str>, &str, &Expr)> { - match filter { - Expr::Binary { left, op, right } if *op == BinaryOp::Eq => match (&**left, &**right) { - (Expr::Column { table, column }, value) if simple_btree_lookup_value_expr(value) => { - Some((table.as_deref(), column.as_str(), value)) - } - (value, Expr::Column { table, column }) if simple_btree_lookup_value_expr(value) => { - Some((table.as_deref(), column.as_str(), value)) - } - _ => None, - }, - _ => None, - } +struct CastBillingFastRow { + row_id: i64, + movie_id: i64, + person_id: Value, + billing_order: i64, + billing_value: Value, } -fn simple_btree_lookup_value_expr(expr: &Expr) -> bool { - match expr { - Expr::Literal(_) | Expr::Parameter(_) => true, - Expr::Cast { expr, .. } => simple_btree_lookup_value_expr(expr), - _ => false, - } +struct RollingAvgFastRow { + row_id: i64, + movie_id: i64, + id_value: Value, + rating: Value, } -fn simple_btree_lookup_terms(filter: &Expr) -> Option, &str, &Expr)>> { - fn collect<'a>( - expr: &'a Expr, - terms: &mut Vec<(Option<&'a str>, &'a str, &'a Expr)>, - ) -> Option<()> { - match expr { - Expr::Binary { - left, - op: BinaryOp::And, - right, - } => { - collect(left, terms)?; - collect(right, terms)?; - Some(()) - } - _ => { - let term = simple_btree_lookup(expr)?; - if terms - .iter() - .any(|(_, column, _)| identifiers_equal(column, term.1)) - { - return None; - } - terms.push(term); - Some(()) - } +fn compare_window_sorted_rows( + left: &WindowSortedRow, + right: &WindowSortedRow, + order_by: &[OrderBy], +) -> std::cmp::Ordering { + for (order, (left_value, right_value)) in order_by + .iter() + .zip(left.order_keys.iter().zip(right.order_keys.iter())) + { + let ordering = compare_values(left_value, right_value).unwrap_or(std::cmp::Ordering::Equal); + if ordering != std::cmp::Ordering::Equal { + return if order.descending { + ordering.reverse() + } else { + ordering + }; } } - - let mut terms = Vec::new(); - collect(filter, &mut terms)?; - (!terms.is_empty()).then_some(terms) + left.row_index.cmp(&right.row_index) } -fn ordered_lookup_terms_for_index<'a>( - index: &IndexSchema, - lookup_terms: &[(Option<&'a str>, &'a str, &'a Expr)], -) -> Result, &'a str, &'a Expr)>> { - let mut ordered = Vec::with_capacity(lookup_terms.len()); - for index_column in index.columns.iter().take(lookup_terms.len()) { - let Some(column_name) = index_column.column_name.as_deref() else { - return Err(DbError::internal( - "compound indexed projection matched expression index column", - )); - }; - let Some(term) = lookup_terms - .iter() - .copied() - .find(|(_, lookup_column, _)| identifiers_equal(column_name, lookup_column)) - else { - return Err(DbError::internal( - "compound indexed projection matched non-prefix lookup terms", - )); - }; - ordered.push(term); +fn resolve_dataset_column_position( + dataset: &Dataset, + table: Option<&str>, + column: &str, +) -> Result { + let mut matched_index = None; + for (index, binding) in dataset.columns.iter().enumerate() { + let visible_match = table.is_some() || !binding.hidden; + if !visible_match || !identifiers_equal(&binding.name, column) { + continue; + } + if table.is_some_and(|table| { + !binding + .table + .as_deref() + .is_some_and(|binding_table| identifiers_equal(binding_table, table)) + }) { + continue; + } + if matched_index.replace(index).is_some() { + return Err(DbError::sql(format!("ambiguous column reference {column}"))); + } } - Ok(ordered) + matched_index.ok_or_else(|| DbError::sql(format!("unknown column {column}"))) } -fn row_ids_for_simple_indexed_projection_lookup<'a>( - keys: &'a RuntimeBtreeKeys, - plan: &SimpleIndexedProjectionPlan<'_>, -) -> Result> { - if plan.extra_lookup_terms.is_empty() { - return keys - .row_ids_for_value_set(&plan.lookup_value) - .map(SimpleIndexedProjectionRowIds::Borrowed); - } - let values = std::iter::once(plan.lookup_value.clone()) - .chain( - plan.extra_lookup_terms - .iter() - .map(|(_, value)| value.clone()), - ) - .collect::>(); - Ok(SimpleIndexedProjectionRowIds::Owned(keys.row_ids_for_key( - &RuntimeBtreeKey::Encoded(RuntimeEncodedKey::from_vec(Row::new(values).encode()?)), - ))) +fn values_from_positions(row: &[Value], positions: &[usize]) -> Result> { + positions + .iter() + .map(|position| { + row.get(*position) + .cloned() + .ok_or_else(|| DbError::internal("window row is shorter than its bindings")) + }) + .collect() } -fn indexed_projection_row_id_order( - plan: &SimpleIndexedProjectionPlan<'_>, -) -> Option<(bool, usize)> { - let order_by = plan.order_by.as_ref()?; - if order_by.len() != 1 { +fn rows_preceding_current_frame(frame: Option<&crate::sql::ast::WindowFrame>) -> Option { + let frame = frame?; + if frame.unit != crate::sql::ast::WindowFrameUnit::Rows { return None; } - let row_id_alias = row_id_alias_column_name(plan.table_schema)?; - let row_id_order = &order_by[0]; - if row_id_order.collation.is_some() { + if !matches!( + frame.end.as_ref(), + None | Some(crate::sql::ast::WindowFrameBound::CurrentRow) + ) { return None; } - let projection_index = plan - .projection_indexes - .get(row_id_order.projection_index) - .copied()?; - let order_column = plan.table_schema.columns.get(projection_index)?; - if !identifiers_equal(&order_column.name, row_id_alias) { + let crate::sql::ast::WindowFrameBound::Preceding(offset) = &frame.start else { return None; - } - let limit_with_offset = plan - .limit - .map(|limit| limit.saturating_add(plan.offset)) - .unwrap_or(usize::MAX); - Some((row_id_order.descending, limit_with_offset)) + }; + let Expr::Literal(Value::Int64(offset)) = offset.as_ref() else { + return None; + }; + usize::try_from(*offset).ok() } -fn view_projection_expr_for_output_column(items: &[SelectItem], column: &str) -> Option { - for (index, item) in items.iter().enumerate() { - let SelectItem::Expr { expr, alias } = item else { - continue; +pub(crate) struct IndexedJoinGroupedCountPlan<'a> { + parent_table_name: &'a str, + parent_join_column: &'a str, + child_index_name: String, + group_column_indexes: Vec, + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, +} + +impl IndexedJoinGroupedCountPlan<'_> { + fn scalar_count_top_n_limit(&self) -> Option { + let order_by = self.order_by.as_deref()?; + let [order] = order_by else { + return None; }; - let output_name = alias - .as_deref() - .map(std::borrow::Cow::Borrowed) - .unwrap_or_else(|| std::borrow::Cow::Owned(infer_expr_name(expr, index + 1))); - if identifiers_equal(output_name.as_ref(), column) { - return Some(expr.clone()); + if self.offset != 0 + || order.projection_index != self.group_column_indexes.len() + || !order.descending + || order.collation.is_some() + { + return None; } + self.limit } - None -} - -#[derive(Clone, Copy, Debug)] -struct SimpleRangeBound<'a> { - inclusive: bool, - value_expr: &'a Expr, } -#[derive(Clone, Debug)] -pub(crate) struct SimpleRangeBoundValue { - pub(crate) inclusive: bool, - pub(crate) value: Value, +#[derive(Clone, Copy)] +struct IndexedJoinLimitTablePlan<'a> { + name: &'a str, + alias: &'a Option, } -#[derive(Clone, Debug)] -struct SimpleGroupedNumericState { - numeric_count: i64, - total_int: i64, - total_float: f64, - saw_float: bool, - saw_value: bool, +struct IndexedJoinLimitStep { + previous_table_index: usize, + previous_column_index: usize, + right_index_name: Option, } -impl SimpleGroupedNumericState { - fn add(&mut self, value: &Value) -> Result<()> { - match value { - Value::Null => Ok(()), - Value::Int64(value) => { - self.numeric_count += 1; - self.total_int += value; - self.total_float += *value as f64; - self.saw_value = true; - Ok(()) - } - Value::Float64(value) => { - self.numeric_count += 1; - self.total_float += *value; - self.saw_float = true; - self.saw_value = true; - Ok(()) - } - Value::Decimal { scaled, scale } => { - self.numeric_count += 1; - self.total_float += decimal_to_f64(*scaled, *scale); - self.saw_float = true; - self.saw_value = true; - Ok(()) - } - other => Err(DbError::sql(format!( - "numeric aggregate does not support {other:?}" - ))), - } - } - - fn value(&self, kind: SimpleGroupedNumericAggregateKind) -> Value { - match kind { - SimpleGroupedNumericAggregateKind::Sum => { - if !self.saw_value { - Value::Null - } else if self.saw_float { - Value::Float64(self.total_float) - } else { - Value::Int64(self.total_int) - } - } - SimpleGroupedNumericAggregateKind::Avg => { - if self.numeric_count == 0 { - Value::Null - } else { - Value::Float64(self.total_float / self.numeric_count as f64) - } - } - SimpleGroupedNumericAggregateKind::SumDistinct => { - if !self.saw_value { - Value::Null - } else if self.saw_float { - Value::Float64(self.total_float) - } else { - Value::Int64(self.total_int) - } - } - SimpleGroupedNumericAggregateKind::AvgDistinct => { - if self.numeric_count == 0 { - Value::Null - } else { - Value::Float64(self.total_float / self.numeric_count as f64) - } - } - SimpleGroupedNumericAggregateKind::Total - | SimpleGroupedNumericAggregateKind::TotalDistinct => { - if self.numeric_count == 0 { - Value::Float64(0.0) - } else { - Value::Float64(self.total_float) - } - } - _ => Value::Null, - } - } +struct IndexedJoinLimitProjection { + table_index: usize, + column_index: usize, + column_name: String, } -#[derive(Clone, Debug, Default)] -struct SimpleGroupedVarianceState { - count: u64, - mean: f64, - m2: f64, +pub(crate) struct IndexedJoinLimitPlan<'a> { + tables: Vec>, + steps: Vec, + projections: Vec, + limit: usize, + offset: usize, } -impl SimpleGroupedVarianceState { - fn add(&mut self, value: &Value) -> Result<()> { - let number = match value { - Value::Null => return Ok(()), - Value::Int64(value) => *value as f64, - Value::Float64(value) => *value, - Value::Decimal { scaled, scale } => decimal_to_f64(*scaled, *scale), - other => { - return Err(DbError::sql(format!( - "variance aggregate does not support {other:?}" - ))) - } - }; - self.count += 1; - let delta = number - self.mean; - self.mean += delta / (self.count as f64); - let delta2 = number - self.mean; - self.m2 += delta * delta2; - Ok(()) - } - - fn value(&self, kind: SimpleGroupedNumericAggregateKind) -> Value { - if self.count == 0 { - return Value::Null; - } - let denominator = match kind { - SimpleGroupedNumericAggregateKind::StddevPop - | SimpleGroupedNumericAggregateKind::StddevPopDistinct - | SimpleGroupedNumericAggregateKind::VarPop - | SimpleGroupedNumericAggregateKind::VarPopDistinct => self.count as f64, - SimpleGroupedNumericAggregateKind::StddevSamp - | SimpleGroupedNumericAggregateKind::StddevSampDistinct - | SimpleGroupedNumericAggregateKind::VarSamp - | SimpleGroupedNumericAggregateKind::VarSampDistinct => { - if self.count < 2 { - return Value::Null; - } - (self.count - 1) as f64 - } - _ => return Value::Null, - }; - let variance = self.m2 / denominator; - match kind { - SimpleGroupedNumericAggregateKind::StddevPop - | SimpleGroupedNumericAggregateKind::StddevPopDistinct - | SimpleGroupedNumericAggregateKind::StddevSamp - | SimpleGroupedNumericAggregateKind::StddevSampDistinct => { - Value::Float64(variance.sqrt()) - } - SimpleGroupedNumericAggregateKind::VarPop - | SimpleGroupedNumericAggregateKind::VarPopDistinct - | SimpleGroupedNumericAggregateKind::VarSamp - | SimpleGroupedNumericAggregateKind::VarSampDistinct => Value::Float64(variance), - _ => Value::Null, - } - } +pub(crate) struct BaseTableJoinPlan<'a> { + left_name: &'a str, + left_alias: Option<&'a str>, + right_name: &'a str, + right_alias: Option<&'a str>, + kind: JoinKind, + constraint: &'a JoinConstraint, + filter: Option<&'a Expr>, + projection: &'a [SelectItem], + order_by: &'a [crate::sql::ast::OrderBy], + distinct: bool, + limit: Option<&'a Expr>, + offset: Option<&'a Expr>, } -#[derive(Clone, Debug)] -struct SimpleGroupedBoolState { - saw_non_null: bool, - and_value: bool, - or_value: bool, +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum SimpleGroupedNumericAggregateKind { + CountRows, + CountNonNull, + CountDistinct, + Sum, + SumDistinct, + Avg, + AvgDistinct, + Total, + TotalDistinct, + StddevSamp, + StddevSampDistinct, + StddevPop, + StddevPopDistinct, + VarSamp, + VarSampDistinct, + VarPop, + VarPopDistinct, + BoolAnd, + BoolAndDistinct, + BoolOr, + BoolOrDistinct, + Min, + Max, } -impl Default for SimpleGroupedBoolState { - fn default() -> Self { - Self { - saw_non_null: false, - and_value: true, - or_value: false, +impl SimpleGroupedNumericAggregateKind { + fn aggregate_name(self) -> &'static str { + match self { + Self::CountRows | Self::CountNonNull | Self::CountDistinct => "count", + Self::Sum | Self::SumDistinct => "sum", + Self::Avg | Self::AvgDistinct => "avg", + Self::Total | Self::TotalDistinct => "total", + Self::StddevSamp | Self::StddevSampDistinct => "stddev", + Self::StddevPop | Self::StddevPopDistinct => "stddev_pop", + Self::VarSamp | Self::VarSampDistinct => "variance", + Self::VarPop | Self::VarPopDistinct => "var_pop", + Self::BoolAnd | Self::BoolAndDistinct => "bool_and", + Self::BoolOr | Self::BoolOrDistinct => "bool_or", + Self::Min => "min", + Self::Max => "max", } } -} -impl SimpleGroupedBoolState { - fn add(&mut self, value: &Value) -> Result<()> { - let boolean = match value { - Value::Null => return Ok(()), - Value::Bool(value) => *value, - other => { - return Err(DbError::sql(format!( - "boolean aggregate does not support {other:?}" - ))) - } - }; - self.saw_non_null = true; - self.and_value &= boolean; - self.or_value |= boolean; - Ok(()) + fn uses_distinct(self) -> bool { + matches!( + self, + Self::CountDistinct + | Self::SumDistinct + | Self::AvgDistinct + | Self::TotalDistinct + | Self::StddevSampDistinct + | Self::StddevPopDistinct + | Self::VarSampDistinct + | Self::VarPopDistinct + | Self::BoolAndDistinct + | Self::BoolOrDistinct + ) } - fn value(&self, kind: SimpleGroupedNumericAggregateKind) -> Value { - if !self.saw_non_null { - return Value::Null; - } - match kind { - SimpleGroupedNumericAggregateKind::BoolAnd - | SimpleGroupedNumericAggregateKind::BoolAndDistinct => Value::Bool(self.and_value), - SimpleGroupedNumericAggregateKind::BoolOr - | SimpleGroupedNumericAggregateKind::BoolOrDistinct => Value::Bool(self.or_value), - _ => Value::Null, + fn matches_aggregate_name(self, name: &str) -> bool { + match self { + Self::StddevSamp | Self::StddevSampDistinct => { + name.eq_ignore_ascii_case("stddev") || name.eq_ignore_ascii_case("stddev_samp") + } + Self::VarSamp | Self::VarSampDistinct => { + name.eq_ignore_ascii_case("variance") || name.eq_ignore_ascii_case("var_samp") + } + _ => name.eq_ignore_ascii_case(self.aggregate_name()), } } } #[derive(Clone, Debug)] -struct SimpleGroupedNumericAggregate { - group_values: Vec, - count: i64, - value_counts: Vec, - distinct_values: Vec>>, - numeric_states: Vec, - variance_states: Vec, - bool_states: Vec, - extreme_values: Vec, +pub(crate) struct SimpleGroupedNumericAggregateBinding { + kind: SimpleGroupedNumericAggregateKind, + projection_index: usize, + source_column_name: Option, + source_column_index: Option, + source_expr: Option, } -impl SimpleGroupedNumericAggregate { - fn new(group_values: Vec, aggregate_count: usize) -> Self { - Self { - group_values, - count: 0, - value_counts: vec![0; aggregate_count], - distinct_values: vec![BTreeSet::new(); aggregate_count], - numeric_states: vec![ - SimpleGroupedNumericState { - numeric_count: 0, - total_int: 0, - total_float: 0.0, - saw_float: false, - saw_value: false, - }; - aggregate_count - ], - variance_states: vec![SimpleGroupedVarianceState::default(); aggregate_count], - bool_states: vec![SimpleGroupedBoolState::default(); aggregate_count], - extreme_values: vec![Value::Null; aggregate_count], - } - } +#[derive(Default)] +struct SimpleScalarInt64AggregateStats { + row_count: i64, + non_null_count: i64, + total_int: i64, + total_float: f64, + min_value: Option, + max_value: Option, +} - fn add_numeric(&mut self, aggregate_index: usize, value: &Value) -> Result<()> { - self.numeric_states[aggregate_index].add(value) +impl SimpleScalarInt64AggregateStats { + fn add(&mut self, value: Option) { + self.row_count += 1; + let Some(value) = value else { + return; + }; + self.non_null_count += 1; + self.total_int += value; + self.total_float += value as f64; + self.min_value = Some(self.min_value.map_or(value, |min| min.min(value))); + self.max_value = Some(self.max_value.map_or(value, |max| max.max(value))); } - fn count_non_null(&mut self, aggregate_index: usize, value: &Value) { - if !matches!(value, Value::Null) { - self.value_counts[aggregate_index] += 1; - } + fn into_values( + self, + aggregate_bindings: &[SimpleGroupedNumericAggregateBinding], + ) -> Vec { + aggregate_bindings + .iter() + .map(|aggregate| match aggregate.kind { + SimpleGroupedNumericAggregateKind::CountRows => Value::Int64(self.row_count), + SimpleGroupedNumericAggregateKind::CountNonNull => { + Value::Int64(self.non_null_count) + } + SimpleGroupedNumericAggregateKind::Sum => { + if self.non_null_count == 0 { + Value::Null + } else { + Value::Int64(self.total_int) + } + } + SimpleGroupedNumericAggregateKind::Avg => { + if self.non_null_count == 0 { + Value::Null + } else { + Value::Float64(self.total_float / self.non_null_count as f64) + } + } + SimpleGroupedNumericAggregateKind::Min => { + self.min_value.map(Value::Int64).unwrap_or(Value::Null) + } + SimpleGroupedNumericAggregateKind::Max => { + self.max_value.map(Value::Int64).unwrap_or(Value::Null) + } + _ => Value::Null, + }) + .collect() } +} - fn count_distinct(&mut self, aggregate_index: usize, value: &Value) -> Result<()> { - if matches!(value, Value::Null) { - return Ok(()); - } - let key = row_identity(std::slice::from_ref(value))?; - if self.distinct_values[aggregate_index].insert(key) { - self.value_counts[aggregate_index] += 1; - } - Ok(()) - } +#[derive(Clone, Debug)] +struct ManifestTemplate { + schema_cookie: u32, + table_next_row_id_offsets: BTreeMap, + table_state_offsets: BTreeMap, + table_pk_index_root_offsets: BTreeMap, + bytes: Vec, +} - fn add_numeric_distinct(&mut self, aggregate_index: usize, value: &Value) -> Result<()> { - if matches!(value, Value::Null) { - return Ok(()); - } - let key = row_identity(std::slice::from_ref(value))?; - if self.distinct_values[aggregate_index].insert(key) { - self.numeric_states[aggregate_index].add(value)?; - } - Ok(()) - } +#[derive(Clone, Debug)] +pub(crate) struct ManifestEncoding { + bytes: Vec, + table_next_row_id_offsets: BTreeMap, + table_state_offsets: BTreeMap, + table_pk_index_root_offsets: BTreeMap, +} - fn add_variance(&mut self, aggregate_index: usize, value: &Value) -> Result<()> { - self.variance_states[aggregate_index].add(value) - } +#[derive(Debug)] +struct SnapshotPageStore<'a> { + pager: &'a PagerHandle, + wal: &'a WalHandle, + snapshot_lsn: u64, +} - fn add_variance_distinct(&mut self, aggregate_index: usize, value: &Value) -> Result<()> { - if matches!(value, Value::Null) { - return Ok(()); - } - let key = row_identity(std::slice::from_ref(value))?; - if self.distinct_values[aggregate_index].insert(key) { - self.variance_states[aggregate_index].add(value)?; - } - Ok(()) +impl PageStore for SnapshotPageStore<'_> { + fn page_size(&self) -> u32 { + self.pager.page_size() } - fn add_bool(&mut self, aggregate_index: usize, value: &Value) -> Result<()> { - self.bool_states[aggregate_index].add(value) + fn allocate_page(&mut self) -> Result { + Err(DbError::internal( + "snapshot page store does not support allocation", + )) } - fn add_bool_distinct(&mut self, aggregate_index: usize, value: &Value) -> Result<()> { - if matches!(value, Value::Null) { - return Ok(()); - } - let key = row_identity(std::slice::from_ref(value))?; - if self.distinct_values[aggregate_index].insert(key) { - self.bool_states[aggregate_index].add(value)?; - } - Ok(()) + fn free_page(&mut self, _page_id: PageId) -> Result<()> { + Err(DbError::internal( + "snapshot page store does not support free", + )) } - fn aggregate_value( - &self, - kind: SimpleGroupedNumericAggregateKind, - aggregate_index: usize, - ) -> Value { - match kind { - SimpleGroupedNumericAggregateKind::CountRows => Value::Int64(self.count), - SimpleGroupedNumericAggregateKind::CountNonNull - | SimpleGroupedNumericAggregateKind::CountDistinct => { - Value::Int64(self.value_counts[aggregate_index]) - } - SimpleGroupedNumericAggregateKind::Sum - | SimpleGroupedNumericAggregateKind::SumDistinct - | SimpleGroupedNumericAggregateKind::Avg - | SimpleGroupedNumericAggregateKind::AvgDistinct - | SimpleGroupedNumericAggregateKind::Total - | SimpleGroupedNumericAggregateKind::TotalDistinct => { - self.numeric_states[aggregate_index].value(kind) - } - SimpleGroupedNumericAggregateKind::StddevSamp - | SimpleGroupedNumericAggregateKind::StddevSampDistinct - | SimpleGroupedNumericAggregateKind::StddevPop - | SimpleGroupedNumericAggregateKind::StddevPopDistinct - | SimpleGroupedNumericAggregateKind::VarSamp - | SimpleGroupedNumericAggregateKind::VarSampDistinct - | SimpleGroupedNumericAggregateKind::VarPop - | SimpleGroupedNumericAggregateKind::VarPopDistinct => { - self.variance_states[aggregate_index].value(kind) - } - SimpleGroupedNumericAggregateKind::BoolAnd - | SimpleGroupedNumericAggregateKind::BoolAndDistinct - | SimpleGroupedNumericAggregateKind::BoolOr - | SimpleGroupedNumericAggregateKind::BoolOrDistinct => { - self.bool_states[aggregate_index].value(kind) - } - SimpleGroupedNumericAggregateKind::Min | SimpleGroupedNumericAggregateKind::Max => { - self.extreme_values[aggregate_index].clone() - } + fn read_page(&self, page_id: PageId) -> Result> { + if let Some(page) = + self.wal + .read_page_at_snapshot(self.pager, page_id, self.snapshot_lsn)? + { + Ok(page) + } else { + self.pager.read_page_from_disk(page_id) } } - fn into_row(self, aggregate_bindings: &[SimpleGroupedNumericAggregateBinding]) -> QueryRow { - let mut group_values = self.group_values.clone(); - for (aggregate_index, aggregate) in aggregate_bindings.iter().enumerate() { - group_values.push(self.aggregate_value(aggregate.kind, aggregate_index)); - } - QueryRow::new(group_values) + fn advise_sequential(&self) -> Result<()> { + self.pager.advise_sequential() } - fn aggregate_values( - &self, - aggregate_bindings: &[SimpleGroupedNumericAggregateBinding], - ) -> Vec { - aggregate_bindings - .iter() - .enumerate() - .map(|(aggregate_index, aggregate)| { - self.aggregate_value(aggregate.kind, aggregate_index) - }) - .collect() + fn write_page(&mut self, _page_id: PageId, _data: &[u8]) -> Result<()> { + Err(DbError::internal( + "snapshot page store does not support writes", + )) } } -#[derive(Clone, Debug)] -struct SimpleGroupedCountAggregate { - group_values: Vec, - count: i64, +struct OverflowPayloadCursor<'a, S: PageStore> { + store: &'a S, + next_page_id: PageId, + page: Option>, + chunk_offset: usize, + chunk_remaining: usize, + remaining: usize, } -impl SimpleGroupedCountAggregate { - fn new(group_values: Vec) -> Self { +impl<'a, S: PageStore> OverflowPayloadCursor<'a, S> { + fn new(store: &'a S, pointer: OverflowPointer) -> Self { Self { - group_values, - count: 0, + store, + next_page_id: pointer.head_page_id, + page: None, + chunk_offset: OVERFLOW_HEADER_SIZE, + chunk_remaining: 0, + remaining: pointer.logical_len as usize, } } - fn into_row(self) -> QueryRow { - let mut row = self.group_values; - row.push(Value::Int64(self.count)); - QueryRow::new(row) + fn read_i64(&mut self) -> Result { + let mut bytes = [0_u8; 8]; + self.read_exact(&mut bytes)?; + Ok(i64::from_le_bytes(bytes)) } - fn aggregate_values(&self) -> Vec { - vec![Value::Int64(self.count)] + fn read_u32(&mut self) -> Result { + let mut bytes = [0_u8; 4]; + self.read_exact(&mut bytes)?; + Ok(u32::from_le_bytes(bytes)) } -} -fn evaluate_simple_grouped_values( - runtime: &EngineRuntime, - exprs: &[Expr], - dataset: &Dataset, - row: &[Value], - params: &[Value], -) -> Result> { - exprs - .iter() - .map(|expr| runtime.eval_expr(expr, dataset, row, params, &BTreeMap::new(), None)) - .collect() -} + fn read_vec(&mut self, len: usize) -> Result> { + let mut bytes = vec![0_u8; len]; + self.read_exact(&mut bytes)?; + Ok(bytes) + } -fn render_simple_grouped_numeric_aggregate_groups( - runtime: &EngineRuntime, - groups: Vec, - plan: &SimpleGroupedNumericAggregatePlan<'_>, - params: &[Value], -) -> Result { - if let (Some(projection_exprs), Some(raw_projection_bindings)) = - (&plan.projection_exprs, &plan.raw_projection_bindings) - { - let raw_dataset = Dataset::with_rows(raw_projection_bindings.clone(), Vec::new()); - let mut rows = Vec::with_capacity(groups.len()); - for group in groups { - let mut raw_values = group.group_values.clone(); - raw_values.extend(group.aggregate_values(&plan.aggregate_bindings)); - if let Some(having) = plan.having.as_ref() { - if !matches!( - runtime.eval_expr( - having, - &raw_dataset, - &raw_values, - params, - &BTreeMap::new(), - None, - )?, - Value::Bool(true) - ) { - continue; - } + fn read_exact(&mut self, out: &mut [u8]) -> Result<()> { + if out.len() > self.remaining { + return Err(DbError::corruption("overflow payload length mismatch")); + } + let mut written = 0; + while written < out.len() { + self.ensure_chunk()?; + let take = (out.len() - written).min(self.chunk_remaining); + if take == 0 { + return Err(DbError::corruption("overflow payload truncated")); } - let output = projection_exprs - .iter() - .map(|expr| { - runtime.eval_expr( - expr, - &raw_dataset, - &raw_values, - params, - &BTreeMap::new(), - None, - ) - }) - .collect::>>()?; - rows.push(QueryRow::new(output)); + let page = self + .page + .as_ref() + .ok_or_else(|| DbError::corruption("overflow payload page is missing"))?; + out[written..written + take] + .copy_from_slice(&page[self.chunk_offset..self.chunk_offset + take]); + self.chunk_offset += take; + self.chunk_remaining -= take; + self.remaining -= take; + written += take; } - return runtime.apply_simple_grouped_postprocessing( - rows, - plan.column_names.clone(), - &[], - None, - params, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - ); + Ok(()) } - runtime.apply_simple_grouped_postprocessing( - groups - .into_iter() - .map(|group| group.into_row(&plan.aggregate_bindings)), - plan.column_names.clone(), - &plan.having_bindings, - plan.having.as_ref(), - params, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - ) -} - -fn render_simple_grouped_count_groups( - runtime: &EngineRuntime, - groups: Vec, - plan: &SimpleGroupedCountPlan<'_>, - params: &[Value], -) -> Result { - if let (Some(projection_exprs), Some(raw_projection_bindings)) = - (&plan.projection_exprs, &plan.raw_projection_bindings) - { - let raw_dataset = Dataset::with_rows(raw_projection_bindings.clone(), Vec::new()); - let mut rows = Vec::with_capacity(groups.len()); - for group in groups { - let mut raw_values = group.group_values.clone(); - raw_values.extend(group.aggregate_values()); - if let Some(having) = plan.having.as_ref() { - if !matches!( - runtime.eval_expr( - having, - &raw_dataset, - &raw_values, - params, - &BTreeMap::new(), - None, - )?, - Value::Bool(true) - ) { - continue; - } + fn skip(&mut self, len: usize) -> Result<()> { + if len > self.remaining { + return Err(DbError::corruption("overflow payload length mismatch")); + } + let mut skipped = 0; + while skipped < len { + self.ensure_chunk()?; + let take = (len - skipped).min(self.chunk_remaining); + if take == 0 { + return Err(DbError::corruption("overflow payload truncated")); } - let output = projection_exprs - .iter() - .map(|expr| { - runtime.eval_expr( - expr, - &raw_dataset, - &raw_values, - params, - &BTreeMap::new(), - None, - ) - }) - .collect::>>()?; - rows.push(QueryRow::new(output)); + self.chunk_offset += take; + self.chunk_remaining -= take; + self.remaining -= take; + skipped += take; } - return runtime.apply_simple_grouped_postprocessing( - rows, - plan.column_names.clone(), - &[], - None, - params, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - ); + Ok(()) + } + + fn ensure_chunk(&mut self) -> Result<()> { + while self.chunk_remaining == 0 { + if self.remaining == 0 { + return Ok(()); + } + if self.next_page_id == 0 { + return Err(DbError::corruption("overflow payload truncated")); + } + let page = self.store.read_page(self.next_page_id)?; + if page.len() < OVERFLOW_HEADER_SIZE { + return Err(DbError::corruption("overflow page shorter than header")); + } + let next_page_id = u32::from_le_bytes(page[0..4].try_into().expect("header next page")); + let chunk_len = u32::from_le_bytes(page[4..8].try_into().expect("header chunk len")); + if chunk_len == 0 { + return Err(DbError::corruption( + "overflow chunk made no progress toward logical payload length", + )); + } + let chunk_end = OVERFLOW_HEADER_SIZE + chunk_len as usize; + if chunk_end > page.len() { + return Err(DbError::corruption( + "overflow chunk length exceeds page payload", + )); + } + self.next_page_id = next_page_id; + self.page = Some(page); + self.chunk_offset = OVERFLOW_HEADER_SIZE; + self.chunk_remaining = chunk_len as usize; + } + Ok(()) } +} - runtime.apply_simple_grouped_postprocessing( - groups - .into_iter() - .map(SimpleGroupedCountAggregate::into_row), - plan.column_names.clone(), - &plan.having_bindings, - plan.having.as_ref(), - params, - plan.order_by.as_deref(), - plan.limit, - plan.offset, - ) +#[derive(Debug)] +pub(crate) struct DbTxnPageStore<'a> { + db: &'a crate::db::Db, } -fn update_simple_min_max_value(best: &mut Value, candidate: Value, is_max: bool) -> Result<()> { - if matches!(candidate, Value::Null) { - return Ok(()); +impl PageStore for DbTxnPageStore<'_> { + fn page_size(&self) -> u32 { + self.db.config().page_size } - if matches!(best, Value::Null) { - *best = candidate; - return Ok(()); + + fn allocate_page(&mut self) -> Result { + self.db.allocate_page() } - let ordering = compare_values(&candidate, best)?; - let should_replace = if is_max { - ordering == std::cmp::Ordering::Greater - } else { - ordering == std::cmp::Ordering::Less - }; - if should_replace { - *best = candidate; + + fn free_page(&mut self, page_id: PageId) -> Result<()> { + self.db.free_page(page_id) } - Ok(()) -} -fn expr_references_only_binding(expr: &Expr, binding: TableBindingRef<'_>) -> bool { - match expr { - Expr::Literal(_) | Expr::Parameter(_) => true, - Expr::Column { table, .. } => table - .as_deref() - .is_none_or(|qualifier| identifiers_equal(qualifier, binding.binding_name())), - Expr::Unary { expr, .. } - | Expr::Cast { expr, .. } - | Expr::IsNull { expr, .. } - | Expr::Collate { expr, .. } => expr_references_only_binding(expr, binding), - Expr::Binary { left, right, .. } => { - expr_references_only_binding(left, binding) - && expr_references_only_binding(right, binding) - } - Expr::Between { - expr, low, high, .. - } => { - expr_references_only_binding(expr, binding) - && expr_references_only_binding(low, binding) - && expr_references_only_binding(high, binding) - } - Expr::InList { expr, items, .. } => { - expr_references_only_binding(expr, binding) - && items - .iter() - .all(|item| expr_references_only_binding(item, binding)) - } - Expr::Like { - expr, - pattern, - escape, - .. - } => { - expr_references_only_binding(expr, binding) - && expr_references_only_binding(pattern, binding) - && escape - .as_ref() - .is_none_or(|expr| expr_references_only_binding(expr, binding)) - } - Expr::Function { args, .. } => args - .iter() - .all(|arg| expr_references_only_binding(arg, binding)), - Expr::Case { - operand, - branches, - else_expr, - } => { - operand - .as_ref() - .is_none_or(|expr| expr_references_only_binding(expr, binding)) - && branches.iter().all(|(condition, value)| { - expr_references_only_binding(condition, binding) - && expr_references_only_binding(value, binding) - }) - && else_expr - .as_ref() - .is_none_or(|expr| expr_references_only_binding(expr, binding)) - } - Expr::Row(items) => items - .iter() - .all(|item| expr_references_only_binding(item, binding)), - Expr::Aggregate { .. } - | Expr::RowNumber { .. } - | Expr::WindowFunction { .. } - | Expr::InSubquery { .. } - | Expr::CompareSubquery { .. } - | Expr::ScalarSubquery(_) - | Expr::Exists(_) => false, + fn read_page(&self, page_id: PageId) -> Result> { + self.db.read_page_in_write_txn(page_id) } -} -fn expr_references_binding_names(expr: &Expr, table_name: &str, binding_name: &str) -> bool { - match expr { - Expr::Literal(_) | Expr::Parameter(_) => true, - Expr::Column { table, .. } => table.as_deref().is_none_or(|qualifier| { - identifiers_equal(qualifier, table_name) || identifiers_equal(qualifier, binding_name) - }), - Expr::Unary { expr, .. } - | Expr::Cast { expr, .. } - | Expr::IsNull { expr, .. } - | Expr::Collate { expr, .. } => { - expr_references_binding_names(expr, table_name, binding_name) - } - Expr::Binary { left, right, .. } => { - expr_references_binding_names(left, table_name, binding_name) - && expr_references_binding_names(right, table_name, binding_name) - } - Expr::Between { - expr, low, high, .. - } => { - expr_references_binding_names(expr, table_name, binding_name) - && expr_references_binding_names(low, table_name, binding_name) - && expr_references_binding_names(high, table_name, binding_name) - } - Expr::InList { expr, items, .. } => { - expr_references_binding_names(expr, table_name, binding_name) - && items - .iter() - .all(|item| expr_references_binding_names(item, table_name, binding_name)) - } - Expr::Like { - expr, - pattern, - escape, - .. - } => { - expr_references_binding_names(expr, table_name, binding_name) - && expr_references_binding_names(pattern, table_name, binding_name) - && escape.as_ref().is_none_or(|expr| { - expr_references_binding_names(expr, table_name, binding_name) - }) - } - Expr::Function { args, .. } => args - .iter() - .all(|arg| expr_references_binding_names(arg, table_name, binding_name)), - Expr::Case { - operand, - branches, - else_expr, - } => { - operand - .as_ref() - .is_none_or(|expr| expr_references_binding_names(expr, table_name, binding_name)) - && branches.iter().all(|(condition, value)| { - expr_references_binding_names(condition, table_name, binding_name) - && expr_references_binding_names(value, table_name, binding_name) - }) - && else_expr.as_ref().is_none_or(|expr| { - expr_references_binding_names(expr, table_name, binding_name) - }) - } - Expr::Row(items) => items - .iter() - .all(|item| expr_references_binding_names(item, table_name, binding_name)), - Expr::Aggregate { .. } - | Expr::RowNumber { .. } - | Expr::WindowFunction { .. } - | Expr::InSubquery { .. } - | Expr::CompareSubquery { .. } - | Expr::ScalarSubquery(_) - | Expr::Exists(_) => false, + fn advise_sequential(&self) -> Result<()> { + self.db.advise_sequential() + } + + fn write_page(&mut self, page_id: PageId, data: &[u8]) -> Result<()> { + self.db.write_page(page_id, data) + } + + fn write_page_owned(&mut self, page_id: PageId, data: Vec) -> Result<()> { + self.db.write_page_owned(page_id, data) } } -fn expr_resolves_against_dataset(expr: &Expr, dataset: &Dataset) -> bool { - match expr { - Expr::Literal(_) | Expr::Parameter(_) => true, - Expr::Column { table, column } => { - simple_select_item_column_index(dataset, table.as_deref(), column).is_some() - } - Expr::Unary { expr, .. } - | Expr::Cast { expr, .. } - | Expr::IsNull { expr, .. } - | Expr::Collate { expr, .. } => expr_resolves_against_dataset(expr, dataset), - Expr::Binary { left, right, .. } => { - expr_resolves_against_dataset(left, dataset) - && expr_resolves_against_dataset(right, dataset) - } - Expr::Between { - expr, low, high, .. - } => { - expr_resolves_against_dataset(expr, dataset) - && expr_resolves_against_dataset(low, dataset) - && expr_resolves_against_dataset(high, dataset) - } - Expr::InList { expr, items, .. } => { - expr_resolves_against_dataset(expr, dataset) - && items - .iter() - .all(|item| expr_resolves_against_dataset(item, dataset)) - } - Expr::Like { - expr, - pattern, - escape, - .. - } => { - expr_resolves_against_dataset(expr, dataset) - && expr_resolves_against_dataset(pattern, dataset) - && escape - .as_ref() - .is_none_or(|expr| expr_resolves_against_dataset(expr, dataset)) - } - Expr::Function { args, .. } => args +fn covering_payloads_for_index( + index: &IndexSchema, + table: &TableSchema, +) -> Option { + let columns = covering_payload_column_names(index, table)?; + Some(RuntimeCoveringPayloads::new(columns)) +} + +fn covering_payload_column_names(index: &IndexSchema, table: &TableSchema) -> Option> { + if index.kind != IndexKind::Btree + || index.predicate_sql.is_some() + || index.include_columns.is_empty() + || !generated_columns_are_stored(table) + { + return None; + } + + let mut columns: Vec = Vec::new(); + for column in index + .columns + .iter() + .map(|column| column.column_name.as_deref()) + .chain( + index + .include_columns + .iter() + .map(|column| Some(column.as_str())), + ) + { + let column = column?; + schema_column_index(table, column)?; + if !columns .iter() - .all(|arg| expr_resolves_against_dataset(arg, dataset)), - Expr::Case { - operand, - branches, - else_expr, - } => { - operand - .as_ref() - .is_none_or(|expr| expr_resolves_against_dataset(expr, dataset)) - && branches.iter().all(|(condition, value)| { - expr_resolves_against_dataset(condition, dataset) - && expr_resolves_against_dataset(value, dataset) - }) - && else_expr - .as_ref() - .is_none_or(|expr| expr_resolves_against_dataset(expr, dataset)) + .any(|existing| identifiers_equal(existing, column)) + { + columns.push(column.to_string()); } - Expr::Row(items) => items - .iter() - .all(|item| expr_resolves_against_dataset(item, dataset)), - Expr::Aggregate { .. } - | Expr::RowNumber { .. } - | Expr::WindowFunction { .. } - | Expr::InSubquery { .. } - | Expr::CompareSubquery { .. } - | Expr::ScalarSubquery(_) - | Expr::Exists(_) => false, + } + if columns.is_empty() { + None + } else { + Some(columns) } } -fn simple_join_projection_eval_bindings( - left_table_name: &str, - left_alias: &Option, - left_schema: &TableSchema, - right_table_name: &str, - right_alias: &Option, - right_schema: &TableSchema, -) -> Vec { - let left_binding = left_alias.as_deref().unwrap_or(left_table_name); - let right_binding = right_alias.as_deref().unwrap_or(right_table_name); - let mut bindings = Vec::with_capacity(left_schema.columns.len() + right_schema.columns.len()); - bindings.extend(left_schema.columns.iter().map(|column| { - ColumnBinding::visible_source( - Some(left_binding.to_string()), - Some(left_table_name.to_string()), - column.name.clone(), - ) - })); - bindings.extend(right_schema.columns.iter().map(|column| { - ColumnBinding::visible_source( - Some(right_binding.to_string()), - Some(right_table_name.to_string()), - column.name.clone(), - ) - })); - bindings +fn covering_payload_values_for_row( + index: &IndexSchema, + table: &TableSchema, + row_values: &[Value], +) -> Option> { + let columns = covering_payload_column_names(index, table)?; + columns + .iter() + .map(|column| { + schema_column_index(table, column) + .and_then(|offset| row_values.get(offset)) + .cloned() + }) + .collect() } -fn grouped_projection_expr_matches_group_expr( - projection_expr: &Expr, - group_expr: &Expr, - binding: TableBindingRef<'_>, +fn preserve_resident_payload_offsets_for_delete_tombstones( + config: &crate::config::DbConfig, ) -> bool { - if projection_expr == group_expr { - return true; + !config.paged_row_storage && !config.persistent_pk_index +} + +/// Returns the row-position of the single indexed column for a plain +/// single-column BTREE index (no expression, no INCLUDE columns), so the +/// bulk-build fast path can encode the key directly from the borrowed row +/// slice without cloning the indexed `Value` or re-resolving the column on +/// every row. Returns `None` for composite, expression, or covering indexes. +fn single_plain_index_column_position(index: &IndexSchema, table: &TableSchema) -> Option { + if !index.include_columns.is_empty() { + return None; } - matches!(group_expr, Expr::Column { column, .. } if expr_matches_binding_column(projection_expr, binding, column)) + let [column] = index.columns.as_slice() else { + return None; + }; + let column_name = column.column_name.as_deref()?; + if column.expression_sql.is_some() { + return None; + } + column_position(table, column_name) } -fn simple_grouped_projection_bindings( - group_count: usize, - aggregate_count: usize, -) -> Vec { - let mut bindings = Vec::with_capacity(group_count + aggregate_count); - bindings.extend( - (0..group_count).map(|index| { - ColumnBinding::visible(None, format!("__grouped_projection_group_{index}")) - }), - ); - bindings - .extend((0..aggregate_count).map(|index| { - ColumnBinding::visible(None, format!("__grouped_projection_agg_{index}")) - })); - bindings +/// Resolves the stored-column positions for a btree index whose columns are +/// all plain stored columns (no expressions, no INCLUDE columns, no virtual +/// generated columns). Used by the build loop to read index key values +/// directly by position without building a `Dataset`. +pub(crate) fn plain_index_column_positions( + index: &IndexSchema, + table: &TableSchema, +) -> Option> { + if !index.include_columns.is_empty() { + return None; + } + if index.columns.is_empty() { + return None; + } + let stored_generated_ok = generated_columns_are_stored(table); + let mut positions = Vec::with_capacity(index.columns.len()); + for column in &index.columns { + if column.expression_sql.is_some() { + return None; + } + let Some(column_name) = &column.column_name else { + return None; + }; + let position = column_position(table, column_name)?; + if !stored_generated_ok + && table + .columns + .get(position) + .is_some_and(|col| col.generated_sql.is_some() && !col.generated_stored) + { + return None; + } + positions.push(position); + } + Some(positions) } -fn simple_grouped_projection_group_index( - expr: &Expr, - group_exprs: &[Expr], - table_binding: TableBindingRef<'_>, +/// Resolves the single stored-column position for a trigram index over a +/// plain TEXT column (no expression, no INCLUDE columns, no predicate, no +/// virtual generated column). Returns `None` for any unsupported shape so +/// the trigram build loop falls back to `compute_index_values`. +pub(super) fn plain_single_text_index_column_position( + index: &IndexSchema, + table: &TableSchema, ) -> Option { - let mut matched = None; - for (index, group_expr) in group_exprs.iter().enumerate() { - if !grouped_projection_expr_matches_group_expr(expr, group_expr, table_binding) { - continue; - } - if matched.replace(index).is_some() { + if !index.include_columns.is_empty() || index.predicate_sql.is_some() { + return None; + } + if index.columns.len() != 1 { + return None; + } + plain_index_column_positions(index, table)? + .into_iter() + .next() +} + +/// Resolves the stored-column positions for a fulltext index over plain TEXT +/// columns (no expressions, no INCLUDE columns, no predicate, no virtual +/// generated columns). Returns `None` for any unsupported shape so the +/// fulltext build loop falls back to `full_text_fields_for_row`. +fn plain_text_index_column_positions( + index: &IndexSchema, + table: &TableSchema, +) -> Option> { + if index.predicate_sql.is_some() { + return None; + } + let positions = plain_index_column_positions(index, table)?; + // Confirm every indexed column is actually TEXT so the fast path matches + // full_text_fields_for_row's TEXT requirement. + for position in &positions { + let column = table.columns.get(*position)?; + if column.column_type != ColumnType::Text { return None; } } - matched + Some(positions) +} + +pub(super) fn spatial_index_backend( + index: &IndexSchema, + table: &TableSchema, +) -> Result { + let column_index = spatial_index_column_index(index, table)?; + match table.columns[column_index].column_type { + ColumnType::Geography => Ok(SpatialIndexBackend::GeographyS2), + ColumnType::Geometry => Ok(SpatialIndexBackend::GeometryQuadCell), + _ => Err(DbError::internal(format!( + "SPATIAL index {} targets a non-spatial column", + index.name + ))), + } +} + +pub(super) fn spatial_index_value_for_row( + runtime: &EngineRuntime, + index: &IndexSchema, + table: &TableSchema, + row_values: &[Value], +) -> Result> { + if !row_satisfies_index_predicate(runtime, index, table, row_values)? { + return Ok(None); + } + let column_index = spatial_index_column_index(index, table)?; + let value = row_values + .get(column_index) + .ok_or_else(|| DbError::internal("row is shorter than table schema"))?; + let Some((is_geography, spatial)) = spatial_value_from_db(value)? else { + return Ok(None); + }; + match (table.columns[column_index].column_type, is_geography) { + (ColumnType::Geography, true) | (ColumnType::Geometry, false) => Ok(Some(spatial)), + (ColumnType::Geography, false) => Err(DbError::constraint(format!( + "SPATIAL index {} expected GEOGRAPHY values", + index.name + ))), + (ColumnType::Geometry, true) => Err(DbError::constraint(format!( + "SPATIAL index {} expected GEOMETRY values", + index.name + ))), + _ => Err(DbError::internal(format!( + "SPATIAL index {} targets a non-spatial column", + index.name + ))), + } +} + +fn spatial_index_column_index(index: &IndexSchema, table: &TableSchema) -> Result { + if index.kind != IndexKind::Spatial || index.columns.len() != 1 { + return Err(DbError::internal(format!( + "SPATIAL index {} must target exactly one column", + index.name + ))); + } + let column_name = index.columns[0].column_name.as_deref().ok_or_else(|| { + DbError::internal(format!( + "SPATIAL index {} must target a plain column", + index.name + )) + })?; + if index.columns[0].expression_sql.is_some() { + return Err(DbError::internal(format!( + "SPATIAL index {} cannot target an expression", + index.name + ))); + } + table + .columns + .iter() + .position(|entry| identifiers_equal(&entry.name, column_name)) + .ok_or_else(|| DbError::constraint(format!("index column {} does not exist", column_name))) +} + +fn btree_uses_typed_int64_keys(index: &IndexSchema, table: &TableSchema) -> bool { + let [column] = index.columns.as_slice() else { + return false; + }; + if column.expression_sql.is_some() { + return false; + } + let Some(column_name) = &column.column_name else { + return false; + }; + column_schema(table, column_name).is_some_and(|column| { + column.column_type == crate::catalog::ColumnType::Int64 && !column.nullable + }) } -fn rewrite_simple_grouped_output_expr( - expr: &Expr, - group_exprs: &[Expr], - table_name: &str, - binding_name: &str, - table_binding: TableBindingRef<'_>, - synthetic_names: &[String], - aggregate_bindings: &[SimpleGroupedNumericAggregateBinding], -) -> Option { - let group_count = group_exprs.len(); - if let Some(index) = simple_grouped_projection_group_index(expr, group_exprs, table_binding) { - return Some(Expr::Column { - table: None, - column: synthetic_names[index].clone(), - }); +fn btree_uses_typed_uuid_keys(index: &IndexSchema, table: &TableSchema) -> bool { + let [column] = index.columns.as_slice() else { + return false; + }; + if column.expression_sql.is_some() { + return false; } + let Some(column_name) = &column.column_name else { + return false; + }; + column_schema(table, column_name).is_some_and(|column| { + column.column_type == crate::catalog::ColumnType::Uuid && !column.nullable + }) +} - match expr { - Expr::Literal(_) | Expr::Parameter(_) => Some(expr.clone()), - Expr::Unary { op, expr } => Some(Expr::Unary { - op: *op, - expr: Box::new(rewrite_simple_grouped_output_expr( - expr, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?), - }), - Expr::Binary { left, op, right } => Some(Expr::Binary { - left: Box::new(rewrite_simple_grouped_output_expr( - left, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?), - op: *op, - right: Box::new(rewrite_simple_grouped_output_expr( - right, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?), - }), - Expr::Between { - expr, - low, - high, - negated, - } => Some(Expr::Between { - expr: Box::new(rewrite_simple_grouped_output_expr( - expr, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?), - low: Box::new(rewrite_simple_grouped_output_expr( - low, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?), - high: Box::new(rewrite_simple_grouped_output_expr( - high, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?), - negated: *negated, - }), - Expr::InList { - expr, - items, - negated, - } => Some(Expr::InList { - expr: Box::new(rewrite_simple_grouped_output_expr( - expr, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?), - items: items - .iter() - .map(|item| { - rewrite_simple_grouped_output_expr( - item, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - ) - }) - .collect::>>()?, - negated: *negated, - }), - Expr::Like { - expr, - pattern, - escape, - case_insensitive, - negated, - } => Some(Expr::Like { - expr: Box::new(rewrite_simple_grouped_output_expr( - expr, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?), - pattern: Box::new(rewrite_simple_grouped_output_expr( - pattern, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?), - escape: match escape.as_ref() { - Some(expr) => Some(Box::new(rewrite_simple_grouped_output_expr( - expr, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?)), - None => None, - }, - case_insensitive: *case_insensitive, - negated: *negated, - }), - Expr::IsNull { expr, negated } => Some(Expr::IsNull { - expr: Box::new(rewrite_simple_grouped_output_expr( - expr, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?), - negated: *negated, - }), - Expr::Collate { expr, collation } => Some(Expr::Collate { - expr: Box::new(rewrite_simple_grouped_output_expr( - expr, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?), - collation: collation.clone(), - }), - Expr::Function { name, args } => Some(Expr::Function { - name: name.clone(), - args: args - .iter() - .map(|arg| { - rewrite_simple_grouped_output_expr( - arg, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - ) - }) - .collect::>>()?, - }), - Expr::Case { - operand, - branches, - else_expr, - } => Some(Expr::Case { - operand: match operand.as_ref() { - Some(expr) => Some(Box::new(rewrite_simple_grouped_output_expr( - expr, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?)), - None => None, - }, - branches: branches - .iter() - .map(|(condition, value)| { - Some(( - rewrite_simple_grouped_output_expr( - condition, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?, - rewrite_simple_grouped_output_expr( - value, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?, - )) - }) - .collect::>>()?, - else_expr: match else_expr.as_ref() { - Some(expr) => Some(Box::new(rewrite_simple_grouped_output_expr( - expr, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?)), - None => None, - }, - }), - Expr::Row(items) => Some(Expr::Row( - items - .iter() - .map(|item| { - rewrite_simple_grouped_output_expr( - item, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - ) - }) - .collect::>>()?, - )), - Expr::Cast { expr, target_type } => Some(Expr::Cast { - expr: Box::new(rewrite_simple_grouped_output_expr( - expr, - group_exprs, - table_name, - binding_name, - table_binding, - synthetic_names, - aggregate_bindings, - )?), - target_type: *target_type, - }), - Expr::Aggregate { .. } => { - let index = aggregate_bindings.iter().position(|binding| { - matching_simple_grouped_aggregate_binding( - expr, - table_name, - binding_name, - std::slice::from_ref(binding), - ) - .is_some() - })?; - Some(Expr::Column { - table: None, - column: synthetic_names[group_count + index].clone(), - }) - } - Expr::Column { .. } - | Expr::RowNumber { .. } - | Expr::WindowFunction { .. } - | Expr::InSubquery { .. } - | Expr::CompareSubquery { .. } - | Expr::ScalarSubquery(_) - | Expr::Exists(_) => None, +pub(super) fn full_text_fields_for_row( + runtime: &EngineRuntime, + index: &IndexSchema, + table: &TableSchema, + row_values: &[Value], +) -> Result>> { + if !row_satisfies_index_predicate(runtime, index, table, row_values)? { + return Ok(Vec::new()); } + compute_index_values(runtime, index, table, row_values)? + .into_iter() + .map(|value| match value { + Value::Text(text) => Ok(Some(text)), + Value::Null => Ok(None), + other => Err(DbError::constraint(format!( + "fulltext index requires TEXT columns, got {other:?}" + ))), + }) + .collect() } -#[derive(Clone, Debug)] -struct BenchmarkReportAggregate { - item_name: String, - quantity_total: i64, - revenue_total: f64, +pub(super) fn row_satisfies_index_predicate( + runtime: &EngineRuntime, + index: &IndexSchema, + table: &TableSchema, + row_values: &[Value], +) -> Result { + row_satisfies_index_predicate_with_expr(runtime, index, table, row_values, None) } -impl BenchmarkReportAggregate { - fn new(item_name: String) -> Self { - Self { - item_name, - quantity_total: 0, - revenue_total: 0.0, +/// Like [`row_satisfies_index_predicate`], but accepts an optional pre-parsed +/// predicate expression. When provided, the predicate SQL is not re-parsed for +/// every row, which can dominate wall time for bulk DML on tables with partial +/// or expression-indexed indexes. +pub(super) fn row_satisfies_index_predicate_with_expr( + runtime: &EngineRuntime, + index: &IndexSchema, + table: &TableSchema, + row_values: &[Value], + pre_parsed_predicate: Option<&Expr>, +) -> Result { + let Some(predicate_sql) = &index.predicate_sql else { + return Ok(true); + }; + let expr_owned; + let expr = match pre_parsed_predicate { + Some(expr) => expr, + None => { + expr_owned = crate::sql::parser::parse_expression_sql(predicate_sql)?; + &expr_owned } + }; + if let Some(result) = simple_stored_column_eq_literal_predicate(table, row_values, expr)? { + return Ok(result); } + let row_materialized = if generated_columns_are_stored(table) { + Cow::Borrowed(row_values) + } else { + let mut materialized = row_values.to_vec(); + runtime.apply_virtual_generated_columns(table, &mut materialized)?; + Cow::Owned(materialized) + }; + let row_for_eval = row_materialized.as_ref(); + let dataset = table_row_dataset(table, row_for_eval, &table.name); + let bindings = dataset.rows.first().map(Vec::as_slice).unwrap_or(&[]); + Ok(matches!( + runtime.eval_expr(expr, &dataset, bindings, &[], &BTreeMap::new(), None)?, + Value::Bool(true) + )) } -#[derive(Clone, Debug)] -struct SimpleOrderByPlan { - projection_index: usize, - descending: bool, - collation: Option, -} - -#[derive(Clone, Copy, Debug)] -enum SimpleExpressionProjectionSource<'a> { - Column(usize), - Expr(&'a Expr), -} - -#[derive(Clone, Debug)] -struct SimpleExpressionProjectionPlan<'a> { - sources: Vec>, - column_names: Vec, +/// Pre-parse an index's predicate expression once. Returns `Ok(None)` if the +/// index has no predicate. The returned `Expr` can be reused across many rows +/// to avoid re-parsing the predicate SQL on every per-row index update. +pub(super) fn prepare_index_predicate_expr(index: &IndexSchema) -> Result> { + let Some(predicate_sql) = &index.predicate_sql else { + return Ok(None); + }; + Ok(Some(crate::sql::parser::parse_expression_sql( + predicate_sql, + )?)) } -#[derive(Clone, Debug)] -enum SimpleJoinProjectionSource { - Left(usize), - Right(usize), - Expr(Expr), +pub(crate) fn row_satisfies_expression( + runtime: &EngineRuntime, + table_name: &str, + column_names: &[String], + row_values: &[Value], + expr: &Expr, +) -> Result { + if column_names.len() != row_values.len() { + return Err(DbError::internal( + "row filter evaluation received mismatched column/value counts", + )); + } + let dataset = Dataset::with_rows( + column_names + .iter() + .map(|column| ColumnBinding::visible(Some(table_name.to_string()), column.clone())) + .collect(), + vec![row_values.to_vec()], + ); + let row = dataset.rows.first().map(Vec::as_slice).unwrap_or(&[]); + Ok(matches!( + runtime.eval_expr(expr, &dataset, row, &[], &BTreeMap::new(), None)?, + Value::Bool(true) + )) } -#[derive(Clone, Debug)] -struct SimpleRangeProjectionFilter<'a> { - table: Option<&'a str>, - column: &'a str, - lower: Option>, - upper: Option>, - residual: Vec>, +pub(super) fn table_row_dataset(table: &TableSchema, row: &[Value], table_name: &str) -> Dataset { + Dataset::with_rows( + table + .columns + .iter() + .map(|column| ColumnBinding::visible(Some(table_name.to_string()), column.name.clone())) + .collect(), + vec![row.to_vec()], + ) } -#[derive(Clone, Copy, Debug)] -struct SimpleResidualFilterTerm<'a> { - table: Option<&'a str>, - column: &'a str, - op: BinaryOp, - value_expr: &'a Expr, +fn table_bindings_with_hidden_row_id(table: &TableSchema, table_name: &str) -> Vec { + let mut columns = table + .columns + .iter() + .map(|column| { + ColumnBinding::visible_source( + Some(table_name.to_string()), + Some(table.name.clone()), + column.name.clone(), + ) + }) + .collect::>(); + columns.push(ColumnBinding::hidden_source( + Some(table_name.to_string()), + Some(table.name.clone()), + FTS_HIDDEN_ROW_ID_COLUMN.to_string(), + )); + columns } -#[derive(Clone, Debug)] -struct SimpleResidualPlan { - column_index: usize, - op: BinaryOp, - value: Value, +fn encoded_table_row_len(row: &StoredRow, scratch: &mut Vec) -> Result { + scratch.clear(); + Row::encode_values_into(&row.values, scratch)?; + Ok(8usize + .saturating_add(4) + .saturating_add(scratch.len()) + .saturating_add(TABLE_PAYLOAD_ROW_BODY_PADDING_BYTES)) } -fn simple_residual_matches(candidate: &Value, plan: &SimpleResidualPlan) -> Result { - // SQL three-valued logic: any comparison with a NULL operand yields - // NULL (unknown), which a WHERE treats as false. Mirror the generic - // executor's NULL short-circuit (eval_binary, expressions.rs) so the - // residual fast path never includes rows that the generic path would - // exclude for `col <> v`, `col < v`, `col <= v` on a NULL candidate. - if matches!(candidate, Value::Null) || matches!(plan.value, Value::Null) { +fn resident_table_should_use_paged_storage( + data: &TableData, + previous_state: PersistedTableState, + delta: &PagedMutationDelta, + page_size: u32, +) -> Result { + if data.rows.is_empty() { return Ok(false); } - // Incompatible-type comparisons return Err from compare_values. The - // generic executor may coerce some of these; rather than aborting the - // query on the fast path, treat the term as not satisfied so the row is - // excluded consistently with a WHERE that cannot match. - let ordering = if let Some(ordering) = simple_fast_compare_values(candidate, &plan.value) { - ordering + + let target_chunk_bytes = paged_table_target_chunk_bytes(page_size); + if previous_state.pointer.head_page_id != 0 + && previous_state.pointer.logical_len as usize > target_chunk_bytes + { + return Ok(true); + } + + let append_only = delta.append_count > 0 + && delta.updated_rows.is_empty() + && delta.deleted_rows.is_empty() + && !data.has_tombstoned_rows(); + let mut encoded_len = if append_only && previous_state.pointer.head_page_id != 0 { + previous_state.pointer.logical_len as usize } else { - let Ok(ordering) = compare_values(candidate, &plan.value) else { - return Ok(false); - }; - ordering + TABLE_PAYLOAD_MAGIC.len() + 4 }; - let truthy = match plan.op { - BinaryOp::Eq => ordering == std::cmp::Ordering::Equal, - BinaryOp::NotEq => ordering != std::cmp::Ordering::Equal, - BinaryOp::Gt => ordering == std::cmp::Ordering::Greater, - BinaryOp::GtEq => ordering != std::cmp::Ordering::Less, - BinaryOp::Lt => ordering == std::cmp::Ordering::Less, - BinaryOp::LtEq => ordering != std::cmp::Ordering::Greater, - _ => false, + let start = if append_only && previous_state.pointer.head_page_id != 0 { + data.rows.len().saturating_sub(delta.append_count) + } else { + 0 }; - Ok(truthy) + let mut scratch = Vec::with_capacity(64); + for row in &data.rows[start..] { + encoded_len = encoded_len.saturating_add(encoded_table_row_len(row, &mut scratch)?); + if encoded_len > target_chunk_bytes { + return Ok(true); + } + } + Ok(false) } -fn simple_fast_compare_values(left: &Value, right: &Value) -> Option { - match (left, right) { - (Value::Int64(left), Value::Int64(right)) => Some(left.cmp(right)), - (Value::Float64(left), Value::Float64(right)) => Some(left.total_cmp(right)), - (Value::DateDays(left), Value::DateDays(right)) => Some(left.cmp(right)), - (Value::TimestampMicros(left), Value::TimestampMicros(right)) => Some(left.cmp(right)), - (Value::TimeMicros(left), Value::TimeMicros(right)) => Some(left.cmp(right)), - (Value::TimestampTzMicros(left), Value::TimestampTzMicros(right)) => Some(left.cmp(right)), - _ => None, +fn finalize_encoded_paged_table_chunk( + mut payload: Vec, + row_count: usize, +) -> Result { + if payload.len() < TABLE_PAYLOAD_MAGIC.len() + 4 { + return Err(DbError::internal( + "paged table chunk payload shorter than header", + )); } + payload[TABLE_PAYLOAD_MAGIC.len()..TABLE_PAYLOAD_MAGIC.len() + 4].copy_from_slice( + &u32::try_from(row_count) + .map_err(|_| DbError::constraint("paged table chunk row count exceeds u32"))? + .to_le_bytes(), + ); + let checksum = crc32c_parts(&[payload.as_slice()]); + Ok(EncodedPagedTableChunk { + payload, + checksum, + row_count, + }) } -fn simple_residual_matches_all( - values: &[Value], - residual_plans: &[SimpleResidualPlan], -) -> Result { - if residual_plans.is_empty() { - return Ok(true); +fn read_paged_table_chunk_payloads( + store: &S, + state: PersistedTableState, +) -> Result> { + if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { + return Ok(Vec::new()); } - for plan in residual_plans { - let Some(candidate) = values.get(plan.column_index) else { - return Ok(false); - }; - if !simple_residual_matches(candidate, plan)? { - return Ok(false); + let manifest_payload = read_overflow(store, state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); + } + let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + let mut chunks = Vec::with_capacity(manifest.chunks.len()); + let mut total_row_count = 0usize; + for chunk in manifest.chunks { + let payload = Arc::new(read_overflow(store, chunk.pointer)?); + if crc32c_parts(&[payload.as_slice()]) != chunk.checksum { + return Err(DbError::corruption("paged table chunk checksum mismatch")); + } + let tombstoned_row_ids = Arc::new( + chunk + .tombstoned_row_ids + .iter() + .copied() + .collect::>(), + ); + let mut overlay_payload = None; + if let Some(overlay_pointer) = chunk.overlay_pointer { + let p = Arc::new(read_overflow(store, overlay_pointer)?); + if Some(crc32c_parts(&[p.as_slice()])) != chunk.overlay_checksum { + return Err(DbError::corruption( + "paged table overlay chunk checksum mismatch", + )); + } + overlay_payload = Some(p); } + total_row_count = total_row_count.saturating_add(chunk.row_count); + chunks.push(TablePageManifestChunk { + pointer: chunk.pointer, + checksum: chunk.checksum, + row_count: chunk.row_count, + payload, + tombstoned_row_ids, + overlay_pointer: chunk.overlay_pointer, + overlay_checksum: chunk.overlay_checksum, + overlay_payload, + }); } - Ok(true) -} - -fn simple_range_projection_filter(filter: &Expr) -> Option> { - let mut state = SimpleRangeFilterState::default(); - collect_simple_range_projection_terms(filter, &mut state)?; - Some(SimpleRangeProjectionFilter { - table: state.table, - column: state.column?, - lower: state.lower, - upper: state.upper, - residual: state.residual, - }) -} - -fn residual_like_filter_can_use_direct_scan(filter: &Expr) -> bool { - simple_contains_like_projection_filter(filter).is_some() -} - -fn simple_contains_like_projection_filter(filter: &Expr) -> Option<(Option<&str>, &str, &str)> { - let Expr::Like { - expr, - pattern, - escape: None, - case_insensitive: false, - negated: false, - } = filter - else { - return None; - }; - let Expr::Column { table, column } = expr.as_ref() else { - return None; - }; - let Expr::Literal(Value::Text(pattern)) = pattern.as_ref() else { - return None; - }; - let literal = simple_contains_like_literal(pattern)?; - Some((table.as_deref(), column.as_str(), literal)) -} - -fn simple_contains_like_literal(pattern: &str) -> Option<&str> { - let literal = pattern.strip_prefix('%')?.strip_suffix('%')?; - if literal.is_empty() || literal.bytes().any(|byte| matches!(byte, b'%' | b'_')) { - return None; + if state.row_count != 0 && total_row_count != state.row_count { + return Err(DbError::corruption( + "paged table manifest row count mismatch", + )); } - Some(literal) + Ok(chunks) } -#[derive(Clone, Debug, Default)] -struct SimpleRangeFilterState<'a> { - table: Option<&'a str>, - column: Option<&'a str>, - lower: Option>, - upper: Option>, - residual: Vec>, +fn visit_table_payload_rows_from_bytes(bytes: &[u8], visitor: &mut F) -> Result +where + F: FnMut(i64, &[Value]) -> Result<()>, +{ + if bytes.is_empty() { + return Ok(0); + } + let mut cursor = Cursor::new(bytes); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + let mut visited = 0usize; + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes = cursor.read_slice(row_bytes_len)?; + if is_tombstone { + continue; + } + let row = Row::decode(row_bytes)?; + visitor(row_id, row.values())?; + visited += 1; + } + Ok(visited) } -fn collect_simple_range_projection_terms<'a>( - filter: &'a Expr, - state: &mut SimpleRangeFilterState<'a>, -) -> Option<()> { - match filter { - Expr::Binary { - left, - op: BinaryOp::And, - right, - } => { - collect_simple_range_projection_terms(left, state)?; - collect_simple_range_projection_terms(right, state)?; - Some(()) - } - Expr::Binary { left, op, right } => { - let bound = simple_range_projection_bound(left, *op, right) - .or_else(|| simple_range_projection_bound(right, reverse_binary_op(*op)?, left)); - if let Some((table, column, bound_kind, value_expr)) = bound { - // Determine whether this term is on the same column as the - // range column we are building. If it is on a different - // column, do not bail; fall through to the residual handling - // below so conjunctive filters like - // `rating BETWEEN 7.5 AND 9.0 AND runtime_minutes > 120` can - // still use the filtered projection fast path with a residual - // predicate instead of falling back to the generic executor. - let same_as_range_column = state - .column - .is_some_and(|existing| identifiers_equal(existing, column)); - if same_as_range_column { - if let Some(existing_table) = state.table { - if Some(existing_table) != table { - return None; - } - } else { - state.table = table; - } - match bound_kind { - SimpleRangeBoundKind::Lower(inclusive) => { - if state.lower.is_some() { - return None; - } - state.lower = Some(SimpleRangeBound { - inclusive, - value_expr, - }); - } - SimpleRangeBoundKind::Upper(inclusive) => { - if state.upper.is_some() { - return None; - } - state.upper = Some(SimpleRangeBound { - inclusive, - value_expr, - }); - } - } - return Some(()); - } - if state.column.is_some() { - // A range column is already chosen and this term is on a - // different column; treat it as a residual below. - } else { - // No range column chosen yet and this term is a range - // bound; claim it as the range column. - if let Some(existing_table) = state.table { - if Some(existing_table) != table { - return None; - } - } else { - state.table = table; - } - state.column = Some(column); - match bound_kind { - SimpleRangeBoundKind::Lower(inclusive) => { - if state.lower.is_some() { - return None; - } - state.lower = Some(SimpleRangeBound { - inclusive, - value_expr, - }); - } - SimpleRangeBoundKind::Upper(inclusive) => { - if state.upper.is_some() { - return None; - } - state.upper = Some(SimpleRangeBound { - inclusive, - value_expr, - }); - } - } - return Some(()); - } - } - // Not a range bound on the range column. Try to capture it as a - // simple residual column-vs-literal/param comparison on a - // different column so the filtered projection fast path can - // still apply the range prefilter and evaluate the residual - // inline, avoiding the generic executor for conjunctive filters - // like `rating BETWEEN 7.5 AND 9.0 AND runtime_minutes > 120`. - let (res_table, res_column, res_op, res_value) = - simple_residual_projection_bound(left, *op, right).or_else(|| { - simple_residual_projection_bound(right, reverse_binary_op(*op)?, left) - })?; - if let Some(existing_table) = state.table { - if Some(existing_table) != res_table && res_table.is_some() { - return None; - } - } - if state - .column - .is_some_and(|existing| identifiers_equal(existing, res_column)) - { - // Residual on the same column as the range would duplicate a - // bound we already captured; bail to keep semantics simple. - return None; - } - if state - .residual - .iter() - .any(|existing| identifiers_equal(existing.column, res_column)) - { - // At most one residual term per column to avoid interaction - // edge cases (e.g. two predicates on the same column). - return None; - } - state.residual.push(SimpleResidualFilterTerm { - table: res_table, - column: res_column, - op: res_op, - value_expr: res_value, - }); - Some(()) +fn visit_table_payload_projected_values_from_bytes( + bytes: &[u8], + projection_indexes: &[usize], + tombstoned_row_ids: Option<&[i64]>, + visitor: &mut F, +) -> Result +where + F: FnMut(i64, &[Value]) -> Result<()>, +{ + if bytes.is_empty() { + return Ok(0); + } + let mut cursor = Cursor::new(bytes); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + let mut visible_count = 0usize; + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes = cursor.read_slice(row_bytes_len)?; + if is_tombstone { + continue; + } + if tombstoned_row_ids.is_some_and(|row_ids| row_ids.binary_search(&row_id).is_ok()) { + continue; + } + if projection_indexes.is_empty() { + visitor(row_id, &[])?; + } else { + let values = Row::decode_projection_sorted_unique_with_overflow::< + crate::storage::page::InMemoryPageStore, + >(row_bytes, None, projection_indexes)?; + visitor(row_id, &values)?; } - _ => None, + visible_count += 1; } + Ok(visible_count) } -#[derive(Clone, Copy, Debug)] -enum SimpleRangeBoundKind { - Lower(bool), - Upper(bool), -} - -fn simple_range_projection_bound<'a>( - left: &'a Expr, - op: BinaryOp, - right: &'a Expr, -) -> Option<(Option<&'a str>, &'a str, SimpleRangeBoundKind, &'a Expr)> { - let Expr::Column { table, column } = left else { - return None; - }; - if !simple_bound_value_expr_is_constant(right) { - return None; +fn visit_table_payload_projected_values_from_bytes_until( + bytes: &[u8], + projection_indexes: &[usize], + tombstoned_row_ids: Option<&[i64]>, + visitor: &mut F, +) -> Result<(usize, bool)> +where + F: FnMut(i64, &[Value]) -> Result, +{ + if bytes.is_empty() { + return Ok((0, false)); } - let bound_kind = match op { - BinaryOp::Gt => SimpleRangeBoundKind::Lower(false), - BinaryOp::GtEq => SimpleRangeBoundKind::Lower(true), - BinaryOp::Lt => SimpleRangeBoundKind::Upper(false), - BinaryOp::LtEq => SimpleRangeBoundKind::Upper(true), - _ => return None, - }; - Some((table.as_deref(), column.as_str(), bound_kind, right)) -} - -fn simple_residual_projection_bound<'a>( - left: &'a Expr, - op: BinaryOp, - right: &'a Expr, -) -> Option<(Option<&'a str>, &'a str, BinaryOp, &'a Expr)> { - let Expr::Column { table, column } = left else { - return None; - }; - if !simple_bound_value_expr_is_constant(right) { - return None; + let mut cursor = Cursor::new(bytes); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); } - if !matches!( - op, - BinaryOp::Eq - | BinaryOp::NotEq - | BinaryOp::Gt - | BinaryOp::GtEq - | BinaryOp::Lt - | BinaryOp::LtEq - ) { - return None; + let row_count = cursor.read_u32()? as usize; + let mut visible_count = 0usize; + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes = cursor.read_slice(row_bytes_len)?; + if is_tombstone { + continue; + } + if tombstoned_row_ids.is_some_and(|row_ids| row_ids.binary_search(&row_id).is_ok()) { + continue; + } + visible_count += 1; + if projection_indexes.is_empty() { + if visitor(row_id, &[])? { + return Ok((visible_count, true)); + } + } else { + let values = Row::decode_projection_sorted_unique_with_overflow::< + crate::storage::page::InMemoryPageStore, + >(row_bytes, None, projection_indexes)?; + if visitor(row_id, &values)? { + return Ok((visible_count, true)); + } + } } - Some((table.as_deref(), column.as_str(), op, right)) + Ok((visible_count, false)) } -/// A range/residual bound value is "constant" if it can be evaluated once -/// without row context: a literal, a parameter, or a cast of a literal or -/// parameter (e.g. `CAST('2010-01-01' AS DATE)`, which is how the parser -/// represents typed date literals). -fn simple_bound_value_expr_is_constant(expr: &Expr) -> bool { - match expr { - Expr::Literal(_) | Expr::Parameter(_) => true, - Expr::Cast { expr, .. } => simple_bound_value_expr_is_constant(expr), - _ => false, +fn visit_table_payload_rows_from_pointer( + store: &S, + pointer: OverflowPointer, + visitor: &mut F, +) -> Result +where + F: FnMut(i64, &[Value]) -> Result<()>, +{ + if pointer.head_page_id == 0 || pointer.logical_len == 0 { + return Ok(0); + } + if pointer.is_compressed() { + let payload = read_overflow(store, pointer)?; + return visit_table_payload_rows_from_bytes(&payload, visitor); } -} -fn simple_int64_constant_expr_value(expr: &Expr, params: &[Value]) -> Result> { - match expr { - Expr::Literal(Value::Int64(value)) => Ok(Some(*value)), - Expr::Parameter(index) => { - let Some(value) = index.checked_sub(1).and_then(|index| params.get(index)) else { - return Err(DbError::sql(format!("missing parameter ${index}"))); - }; - match value { - Value::Int64(value) => Ok(Some(*value)), - _ => Ok(None), - } + let mut cursor = OverflowPayloadCursor::new(store, pointer); + let mut magic = [0_u8; TABLE_PAYLOAD_MAGIC.len()]; + cursor.read_exact(&mut magic)?; + if magic != *TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + let mut visited = 0usize; + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes = cursor.read_vec(row_bytes_len)?; + if is_tombstone { + continue; } - _ => Ok(None), + let row = Row::decode(&row_bytes)?; + visitor(row_id, row.values())?; + visited += 1; } + Ok(visited) } -fn reverse_binary_op(op: BinaryOp) -> Option { - match op { - BinaryOp::Gt => Some(BinaryOp::Lt), - BinaryOp::GtEq => Some(BinaryOp::LtEq), - BinaryOp::Lt => Some(BinaryOp::Gt), - BinaryOp::LtEq => Some(BinaryOp::GtEq), - _ => None, +fn visit_table_payload_projected_values_from_pointer( + store: &S, + pointer: OverflowPointer, + projection_indexes: &[usize], + visitor: &mut F, +) -> Result +where + F: FnMut(i64, &[Value]) -> Result<()>, +{ + if pointer.head_page_id == 0 || pointer.logical_len == 0 { + return Ok(0); + } + if pointer.is_compressed() { + let payload = read_overflow(store, pointer)?; + return visit_table_payload_projected_values_from_bytes( + &payload, + projection_indexes, + None, + visitor, + ); } -} - -fn simple_range_bounds_match_column_type( - column_type: ColumnType, - lower_bound: Option<&SimpleRangeBoundValue>, - upper_bound: Option<&SimpleRangeBoundValue>, -) -> bool { - lower_bound.is_none_or(|bound| simple_value_matches_column_type(column_type, &bound.value)) - && upper_bound - .is_none_or(|bound| simple_value_matches_column_type(column_type, &bound.value)) -} - -fn simple_value_matches_column_type(column_type: ColumnType, value: &Value) -> bool { - matches!( - (column_type, value), - (ColumnType::Int64, Value::Int64(_)) - | (ColumnType::Float64, Value::Float64(_)) - | (ColumnType::Text, Value::Text(_)) - | (ColumnType::Bool, Value::Bool(_)) - | (ColumnType::Blob, Value::Blob(_)) - | (ColumnType::Decimal, Value::Decimal { .. }) - | (ColumnType::Uuid, Value::Uuid(_)) - | (ColumnType::Timestamp, Value::TimestampMicros(_)) - | (ColumnType::Enum, Value::Enum { .. }) - | (ColumnType::IpAddr, Value::IpAddr { .. }) - | (ColumnType::Cidr, Value::Cidr { .. }) - | (ColumnType::MacAddr, Value::MacAddr { .. }) - | (ColumnType::Date, Value::DateDays(_)) - | (ColumnType::Time, Value::TimeMicros(_)) - | (ColumnType::TimestampTz, Value::TimestampTzMicros(_)) - | (ColumnType::Interval, Value::Interval { .. }) - | (ColumnType::Geometry, Value::Geometry(_)) - | (ColumnType::Geography, Value::Geography(_)) - ) -} -fn simple_range_bound_matches( - candidate: &Value, - lower_bound: Option<&SimpleRangeBoundValue>, - upper_bound: Option<&SimpleRangeBoundValue>, -) -> Result { - if let Some(lower_bound) = lower_bound { - let ordering = simple_fast_compare_values(candidate, &lower_bound.value) - .map(Ok) - .unwrap_or_else(|| compare_values(candidate, &lower_bound.value))?; - let lower_matches = if lower_bound.inclusive { - ordering != std::cmp::Ordering::Less - } else { - ordering == std::cmp::Ordering::Greater - }; - if !lower_matches { - return Ok(false); - } + let mut cursor = OverflowPayloadCursor::new(store, pointer); + let mut magic = [0_u8; TABLE_PAYLOAD_MAGIC.len()]; + cursor.read_exact(&mut magic)?; + if magic != *TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); } - if let Some(upper_bound) = upper_bound { - let ordering = simple_fast_compare_values(candidate, &upper_bound.value) - .map(Ok) - .unwrap_or_else(|| compare_values(candidate, &upper_bound.value))?; - let upper_matches = if upper_bound.inclusive { - ordering != std::cmp::Ordering::Greater + let row_count = cursor.read_u32()? as usize; + let mut visited = 0usize; + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes = cursor.read_vec(row_bytes_len)?; + if is_tombstone { + continue; + } + if projection_indexes.is_empty() { + visitor(row_id, &[])?; } else { - ordering == std::cmp::Ordering::Less - }; - if !upper_matches { - return Ok(false); + let values = Row::decode_projection_sorted_unique_with_overflow::< + crate::storage::page::InMemoryPageStore, + >(&row_bytes, None, projection_indexes)?; + visitor(row_id, &values)?; } + visited += 1; } - Ok(true) + Ok(visited) } -fn simple_int64_range_start(bound: Option<&SimpleRangeBoundValue>) -> Option { - let Some(bound) = bound else { - return Some(i64::MIN); - }; - let Value::Int64(value) = &bound.value else { - return None; - }; - let value = *value; - if bound.inclusive { - Some(value) - } else { - value.checked_add(1) +fn visit_table_payload_projected_values_from_pointer_until( + store: &S, + pointer: OverflowPointer, + projection_indexes: &[usize], + visitor: &mut F, +) -> Result<(usize, bool)> +where + F: FnMut(i64, &[Value]) -> Result, +{ + if pointer.head_page_id == 0 || pointer.logical_len == 0 { + return Ok((0, false)); } -} - -fn simple_int64_range_end_exclusive(bound: Option<&SimpleRangeBoundValue>) -> Option { - let Some(bound) = bound else { - return Some(i64::MAX); - }; - let Value::Int64(value) = &bound.value else { - return None; - }; - let value = *value; - if bound.inclusive { - value.checked_add(1) - } else { - Some(value) + if pointer.is_compressed() { + let payload = read_overflow(store, pointer)?; + return visit_table_payload_projected_values_from_bytes_until( + &payload, + projection_indexes, + None, + visitor, + ); } -} - -#[derive(Clone, Copy, Debug)] -struct QualifiedColumnRef<'a> { - table: Option<&'a str>, - column: &'a str, -} - -#[derive(Clone, Copy, Debug)] -struct TableBindingRef<'a> { - name: &'a str, - alias: &'a Option, -} -impl<'a> TableBindingRef<'a> { - fn binding_name(self) -> &'a str { - self.alias.as_deref().unwrap_or(self.name) + let mut cursor = OverflowPayloadCursor::new(store, pointer); + let mut magic = [0_u8; TABLE_PAYLOAD_MAGIC.len()]; + cursor.read_exact(&mut magic)?; + if magic != *TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); } + let row_count = cursor.read_u32()? as usize; + let mut visited = 0usize; + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes = cursor.read_vec(row_bytes_len)?; + if is_tombstone { + continue; + } + visited += 1; + if projection_indexes.is_empty() { + if visitor(row_id, &[])? { + return Ok((visited, true)); + } + } else { + let values = Row::decode_projection_sorted_unique_with_overflow::< + crate::storage::page::InMemoryPageStore, + >(&row_bytes, None, projection_indexes)?; + if visitor(row_id, &values)? { + return Ok((visited, true)); + } + } + } + Ok((visited, false)) } -#[derive(Clone, Copy, Debug)] -struct SpatialJoinOrientation<'a> { - indexed_table: TableBindingRef<'a>, - indexed_ref: QualifiedColumnRef<'a>, - probe_table: TableBindingRef<'a>, - probe_ref: QualifiedColumnRef<'a>, - indexed_on_left: bool, - left_alias: &'a Option, - right_alias: &'a Option, - constraint: &'a JoinConstraint, - radius_expr: Option<&'a Expr>, - params: &'a [Value], - ctes: &'a BTreeMap, -} - -#[derive(Clone, Debug)] -struct IndexedJoinPlan<'a> { - filtered_table: TableBindingRef<'a>, - filtered_dataset: &'a Dataset, - filtered_join_columns: Vec<&'a str>, - probe_table: TableBindingRef<'a>, - probe_join_columns: Vec<&'a str>, - filtered_on_left: bool, -} - -#[derive(Clone, Copy, Debug)] -struct DeferredViewProjection { - table_index: usize, +fn visit_table_payload_int64_column_from_bytes( + bytes: &[u8], column_index: usize, - is_rowid_alias: bool, + tombstoned_row_ids: Option<&[i64]>, + visitor: &mut F, +) -> Result +where + F: FnMut(i64, Option) -> Result<()>, +{ + if bytes.is_empty() { + return Ok(0); + } + let mut cursor = Cursor::new(bytes); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + let mut visible_count = 0usize; + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes = cursor.read_slice(row_bytes_len)?; + if is_tombstone { + continue; + } + if tombstoned_row_ids.is_some_and(|row_ids| row_ids.binary_search(&row_id).is_ok()) { + continue; + } + visitor(row_id, Row::decode_int64_at(row_bytes, column_index)?)?; + visible_count += 1; + } + Ok(visible_count) } -#[derive(Clone, Debug)] -struct DeferredViewJoinStep { - previous_table_index: usize, - previous_column_index: usize, - current_table_index: usize, - current_index_name: String, - /// True when `previous_column_index` is the previous table's row-id alias - /// (single-column `INTEGER PRIMARY KEY` auto-increment column). In that case - /// the join key value is exactly the previous row's `row_id`, so it never - /// needs to be decoded from the projected row and is omitted from the - /// projection entirely. - previous_is_rowid_alias: bool, +fn visit_table_payload_int64_column_from_pointer( + store: &S, + pointer: OverflowPointer, + column_index: usize, + tombstoned_row_ids: Option<&[i64]>, + visitor: &mut F, +) -> Result +where + F: FnMut(i64, Option) -> Result<()>, +{ + let mut payload_scratch = Vec::new(); + visit_table_payload_int64_column_from_pointer_with_scratch( + store, + pointer, + column_index, + tombstoned_row_ids, + &mut payload_scratch, + visitor, + ) } -#[derive(Clone)] -enum DeferredViewTableRowReader<'a> { - Source(VisibleTableRowSource<'a>), - Deferred { - state: PersistedTableState, - schema: &'a TableSchema, - paged_locator_cache: Option<&'a DeferredPagedRowLocatorCache>, - }, +fn visit_table_payload_int64_column_from_pointer_with_scratch( + store: &S, + pointer: OverflowPointer, + column_index: usize, + tombstoned_row_ids: Option<&[i64]>, + payload_scratch: &mut Vec, + visitor: &mut F, +) -> Result +where + F: FnMut(i64, Option) -> Result<()>, +{ + if pointer.head_page_id == 0 || pointer.logical_len == 0 { + return Ok(0); + } + read_overflow_into(store, pointer, payload_scratch)?; + visit_table_payload_int64_column_from_bytes( + payload_scratch, + column_index, + tombstoned_row_ids, + visitor, + ) } -impl<'a> DeferredViewTableRowReader<'a> { - /// Reads from data already owned by the observed-current runtime. - /// - /// The outer `Option` reports whether the lookup can be completed without - /// storage; the inner `Option` distinguishes an absent row from a row that - /// was decoded successfully. Verified paged payloads were checksummed when - /// the immutable locator cache was built. - fn read_projected_from_observed_cache( - &self, - row_id: i64, - projection_indexes: &[usize], - ) -> Result>> { - match *self { - Self::Source(source) => source - .projected_values_by_id(row_id, projection_indexes) - .map(|values| Some(values.map(|values| StoredRow { row_id, values }))), - Self::Deferred { - state, - paged_locator_cache, - .. - } => { - let Some(cache) = paged_locator_cache.filter(|cache| cache.matches_state(state)) - else { - return Ok(None); - }; - let Some(cached) = cache.locators.get(row_id) else { - return Ok(Some(None)); - }; - let Some(payload) = cache.verified_payload(cached.pointer, cached.checksum) else { - return Ok(None); - }; - decode_projected_values_by_locator_from_payload::( - None, - payload, - cached.locator, - projection_indexes, - ) - .map(|values| Some(Some(StoredRow { row_id, values }))) - } +fn visit_persisted_table_int64_column( + store: &S, + state: PersistedTableState, + column_index: usize, + mut visitor: F, +) -> Result +where + F: FnMut(i64, Option) -> Result<()>, +{ + if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { + return Ok(0); + } + if !state.pointer.is_table_paged_manifest() { + let row_count = visit_table_payload_int64_column_from_pointer( + store, + state.pointer, + column_index, + None, + &mut visitor, + )?; + if state.row_count != 0 && row_count != state.row_count { + return Err(DbError::corruption("table payload row count mismatch")); } + return Ok(row_count); } - fn read_projected_with_chunk_cache( - &self, - store: &S, - row_id: i64, - use_persistent_pk_index: bool, - projection_indexes: &[usize], - chunk_payload_cache: &mut HashMap>>, - ) -> Result> { - match *self { - Self::Source(source) => source - .projected_values_by_id( - row_id, - if projection_indexes.is_empty() { - &[] - } else { - projection_indexes - }, - ) - .map(|values| values.map(|values| StoredRow { row_id, values })), - Self::Deferred { - state, - schema, - paged_locator_cache, - } => { - if !projection_indexes.is_empty() && state.pointer.is_table_paged_manifest() { - if let Some(cache) = - paged_locator_cache.filter(|cache| cache.matches_state(state)) - { - return cache - .locators - .get(row_id) - .map(|cached| { - read_deferred_projected_values_by_cached_paged_locator_with_query_cache( - store, - cached, - cache.verified_payload_arc(cached.pointer, cached.checksum), - chunk_payload_cache, - projection_indexes, - ) - }) - .transpose() - .map(|values| values.map(|values| StoredRow { row_id, values })); - } - } - read_deferred_projected_values_by_id( - store, - state, - schema, - row_id, - use_persistent_pk_index, - paged_locator_cache, - projection_indexes, - ) - .map(|values| values.map(|values| StoredRow { row_id, values })) - } + let manifest_payload = read_overflow(store, state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); + } + let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + let mut total_row_count = 0usize; + let mut payload_scratch = Vec::new(); + for chunk in manifest.chunks { + let mut count = 0usize; + let tombstones = if chunk.tombstoned_row_ids.is_empty() { + None + } else { + Some(chunk.tombstoned_row_ids.as_slice()) + }; + + count += visit_table_payload_int64_column_from_pointer_with_scratch( + store, + chunk.pointer, + column_index, + tombstones, + &mut payload_scratch, + &mut visitor, + )?; + + if let Some(overlay_pointer) = chunk.overlay_pointer { + count += visit_table_payload_int64_column_from_pointer_with_scratch( + store, + overlay_pointer, + column_index, + None, + &mut payload_scratch, + &mut visitor, + )?; } - } -} -#[derive(Clone, Debug)] -struct DeferredViewTableProjection { - projection_indexes: Vec, - projected_positions: Vec>, + if count != chunk.row_count { + return Err(DbError::corruption("paged table chunk row count mismatch")); + } + total_row_count = total_row_count.saturating_add(count); + } + if state.row_count != 0 && total_row_count != state.row_count { + return Err(DbError::corruption( + "paged table manifest row count mismatch", + )); + } + Ok(total_row_count) } -impl DeferredViewTableProjection { - fn new(schema: &TableSchema, required_columns: BTreeSet) -> Self { - let projection_indexes = required_columns.into_iter().collect::>(); - let mut projected_positions = vec![None; schema.columns.len()]; - for (position, column_index) in projection_indexes.iter().copied().enumerate() { - projected_positions[column_index] = Some(position); - } - Self { +fn visit_persisted_table_projected_values( + store: &S, + state: PersistedTableState, + projection_indexes: &[usize], + mut visitor: F, +) -> Result +where + F: FnMut(i64, &[Value]) -> Result<()>, +{ + if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { + return Ok(0); + } + if !state.pointer.is_table_paged_manifest() { + let row_count = visit_table_payload_projected_values_from_pointer( + store, + state.pointer, projection_indexes, - projected_positions, + &mut visitor, + )?; + if state.row_count != 0 && row_count != state.row_count { + return Err(DbError::corruption("table payload row count mismatch")); } + return Ok(row_count); } - fn position(&self, column_index: usize) -> Option { - self.projected_positions - .get(column_index) - .copied() - .flatten() + let manifest_payload = read_overflow(store, state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); + } + let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + let mut total_row_count = 0usize; + for chunk in manifest.chunks { + let mut count = 0usize; + let tombstones = if chunk.tombstoned_row_ids.is_empty() { + None + } else { + Some(chunk.tombstoned_row_ids.as_slice()) + }; + + let base_payload = read_overflow(store, chunk.pointer)?; + count += visit_table_payload_projected_values_from_bytes( + &base_payload, + projection_indexes, + tombstones, + &mut visitor, + )?; + + if let Some(overlay_pointer) = chunk.overlay_pointer { + let overlay_payload = read_overflow(store, overlay_pointer)?; + count += visit_table_payload_projected_values_from_bytes( + &overlay_payload, + projection_indexes, + None, + &mut visitor, + )?; + } + + if count != chunk.row_count { + return Err(DbError::corruption("paged table chunk row count mismatch")); + } + total_row_count = total_row_count.saturating_add(count); + } + if state.row_count != 0 && total_row_count != state.row_count { + return Err(DbError::corruption( + "paged table manifest row count mismatch", + )); } + Ok(total_row_count) } -fn build_deferred_view_table_projections( - table_schemas: &[&TableSchema], - projections: &[DeferredViewProjection], - join_steps: &[DeferredViewJoinStep], -) -> Vec { - let mut required_columns = table_schemas - .iter() - .map(|_| BTreeSet::new()) - .collect::>(); - for projection in projections { - if projection.is_rowid_alias { - continue; +fn visit_persisted_table_projected_values_until( + store: &S, + state: PersistedTableState, + projection_indexes: &[usize], + mut visitor: F, +) -> Result +where + F: FnMut(i64, &[Value]) -> Result, +{ + if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { + return Ok(0); + } + if !state.pointer.is_table_paged_manifest() { + let (row_count, stopped) = visit_table_payload_projected_values_from_pointer_until( + store, + state.pointer, + projection_indexes, + &mut visitor, + )?; + if !stopped && state.row_count != 0 && row_count != state.row_count { + return Err(DbError::corruption("table payload row count mismatch")); } - required_columns[projection.table_index].insert(projection.column_index); + return Ok(row_count); } - for step in join_steps { - if step.previous_is_rowid_alias { - continue; + + let manifest_payload = read_overflow(store, state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); + } + let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + let mut visited_row_count = 0usize; + let mut total_row_count = 0usize; + for chunk in manifest.chunks { + let mut count = 0usize; + let tombstones = if chunk.tombstoned_row_ids.is_empty() { + None + } else { + Some(chunk.tombstoned_row_ids.as_slice()) + }; + + let base_payload = read_overflow(store, chunk.pointer)?; + let (base_count, stopped) = visit_table_payload_projected_values_from_bytes_until( + &base_payload, + projection_indexes, + tombstones, + &mut visitor, + )?; + visited_row_count = visited_row_count.saturating_add(base_count); + count = count.saturating_add(base_count); + if stopped { + return Ok(visited_row_count); } - required_columns[step.previous_table_index].insert(step.previous_column_index); + + if let Some(overlay_pointer) = chunk.overlay_pointer { + let overlay_payload = read_overflow(store, overlay_pointer)?; + let (overlay_count, stopped) = visit_table_payload_projected_values_from_bytes_until( + &overlay_payload, + projection_indexes, + None, + &mut visitor, + )?; + visited_row_count = visited_row_count.saturating_add(overlay_count); + count = count.saturating_add(overlay_count); + if stopped { + return Ok(visited_row_count); + } + } + + if count != chunk.row_count { + return Err(DbError::corruption("paged table chunk row count mismatch")); + } + total_row_count = total_row_count.saturating_add(count); } - table_schemas - .iter() - .zip(required_columns) - .map(|(schema, columns)| DeferredViewTableProjection::new(schema, columns)) - .collect() + if state.row_count != 0 && total_row_count != state.row_count { + return Err(DbError::corruption( + "paged table manifest row count mismatch", + )); + } + Ok(visited_row_count) } -#[derive(Clone, Copy, Debug)] -enum DeferredViewProjectionSource { - RowId { - table_index: usize, - }, - Projected { - table_index: usize, - projected_index: usize, - }, -} +fn visit_persisted_table_rows( + store: &S, + state: PersistedTableState, + mut visitor: F, +) -> Result +where + F: FnMut(i64, &[Value]) -> Result<()>, +{ + if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { + return Ok(0); + } + if !state.pointer.is_table_paged_manifest() { + let row_count = visit_table_payload_rows_from_pointer(store, state.pointer, &mut visitor)?; + if state.row_count != 0 && row_count != state.row_count { + return Err(DbError::corruption("table payload row count mismatch")); + } + return Ok(row_count); + } -fn build_deferred_view_projection_indexes( - projections: &[DeferredViewProjection], - table_projections: &[DeferredViewTableProjection], - context: &str, -) -> Result> { - let mut projection_indexes = Vec::with_capacity(projections.len()); - for projection in projections { - if projection.is_rowid_alias { - projection_indexes.push(DeferredViewProjectionSource::RowId { - table_index: projection.table_index, - }); - continue; + let manifest_payload = read_overflow(store, state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); + } + let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + let mut total_row_count = 0usize; + for chunk in manifest.chunks { + let mut count = 0usize; + let has_tombstones = !chunk.tombstoned_row_ids.is_empty(); + + let base_payload = read_overflow(store, chunk.pointer)?; + for row in decode_table_payload_rows(&base_payload)? { + if has_tombstones && chunk.tombstoned_row_ids.binary_search(&row.row_id).is_ok() { + continue; + } + visitor(row.row_id, &row.values)?; + count += 1; + } + + if let Some(overlay_pointer) = chunk.overlay_pointer { + let overlay_payload = read_overflow(store, overlay_pointer)?; + for row in decode_table_payload_rows(&overlay_payload)? { + visitor(row.row_id, &row.values)?; + count += 1; + } } - let projected_index = table_projections[projection.table_index] - .position(projection.column_index) - .ok_or_else(|| { - DbError::internal(format!( - "{context} projection is missing required column index {}", - projection.column_index - )) - })?; - projection_indexes.push(DeferredViewProjectionSource::Projected { - table_index: projection.table_index, - projected_index, - }); + + if count != chunk.row_count { + return Err(DbError::corruption("paged table chunk row count mismatch")); + } + total_row_count = total_row_count.saturating_add(count); } - Ok(projection_indexes) + if state.row_count != 0 && total_row_count != state.row_count { + return Err(DbError::corruption( + "paged table manifest row count mismatch", + )); + } + Ok(total_row_count) } -/// Resolves the projected position of a join step's previous (outer) column. -/// Returns `None` when the column is the previous table's row-id alias, in -/// which case the join key is available directly from the row's `row_id` and is -/// not present in the projection. -fn join_key_projection_index( - step: &DeferredViewJoinStep, - previous_table_projection: &DeferredViewTableProjection, -) -> Result> { - if step.previous_is_rowid_alias { - return Ok(None); +fn visit_persisted_table_rows_until( + store: &S, + state: PersistedTableState, + mut visitor: F, +) -> Result +where + F: FnMut(i64, &[Value]) -> Result, +{ + if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { + return Ok(0); + } + if !state.pointer.is_table_paged_manifest() { + let mut stopped = false; + let row_count = + visit_table_payload_rows_from_pointer(store, state.pointer, &mut |row_id, values| { + if stopped { + return Ok(()); + } + stopped = visitor(row_id, values)?; + Ok(()) + })?; + if !stopped && state.row_count != 0 && row_count != state.row_count { + return Err(DbError::corruption("table payload row count mismatch")); + } + return Ok(row_count); } - previous_table_projection - .position(step.previous_column_index) - .map(Some) - .ok_or_else(|| { - DbError::internal( - "deferred view linear join projection is missing required join column", - ) - }) -} -fn collect_deferred_view_projection_values( - partial_rows: &[StoredRow], - projection_indexes: &[DeferredViewProjectionSource], - context: &str, -) -> Result> { - let mut values = Vec::with_capacity(projection_indexes.len()); - if projection_indexes.is_empty() { - return Ok(values); + let manifest_payload = read_overflow(store, state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); } - for source in projection_indexes.iter().copied() { - let (table_index, projected_index) = match source { - DeferredViewProjectionSource::RowId { table_index } => { - let Some(row) = partial_rows.get(table_index) else { - return Err(DbError::internal(format!( - "{context} row is shorter than planned schema", - ))); - }; - values.push(Value::Int64(row.row_id)); + let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + let mut visited_row_count = 0usize; + let mut total_row_count = 0usize; + for chunk in manifest.chunks { + let mut count = 0usize; + let has_tombstones = !chunk.tombstoned_row_ids.is_empty(); + + let base_payload = read_overflow(store, chunk.pointer)?; + for row in decode_table_payload_rows(&base_payload)? { + if has_tombstones && chunk.tombstoned_row_ids.binary_search(&row.row_id).is_ok() { continue; } - DeferredViewProjectionSource::Projected { - table_index, - projected_index, - } => (table_index, projected_index), - }; - let Some(row) = partial_rows.get(table_index) else { - return Err(DbError::internal(format!( - "{context} row is shorter than planned schema", - ))); - }; - let Some(value) = row.values.get(projected_index) else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - values.push(value.clone()); - } - Ok(values) -} + visited_row_count = visited_row_count.saturating_add(1); + count += 1; + if visitor(row.row_id, &row.values)? { + return Ok(visited_row_count); + } + } -fn deferred_view_linear_tail_projection_can_move( - projection_indexes: &[DeferredViewProjectionSource], -) -> bool { - let mut previous = None; - for source in projection_indexes { - let DeferredViewProjectionSource::Projected { - table_index: 2, - projected_index, - } = *source - else { - continue; - }; - if previous.is_some_and(|previous| projected_index <= previous) { - return false; + if let Some(overlay_pointer) = chunk.overlay_pointer { + let overlay_payload = read_overflow(store, overlay_pointer)?; + for row in decode_table_payload_rows(&overlay_payload)? { + visited_row_count = visited_row_count.saturating_add(1); + count += 1; + if visitor(row.row_id, &row.values)? { + return Ok(visited_row_count); + } + } } - previous = Some(projected_index); + + if count != chunk.row_count { + return Err(DbError::corruption("paged table chunk row count mismatch")); + } + total_row_count = total_row_count.saturating_add(count); } - true + if state.row_count != 0 && total_row_count != state.row_count { + return Err(DbError::corruption( + "paged table manifest row count mismatch", + )); + } + Ok(visited_row_count) } -fn collect_deferred_view_query_row_from_linear_tail( - root_row: &StoredRow, - row1: &StoredRow, - row2: StoredRow, - projection_indexes: &[DeferredViewProjectionSource], - context: &str, - row2_can_move: bool, -) -> Result { - if row2_can_move && projection_indexes.len() == 4 { - if let [DeferredViewProjectionSource::RowId { table_index: 0 }, DeferredViewProjectionSource::Projected { - table_index: 0, - projected_index: 0, - }, DeferredViewProjectionSource::Projected { - table_index: 1, - projected_index: 0, - }, DeferredViewProjectionSource::Projected { - table_index: 2, - projected_index: 0, - }] = projection_indexes - { - let Some(root_value) = root_row.values.first() else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - let Some(row1_value) = row1.values.first() else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - let mut row2_values = row2.values.into_iter(); - let Some(row2_value) = row2_values.next() else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - return Ok(QueryRow::from_small_values(smallvec![ - Value::Int64(root_row.row_id), - root_value.clone(), - row1_value.clone(), - row2_value, - ])); - } +pub(crate) fn read_table_payload_row_count_from_bytes(bytes: &[u8]) -> Result { + let mut cursor = Cursor::new(bytes); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); } + Ok(cursor.read_u32()? as usize) +} - if row2_can_move && projection_indexes.len() == 3 { - if let [DeferredViewProjectionSource::Projected { - table_index: 1, - projected_index: 0, - }, DeferredViewProjectionSource::Projected { - table_index: 2, - projected_index: 0, - }, DeferredViewProjectionSource::Projected { - table_index: 2, - projected_index: 1, - }] = projection_indexes - { - let Some(row1_value) = row1.values.first() else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - let mut row2_values = row2.values.into_iter(); - let Some(row2_first) = row2_values.next() else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - let Some(row2_second) = row2_values.next() else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - return Ok(QueryRow::from_small_values(smallvec![ - row1_value.clone(), - row2_first, - row2_second, - ])); +/// ADR 0200: count the live (non-tombstoned) rows in a resident table payload. +/// Unlike [`read_table_payload_row_count_from_bytes`] (which returns the +/// physical slot count from the header), this scans the row stream and skips +/// in-place delete tombstones, so it reports the logical row count used for +/// `COUNT(*)`-style metadata. For payloads without tombstones the result equals +/// the header count. +pub(crate) fn read_table_payload_live_row_count_from_bytes(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Ok(0); + } + let mut cursor = Cursor::new(bytes); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + let mut live = 0usize; + for _ in 0..row_count { + let _row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + cursor.read_slice(row_bytes_len)?; + if !is_tombstone { + live += 1; } } - - collect_deferred_view_projection_values_from_linear_tail( - root_row, - row1, - row2, - projection_indexes, - context, - row2_can_move, - ) - .map(QueryRow::new) + Ok(live) } -fn collect_deferred_view_projection_values_from_linear_tail( - root_row: &StoredRow, - row1: &StoredRow, - row2: StoredRow, - projection_indexes: &[DeferredViewProjectionSource], - context: &str, - row2_can_move: bool, -) -> Result> { - if row2_can_move && projection_indexes.len() == 4 { - if let [DeferredViewProjectionSource::RowId { table_index: 0 }, DeferredViewProjectionSource::Projected { - table_index: 0, - projected_index: 0, - }, DeferredViewProjectionSource::Projected { - table_index: 1, - projected_index: 0, - }, DeferredViewProjectionSource::Projected { - table_index: 2, - projected_index: 0, - }] = projection_indexes - { - let Some(root_value) = root_row.values.first() else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - let Some(row1_value) = row1.values.first() else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - let mut row2_values = row2.values.into_iter(); - let Some(row2_value) = row2_values.next() else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - return Ok(vec![ - Value::Int64(root_row.row_id), - root_value.clone(), - row1_value.clone(), - row2_value, - ]); +pub(crate) fn free_persisted_table_bytes( + store: &mut S, + state: PersistedTableState, +) -> Result<()> { + if state.pointer.head_page_id == 0 { + return Ok(()); + } + if state.pointer.is_table_paged_manifest() { + let manifest_payload = read_overflow(store, state.pointer)?; + let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + for chunk in manifest.chunks { + if chunk.pointer.head_page_id != 0 { + free_overflow(store, chunk.pointer.head_page_id)?; + } } + free_overflow(store, state.pointer.head_page_id)?; + return Ok(()); } + free_overflow(store, state.pointer.head_page_id)?; + Ok(()) +} - if row2_can_move && projection_indexes.len() == 3 { - if let [DeferredViewProjectionSource::Projected { - table_index: 1, - projected_index: 0, - }, DeferredViewProjectionSource::Projected { - table_index: 2, - projected_index: 0, - }, DeferredViewProjectionSource::Projected { - table_index: 2, - projected_index: 1, - }] = projection_indexes - { - let Some(row1_value) = row1.values.first() else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - let mut row2_values = row2.values.into_iter(); - let Some(row2_first) = row2_values.next() else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - let Some(row2_second) = row2_values.next() else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - return Ok(vec![row1_value.clone(), row2_first, row2_second]); - } +fn wrap_legacy_table_state_as_paged_manifest( + store: &mut S, + state: PersistedTableState, +) -> Result { + if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { + return Ok(state); + } + let row_count = if state.row_count == 0 { + read_table_payload_row_count(store, state.pointer)? + } else { + state.row_count + }; + let manifest = PersistedPagedTableManifest { + chunks: vec![PersistedTableChunkState { + pointer: state.pointer.with_table_paged_manifest(false), + checksum: state.checksum, + row_count, + tombstoned_row_ids: Vec::new(), + overlay_pointer: None, + overlay_checksum: None, + }], + }; + let manifest_payload = encode_paged_table_manifest_payload(&manifest)?; + let checksum = crc32c_parts(&[manifest_payload.as_slice()]); + let pointer = write_overflow(store, &manifest_payload, CompressionMode::Never)? + .with_table_paged_manifest(true); + let tail = read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default(); + Ok(PersistedTableState { + pointer, + checksum, + row_count, + tail, + pk_index_root: state.pk_index_root, + }) +} + +pub(crate) fn append_paged_table_chunks( + store: &mut S, + mut previous_state: PersistedTableState, + appended_chunks: &[EncodedPagedTableChunk], + row_count: usize, +) -> Result { + if previous_state.pointer.head_page_id == 0 { + return persist_paged_table(store, previous_state, appended_chunks, row_count); + } + if !previous_state.pointer.is_table_paged_manifest() { + previous_state = wrap_legacy_table_state_as_paged_manifest(store, previous_state)?; + } + if appended_chunks.is_empty() { + return Ok(previous_state); } - let row2_row_id = row2.row_id; - let mut row2_values = row2.values; - let mut row2_removed = 0usize; - let mut values = Vec::with_capacity(projection_indexes.len()); - for source in projection_indexes.iter().copied() { - match source { - DeferredViewProjectionSource::RowId { table_index } => { - let row_id = match table_index { - 0 => root_row.row_id, - 1 => row1.row_id, - 2 => row2_row_id, - _ => { - return Err(DbError::internal(format!( - "{context} row is shorter than planned schema", - ))); - } - }; - values.push(Value::Int64(row_id)); - } - DeferredViewProjectionSource::Projected { - table_index, - projected_index, - } => match table_index { - 0 => { - let Some(value) = root_row.values.get(projected_index) else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - values.push(value.clone()); - } - 1 => { - let Some(value) = row1.values.get(projected_index) else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - values.push(value.clone()); - } - 2 if row2_can_move => { - let Some(adjusted_index) = projected_index.checked_sub(row2_removed) else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - if adjusted_index >= row2_values.len() { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - } - values.push(row2_values.remove(adjusted_index)); - row2_removed = row2_removed.saturating_add(1); - } - 2 => { - let Some(value) = row2_values.get(projected_index) else { - return Err(DbError::internal(format!( - "{context} projection row is shorter than planned schema", - ))); - }; - values.push(value.clone()); - } - _ => { - return Err(DbError::internal(format!( - "{context} row is shorter than planned schema", - ))); - } - }, - } + let manifest_payload = read_overflow(store, previous_state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != previous_state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); } - Ok(values) + let mut manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + manifest.chunks.reserve(appended_chunks.len()); + for chunk in appended_chunks { + let pointer = write_overflow(store, &chunk.payload, CompressionMode::Never)?; + manifest.chunks.push(PersistedTableChunkState { + pointer, + checksum: chunk.checksum, + row_count: chunk.row_count, + tombstoned_row_ids: Vec::new(), + overlay_pointer: None, + overlay_checksum: None, + }); + } + + let updated_manifest_payload = encode_paged_table_manifest_payload(&manifest)?; + let checksum = crc32c_parts(&[updated_manifest_payload.as_slice()]); + let pointer = rewrite_overflow( + store, + previous_state.pointer.with_table_paged_manifest(false), + &updated_manifest_payload, + CompressionMode::Never, + )? + .with_table_paged_manifest(true); + let tail = read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default(); + Ok(PersistedTableState { + pointer, + checksum, + row_count, + tail, + pk_index_root: previous_state.pk_index_root, + }) } -fn table_output_columns(table: &TableSchema, alias: &Option) -> Vec { - let table_name = alias.clone().unwrap_or_else(|| table.name.clone()); - table - .columns - .iter() - .map(|column| ColumnBinding::visible(Some(table_name.clone()), column.name.clone())) - .collect() +fn persisted_paged_chunk_is_plain(chunk: &PersistedTableChunkState) -> bool { + chunk.tombstoned_row_ids.is_empty() + && chunk.overlay_pointer.is_none() + && chunk.overlay_checksum.is_none() } -fn flatten_inner_join_chain<'a>( - item: &'a FromItem, - tables: &mut Vec>, - constraints: &mut Vec<&'a Expr>, +fn persisted_chunk_metadata_matches_current( + persisted: &PersistedTableChunkState, + current: &TablePageManifestChunk, ) -> bool { - match item { - FromItem::Table { name, alias } => { - tables.push(TableBindingRef { name, alias }); - true - } - FromItem::Join { - left, - right, - kind: JoinKind::Inner, - constraint: JoinConstraint::On(on), - } => { - flatten_inner_join_chain(left, tables, constraints) - && flatten_inner_join_chain(right, tables, constraints) - && { - constraints.push(on); - true - } - } - _ => false, - } + persisted.pointer == current.pointer + && persisted.checksum == current.checksum + && persisted.row_count == current.row_count + && persisted.overlay_pointer == current.overlay_pointer + && persisted.overlay_checksum == current.overlay_checksum + && persisted.tombstoned_row_ids.len() == current.tombstoned_row_ids.len() + && persisted + .tombstoned_row_ids + .iter() + .all(|row_id| current.tombstoned_row_ids.contains(row_id)) } -fn spatial_join_argument_orientation<'a>( - left_binding: TableBindingRef<'a>, - right_binding: TableBindingRef<'a>, - indexed_ref: QualifiedColumnRef<'a>, - probe_ref: QualifiedColumnRef<'a>, -) -> Option<(TableBindingRef<'a>, TableBindingRef<'a>, bool)> { - let indexed_on_left = matches_table_binding(left_binding, indexed_ref.table); - let indexed_on_right = matches_table_binding(right_binding, indexed_ref.table); - let probe_on_left = matches_table_binding(left_binding, probe_ref.table); - let probe_on_right = matches_table_binding(right_binding, probe_ref.table); - match ( - indexed_on_left, - indexed_on_right, - probe_on_left, - probe_on_right, - ) { - (true, false, false, true) => Some((left_binding, right_binding, true)), - (false, true, true, false) => Some((right_binding, left_binding, false)), - _ => None, +fn persisted_chunk_from_current( + pointer: OverflowPointer, + checksum: u32, + row_count: usize, + current_chunk: &TablePageManifestChunk, +) -> TablePageManifestChunk { + TablePageManifestChunk { + pointer, + checksum, + row_count, + payload: Arc::clone(¤t_chunk.payload), + tombstoned_row_ids: Arc::clone(¤t_chunk.tombstoned_row_ids), + overlay_pointer: None, + overlay_checksum: None, + overlay_payload: None, } } -fn simple_join_equality(on: &Expr) -> Option<(QualifiedColumnRef<'_>, QualifiedColumnRef<'_>)> { - let Expr::Binary { left, op, right } = on else { - return None; - }; - if *op != BinaryOp::Eq { - return None; +fn persist_paged_table( + store: &mut S, + previous_state: PersistedTableState, + encoded_chunks: &[EncodedPagedTableChunk], + row_count: usize, +) -> Result { + if encoded_chunks.is_empty() { + if previous_state.pointer.head_page_id != 0 { + free_persisted_table_bytes(store, previous_state)?; + } + return Ok(PersistedTableState { + pointer: OverflowPointer { + head_page_id: 0, + logical_len: 0, + flags: 0, + }, + checksum: 0, + row_count: 0, + tail: OverflowTailInfo::default(), + pk_index_root: previous_state.pk_index_root, + }); } - let ( - Expr::Column { - table: left_table, - column: left_column, - }, - Expr::Column { - table: right_table, - column: right_column, - }, - ) = (&**left, &**right) - else { - return None; + + let mut persisted_chunks = Vec::with_capacity(encoded_chunks.len()); + for chunk in encoded_chunks { + let pointer = write_overflow(store, &chunk.payload, CompressionMode::Never)?; + persisted_chunks.push(PersistedTableChunkState { + pointer, + checksum: chunk.checksum, + row_count: chunk.row_count, + tombstoned_row_ids: Vec::new(), + overlay_pointer: None, + overlay_checksum: None, + }); + } + let manifest = PersistedPagedTableManifest { + chunks: persisted_chunks, }; - Some(( - QualifiedColumnRef { - table: left_table.as_deref(), - column: left_column, - }, - QualifiedColumnRef { - table: right_table.as_deref(), - column: right_column, - }, - )) + let manifest_payload = encode_paged_table_manifest_payload(&manifest)?; + let checksum = crc32c_parts(&[manifest_payload.as_slice()]); + let pointer = write_overflow(store, &manifest_payload, CompressionMode::Never)? + .with_table_paged_manifest(true); + let tail = read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default(); + if previous_state.pointer.head_page_id != 0 { + free_persisted_table_bytes(store, previous_state)?; + } + Ok(PersistedTableState { + pointer, + checksum, + row_count, + tail, + pk_index_root: previous_state.pk_index_root, + }) } -fn simple_join_equalities( - on: &Expr, -) -> Option, QualifiedColumnRef<'_>)>> { - fn collect<'a>( - expr: &'a Expr, - equalities: &mut Vec<(QualifiedColumnRef<'a>, QualifiedColumnRef<'a>)>, - ) -> Option<()> { - match expr { - Expr::Binary { left, op, right } if *op == BinaryOp::And => { - collect(left, equalities)?; - collect(right, equalities)?; - Some(()) - } - _ => { - equalities.push(simple_join_equality(expr)?); - Some(()) - } +/// Scan a table payload's row ids without decoding row values. Returns the set +/// of row ids stored in the payload (excluding any tombstoned/overlaid rows +/// the caller already handles). Used to decide whether a chunk contains deleted +/// rows without paying the cost of decoding every row's values. +pub(crate) fn scan_table_payload_row_ids(payload: &[u8]) -> Result> { + if payload.len() < TABLE_PAYLOAD_MAGIC.len() + 4 { + return Ok(BTreeSet::new()); + } + let mut cursor = Cursor::new(payload); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + let mut row_ids = BTreeSet::new(); + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + cursor.read_slice(row_bytes_len)?; + if is_tombstone { + continue; } + row_ids.insert(row_id); } + Ok(row_ids) +} - let mut equalities = Vec::new(); - collect(on, &mut equalities)?; - if equalities.is_empty() { - return None; - } - Some(equalities) +fn build_persistent_pk_index_root(db: &crate::db::Db, payload: &[u8]) -> Result> { + let entries = build_row_locator_entries(payload)?; + let mut tree = Btree::new(DbTxnPageStore { db }); + tree.replace_entries(entries)?; + let (_store, root_page_id) = tree.into_parts(); + Ok(root_page_id) } -fn orient_join_equalities<'a>( - equalities: &[(QualifiedColumnRef<'a>, QualifiedColumnRef<'a>)], - filtered_table: TableBindingRef<'a>, - probe_table: TableBindingRef<'a>, -) -> Option<(Vec<&'a str>, Vec<&'a str>)> { - let mut filtered_columns = Vec::with_capacity(equalities.len()); - let mut probe_columns = Vec::with_capacity(equalities.len()); - for (left_ref, right_ref) in equalities { - if matches_table_binding(filtered_table, left_ref.table) - && matches_table_binding(probe_table, right_ref.table) - { - filtered_columns.push(left_ref.column); - probe_columns.push(right_ref.column); - } else if matches_table_binding(filtered_table, right_ref.table) - && matches_table_binding(probe_table, left_ref.table) - { - filtered_columns.push(right_ref.column); - probe_columns.push(left_ref.column); - } else { - return None; - } - } - Some((filtered_columns, probe_columns)) +fn build_persistent_pk_index_root_from_chunk_payloads( + db: &crate::db::Db, + chunk_payloads: &[TablePageManifestChunk], +) -> Result> { + let entries = build_paged_row_locator_entries_from_chunk_payloads(chunk_payloads)?; + let mut tree = Btree::new(DbTxnPageStore { db }); + tree.replace_entries(entries)?; + let (_store, root_page_id) = tree.into_parts(); + Ok(root_page_id) } -fn simple_indexed_join_constraint_equalities<'a>( - constraint: &'a JoinConstraint, - left: TableBindingRef<'a>, - right: TableBindingRef<'a>, - left_schema: &'a TableSchema, - right_schema: &'a TableSchema, -) -> Option, QualifiedColumnRef<'a>)>> { - match constraint { - JoinConstraint::On(on) => simple_join_equalities(on), - JoinConstraint::Using(columns) if !columns.is_empty() => { - let mut equalities = Vec::with_capacity(columns.len()); - for column in columns { - equalities.push(( - QualifiedColumnRef { - table: Some(left.binding_name()), - column: column.as_str(), - }, - QualifiedColumnRef { - table: Some(right.binding_name()), - column: column.as_str(), - }, - )); - } - Some(equalities) - } - JoinConstraint::Using(_) => None, - JoinConstraint::Natural => { - let common_columns = simple_indexed_join_natural_columns(left_schema, right_schema); - if common_columns.is_empty() { - return None; - } - let mut equalities = Vec::with_capacity(common_columns.len()); - for column in common_columns { - equalities.push(( - QualifiedColumnRef { - table: Some(left.binding_name()), - column, - }, - QualifiedColumnRef { - table: Some(right.binding_name()), - column, - }, - )); - } - Some(equalities) +fn replace_table_pk_index_root( + runtime: &mut EngineRuntime, + db: &crate::db::Db, + table_name: &str, + new_pk_index_root: Option, +) -> Result<()> { + let previous_pk_index_root = runtime + .persisted_tables + .get(table_name) + .and_then(|state| state.pk_index_root) + .or_else(|| { + runtime + .catalog + .tables + .get(table_name) + .and_then(|table| table.pk_index_root) + }); + match previous_pk_index_root { + Some(previous_pk_index_root) if Some(previous_pk_index_root) != new_pk_index_root => { + let mut store = DbTxnPageStore { db }; + free_table_btree(&mut store, Some(previous_pk_index_root))?; } + _ => {} + } + let table = runtime + .catalog_mut() + .tables + .get_mut(table_name) + .ok_or_else(|| DbError::internal(format!("table schema for {table_name} is missing")))?; + table.pk_index_root = new_pk_index_root; + if let Some(state) = runtime.persisted_tables_mut().get_mut(table_name) { + state.pk_index_root = new_pk_index_root; } + Ok(()) } -fn simple_indexed_join_using_columns( - constraint: &JoinConstraint, - left_schema: &TableSchema, - right_schema: &TableSchema, -) -> Vec { - match constraint { - JoinConstraint::Using(columns) => columns.clone(), - JoinConstraint::Natural => simple_indexed_join_natural_columns(left_schema, right_schema) - .into_iter() - .map(str::to_string) - .collect(), - JoinConstraint::On(_) => Vec::new(), - } +/// Build a new payload by splicing only the modified rows into the cached +/// previous payload. Unchanged row bytes are copied verbatim from `old`, +/// saving the per-row serialisation cost for the common single-row UPDATE. +/// Result of a splice operation, containing the new payload and dirty byte +/// range metadata. +pub(crate) struct SpliceResult { + payload: Vec, + /// Byte offset of the first modified byte in the OLD payload. + first_dirty_byte: usize, + /// Exclusive byte offset of the first byte after the changed range in the + /// OLD payload, when conservative behavior is used this may be payload + /// length. + last_dirty_byte: usize, + /// Whether the updated payload preserves row offsets and can reuse + /// the previous persistent PK locator root. + pk_locator_preserved: bool, } -fn simple_indexed_join_natural_columns<'a>( - left_schema: &'a TableSchema, - right_schema: &'a TableSchema, -) -> Vec<&'a str> { - left_schema - .columns - .iter() - .filter(|left_column| { - right_schema - .columns - .iter() - .any(|right_column| identifiers_equal(&left_column.name, &right_column.name)) - }) - .map(|column| column.name.as_str()) - .collect() +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct SpliceDirtyRange { + first_dirty_byte: usize, + last_dirty_byte: usize, } -fn flatten_left_deep_inner_join_tables<'a>( - item: &'a FromItem, - tables: &mut Vec>, - constraints: &mut Vec<&'a JoinConstraint>, -) -> bool { - match item { - FromItem::Table { name, alias } => { - if !tables.is_empty() { - return false; - } - tables.push(IndexedJoinLimitTablePlan { - name: name.as_str(), - alias, - }); - true - } - FromItem::Join { - left, - right, - kind: JoinKind::Inner, - constraint, - } => { - if !flatten_left_deep_inner_join_tables(left, tables, constraints) { - return false; - } - let FromItem::Table { name, alias } = &**right else { - return false; - }; - tables.push(IndexedJoinLimitTablePlan { - name: name.as_str(), - alias, - }); - constraints.push(constraint); - true - } - _ => false, - } +fn single_dirty_range(range: Range) -> Vec> { + std::iter::once(range).collect() } -fn indexed_join_group_column_indexes( - group_by: &[Expr], - binding: TableBindingRef<'_>, - schema: &TableSchema, -) -> Option> { - group_by - .iter() - .map(|expr| { - let Expr::Column { table, column } = expr else { - return None; - }; - if !matches_table_binding(binding, table.as_deref()) { - return None; - } - schema - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, column)) - }) - .collect() -} +fn truncate_tail_deleted_rows_payload( + payload: &mut Vec, + deleted_row_ids: &BTreeSet, + live_row_count: usize, +) -> Result>>> { + const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; -fn indexed_join_grouped_count_is_safe( - expr: &Expr, - child_binding: TableBindingRef<'_>, - child_schema: &TableSchema, -) -> bool { - let Expr::Aggregate { - name, - args, - distinct, - star, - order_by, - within_group, - } = expr - else { - return false; - }; - if !name.eq_ignore_ascii_case("count") || *distinct || !order_by.is_empty() || *within_group { - return false; - } - if *star { - return args.is_empty(); + if deleted_row_ids.is_empty() { + return Ok(Some(Vec::new())); } - if args.len() != 1 { - return false; + if payload.len() < HEADER_LEN || payload[..8] != *TABLE_PAYLOAD_MAGIC { + return Ok(None); } - let Expr::Column { table, column } = &args[0] else { - return false; - }; - if !matches_table_binding(child_binding, table.as_deref()) { - return false; + let physical_row_count = + u32::from_le_bytes(payload[8..12].try_into().expect("row-count header length")) as usize; + if physical_row_count != live_row_count.saturating_add(deleted_row_ids.len()) { + return Ok(None); } - child_schema - .columns - .iter() - .find(|candidate| identifiers_equal(&candidate.name, column)) - .is_some_and(|column| column.primary_key || !column.nullable) -} -fn indexed_join_limit_projection_column( - expr: &Expr, - tables: &[IndexedJoinLimitTablePlan<'_>], - runtime: &EngineRuntime, -) -> Option<(usize, usize)> { - let Expr::Column { - table: qualifier, - column, - } = expr - else { - return None; - }; - let mut matched = None; - let unqualified_unique = qualifier.is_none() - && tables - .iter() - .filter(|candidate| { - runtime - .table_schema(candidate.name) - .is_some_and(|schema| schema_column_index(schema, column).is_some()) - }) - .count() - == 1; - for (table_index, table_plan) in tables.iter().enumerate() { - let binding = TableBindingRef { - name: table_plan.name, - alias: table_plan.alias, - }; - let qualifier_matches = qualifier - .as_deref() - .is_some_and(|qualifier| matches_table_binding(binding, Some(qualifier))); - let schema = runtime.table_schema(table_plan.name)?; - let Some(column_index) = schema_column_index(schema, column) else { - continue; + let mut offset = HEADER_LEN; + let mut first_deleted_offset = None; + let mut deleted_seen = 0usize; + for _ in 0..physical_row_count { + if offset + 12 > payload.len() { + return Ok(None); + } + let row_start = offset; + let row_id = i64::from_le_bytes( + payload[offset..offset + 8] + .try_into() + .expect("row id length"), + ); + let len_field_offset = offset + 8; + let raw_len = u32::from_le_bytes( + payload[len_field_offset..len_field_offset + 4] + .try_into() + .expect("row data len"), + ); + let (is_tombstone, body_len) = split_table_payload_row_len(raw_len); + if is_tombstone { + return Ok(None); + } + let Some(row_end) = len_field_offset + .checked_add(4) + .and_then(|value| value.checked_add(body_len)) + else { + return Ok(None); }; - if (qualifier_matches || unqualified_unique) - && matched.replace((table_index, column_index)).is_some() - { - return None; + if row_end > payload.len() { + return Ok(None); } - } - matched -} - -fn pushed_view_projection_for_outer_projection( - outer_projection: &[SelectItem], - view_select: &Select, - view_name: &str, - view_binding: &str, - view_column_names: &[String], -) -> Option> { - let mut pushed = Vec::new(); - for item in outer_projection { - match item { - SelectItem::Wildcard => { - append_all_view_projection_items(&mut pushed, view_select, view_column_names)?; - } - SelectItem::QualifiedWildcard(qualifier) - if identifiers_equal(qualifier, view_binding) - || identifiers_equal(qualifier, view_name) => - { - append_all_view_projection_items(&mut pushed, view_select, view_column_names)?; - } - SelectItem::QualifiedWildcard(_) => return None, - SelectItem::Expr { expr, alias } => { - let Expr::Column { table, column } = expr else { - return None; - }; - if table.as_deref().is_some_and(|qualifier| { - !identifiers_equal(qualifier, view_binding) - && !identifiers_equal(qualifier, view_name) - }) { - return None; - } - let view_expr = view_projection_expr_for_output_column_with_names( - &view_select.projection, - view_column_names, - column, - )?; - pushed.push(SelectItem::Expr { - expr: view_expr, - alias: Some(alias.clone().unwrap_or_else(|| infer_expr_name(expr, 1))), - }); + if deleted_row_ids.contains(&row_id) { + if first_deleted_offset.is_none() { + first_deleted_offset = Some(row_start); } + deleted_seen += 1; + } else if first_deleted_offset.is_some() { + return Ok(None); } + offset = row_end; } - Some(pushed) -} + if offset != payload.len() || deleted_seen != deleted_row_ids.len() { + return Ok(None); + } + let Some(truncate_at) = first_deleted_offset else { + return Ok(None); + }; -fn append_all_view_projection_items( - pushed: &mut Vec, - view_select: &Select, - view_column_names: &[String], -) -> Option<()> { - for (index, item) in view_select.projection.iter().enumerate() { - let SelectItem::Expr { expr, .. } = item else { - return None; - }; - pushed.push(SelectItem::Expr { - expr: expr.clone(), - alias: Some(view_output_column_name( - &view_select.projection, - view_column_names, - index, - )?), - }); + payload[8..12].copy_from_slice( + &u32::try_from(live_row_count) + .map_err(|_| DbError::constraint("table row count exceeds u32"))? + .to_le_bytes(), + ); + payload.truncate(truncate_at); + + let mut dirty_ranges = Vec::with_capacity(2); + dirty_ranges.push(8..12); + if !payload.is_empty() { + let tail_start = payload.len().saturating_sub(1); + dirty_ranges.push(tail_start..payload.len()); } - Some(()) + Ok(Some(dirty_ranges)) } -fn view_projection_expr_for_output_column_with_names( - items: &[SelectItem], - view_column_names: &[String], - column: &str, -) -> Option { - for (index, item) in items.iter().enumerate() { - if identifiers_equal( - &view_output_column_name(items, view_column_names, index)?, - column, - ) { - let SelectItem::Expr { expr, .. } = item else { - return None; - }; - return Some(expr.clone()); +fn read_overflow_cached_logical_bytes( + store: &S, + pointer: OverflowPointer, + cached_page_ids: &[PageId], + offset: usize, + out: &mut [u8], +) -> Result { + if out.is_empty() { + return Ok(true); + } + if pointer.is_compressed() || pointer.is_table_paged_manifest() { + return Ok(false); + } + let logical_len = pointer.logical_len as usize; + let Some(end) = offset.checked_add(out.len()) else { + return Ok(false); + }; + if end > logical_len { + return Ok(false); + } + + let page_size = store.page_size() as usize; + if page_size <= OVERFLOW_HEADER_SIZE { + return Err(DbError::internal("page size too small for overflow pages")); + } + let chunk_capacity = page_size - OVERFLOW_HEADER_SIZE; + let mut read = 0usize; + while read < out.len() { + let logical_offset = offset + read; + let page_index = logical_offset / chunk_capacity; + let Some(&page_id) = cached_page_ids.get(page_index) else { + return Ok(false); + }; + let page_payload_offset = page_index.saturating_mul(chunk_capacity); + let local_offset = logical_offset.saturating_sub(page_payload_offset); + let page = store.read_page(page_id)?; + if page.len() < OVERFLOW_HEADER_SIZE { + return Err(DbError::corruption("overflow page shorter than header")); + } + let chunk_len = + u32::from_le_bytes(page[4..8].try_into().expect("header chunk len")) as usize; + let chunk_end = OVERFLOW_HEADER_SIZE.saturating_add(chunk_len); + if chunk_end > page.len() { + return Err(DbError::corruption( + "overflow chunk length exceeds page payload", + )); + } + if local_offset >= chunk_len { + return Ok(false); } + let take = (out.len() - read).min(chunk_len - local_offset); + out[read..read + take].copy_from_slice( + &page[OVERFLOW_HEADER_SIZE + local_offset..OVERFLOW_HEADER_SIZE + local_offset + take], + ); + read += take; } - None + Ok(true) } -fn view_output_column_name( - items: &[SelectItem], - view_column_names: &[String], - index: usize, -) -> Option { - if let Some(name) = view_column_names.get(index) { - return Some(name.clone()); +fn build_resident_tombstone_locators_from_payload(payload: &[u8]) -> Result> { + const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; + + if payload.is_empty() { + return Ok(Int64Map::with_hasher(Int64HashBuilder::default())); } - let SelectItem::Expr { expr, alias } = items.get(index)? else { - return None; - }; - Some( - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - ) + if payload.len() < HEADER_LEN || payload[..8] != *TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = + u32::from_le_bytes(payload[8..12].try_into().expect("row-count header length")) as usize; + let mut locators = Int64Map::with_capacity_and_hasher(row_count, Int64HashBuilder::default()); + let mut offset = HEADER_LEN; + for _ in 0..row_count { + if offset + 12 > payload.len() { + return Err(DbError::corruption("truncated table payload row header")); + } + let row_id = i64::from_le_bytes( + payload[offset..offset + 8] + .try_into() + .expect("row id length"), + ); + let len_field_offset = offset + 8; + let raw_len = u32::from_le_bytes( + payload[len_field_offset..len_field_offset + 4] + .try_into() + .expect("row data len"), + ); + let (_, body_len) = split_table_payload_row_len(raw_len); + let row_end = len_field_offset + .checked_add(4) + .and_then(|value| value.checked_add(body_len)) + .ok_or_else(|| DbError::corruption("table payload row length overflow"))?; + if row_end > payload.len() { + return Err(DbError::corruption("truncated table payload row body")); + } + locators.insert( + row_id, + u32::try_from(len_field_offset) + .map_err(|_| DbError::constraint("resident tombstone locator exceeds u32"))?, + ); + offset = row_end; + } + Ok(locators) } -fn indexed_join_table_eval_columns( - tables: &[IndexedJoinLimitTablePlan<'_>], - runtime: &EngineRuntime, -) -> Option> { - let mut columns = Vec::new(); - for table in tables { - let schema = runtime.table_schema(table.name)?; - let binding_name = table.alias.as_deref().unwrap_or(table.name); - columns.extend(schema.columns.iter().map(|column| { - ColumnBinding::visible_source( - Some(binding_name.to_string()), - Some(schema.name.clone()), - column.name.clone(), - ) - })); +fn append_encoded_rows_to_table_payload( + mut previous: Vec, + row_count: usize, + appended_rows: &[u8], +) -> Result> { + if appended_rows.is_empty() { + return Ok(previous); + } + let count_offset = TABLE_PAYLOAD_MAGIC.len(); + if previous.len() < count_offset + 4 { + return Err(DbError::corruption("table payload header is truncated")); + } + if previous[..count_offset] != *TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); } - Some(columns) -} -fn single_plain_btree_index_matches_column( - index: &IndexSchema, - table_name: &str, - column_name: &str, -) -> bool { - identifiers_equal(&index.table_name, table_name) - && index.fresh - && index.kind == IndexKind::Btree - && index.columns.len() == 1 - && index.columns[0].expression_sql.is_none() - && index.columns[0] - .column_name - .as_deref() - .is_some_and(|index_column| identifiers_equal(index_column, column_name)) + previous[count_offset..count_offset + 4].copy_from_slice( + &u32::try_from(row_count) + .map_err(|_| DbError::constraint("table row count exceeds u32"))? + .to_le_bytes(), + ); + previous.extend_from_slice(appended_rows); + Ok(previous) } -fn filter_contains_partial_index_predicate( - filter: &Expr, - predicate: &Expr, - root_binding: &str, -) -> bool { - match filter { - Expr::Binary { - left, - op: BinaryOp::And, - right, - } => { - filter_contains_partial_index_predicate(left, predicate, root_binding) - || filter_contains_partial_index_predicate(right, predicate, root_binding) - } - _ => partial_index_predicate_expr_matches(filter, predicate, root_binding), +pub(crate) fn append_table_payload(mut previous: Vec, data: &TableData) -> Result> { + if data.rows.is_empty() { + return Ok(Vec::new()); + } + if data.has_tombstoned_rows() { + return encode_table_payload(data); + } + if previous.is_empty() { + return encode_table_payload(data); + } + let count_offset = TABLE_PAYLOAD_MAGIC.len(); + if previous.len() < count_offset + 4 { + return Err(DbError::corruption("table payload header is truncated")); + } + if previous[..count_offset] != *TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + + let existing_count = u32::from_le_bytes( + previous[count_offset..count_offset + 4] + .try_into() + .expect("row-count header length"), + ) as usize; + let appended_rows = encode_appended_table_rows(data, existing_count)?; + if appended_rows.is_empty() { + return Ok(previous); } + + previous[count_offset..count_offset + 4].copy_from_slice( + &u32::try_from(data.row_count()) + .map_err(|_| DbError::constraint("table row count exceeds u32"))? + .to_le_bytes(), + ); + previous.extend_from_slice(&appended_rows); + Ok(previous) } -fn partial_index_predicate_expr_matches(left: &Expr, right: &Expr, root_binding: &str) -> bool { - match (left, right) { - ( - Expr::Column { - table: left_table, - column: left_column, - }, - Expr::Column { - table: right_table, - column: right_column, - }, - ) => { - identifiers_equal(left_column, right_column) - && partial_index_predicate_qualifier_matches(left_table.as_deref(), root_binding) - && partial_index_predicate_qualifier_matches(right_table.as_deref(), root_binding) - } - (Expr::Literal(left), Expr::Literal(right)) => left == right, - ( - Expr::Binary { - left: left_left, - op: left_op, - right: left_right, - }, - Expr::Binary { - left: right_left, - op: right_op, - right: right_right, - }, - ) if left_op == right_op => { - let direct = partial_index_predicate_expr_matches(left_left, right_left, root_binding) - && partial_index_predicate_expr_matches(left_right, right_right, root_binding); - direct - || binary_op_is_commutative_for_partial_predicate(*left_op) - && partial_index_predicate_expr_matches(left_left, right_right, root_binding) - && partial_index_predicate_expr_matches(left_right, right_left, root_binding) - } - ( - Expr::Unary { - op: left_op, - expr: left_expr, - }, - Expr::Unary { - op: right_op, - expr: right_expr, - }, - ) if left_op == right_op => { - partial_index_predicate_expr_matches(left_expr, right_expr, root_binding) - } - ( - Expr::Cast { - expr: left_expr, - target_type: left_type, - }, - Expr::Cast { - expr: right_expr, - target_type: right_type, - }, - ) if left_type == right_type => { - partial_index_predicate_expr_matches(left_expr, right_expr, root_binding) - } - ( - Expr::IsNull { - expr: left_expr, - negated: left_not, - }, - Expr::IsNull { - expr: right_expr, - negated: right_not, - }, - ) if left_not == right_not => { - partial_index_predicate_expr_matches(left_expr, right_expr, root_binding) - } - _ => left == right, +#[cfg(test)] +fn drop_index_include_columns_section(payload: &[u8]) -> Result> { + let start = payload + .windows(INDEX_INCLUDE_COLUMNS_SECTION_MAGIC.len()) + .position(|window| window == INDEX_INCLUDE_COLUMNS_SECTION_MAGIC) + .ok_or_else(|| DbError::internal("index include columns section not found"))?; + let mut cursor = Cursor::new( + payload + .get(start + INDEX_INCLUDE_COLUMNS_SECTION_MAGIC.len()..) + .ok_or_else(|| DbError::internal("index include columns section header truncated"))?, + ); + let _version = cursor.read_u8()?; + let entry_count = cursor.read_u32()?; + for _ in 0..entry_count { + let _index_name = cursor.read_string()?; + let _include_columns = cursor.read_strings()?; } + let section_len = INDEX_INCLUDE_COLUMNS_SECTION_MAGIC.len() + cursor.offset; + let end = start + .checked_add(section_len) + .ok_or_else(|| DbError::internal("index include section length overflow"))?; + let mut output = Vec::with_capacity(payload.len().saturating_sub(section_len)); + output.extend_from_slice( + payload + .get(..start) + .ok_or_else(|| DbError::internal("invalid include section start"))?, + ); + output.extend_from_slice( + payload + .get(end..) + .ok_or_else(|| DbError::internal("invalid include section end"))?, + ); + Ok(output) } -fn partial_index_predicate_qualifier_matches(qualifier: Option<&str>, root_binding: &str) -> bool { - qualifier.is_none_or(|qualifier| identifiers_equal(qualifier, root_binding)) +pub(crate) struct Cursor<'a> { + bytes: &'a [u8], + offset: usize, } -fn binary_op_is_commutative_for_partial_predicate(op: BinaryOp) -> bool { - matches!( - op, - BinaryOp::Eq - | BinaryOp::NotEq - | BinaryOp::And - | BinaryOp::Or - | BinaryOp::Add - | BinaryOp::Mul - | BinaryOp::IsDistinctFrom - | BinaryOp::IsNotDistinctFrom - ) -} +impl<'a> Cursor<'a> { + fn new(bytes: &'a [u8]) -> Self { + Self { bytes, offset: 0 } + } -fn visit_runtime_btree_row_ids_in_order( - keys: &RuntimeBtreeKeys, - descending: bool, - mut visitor: F, -) -> Result -where - F: FnMut(i64) -> Result, -{ - match keys { - RuntimeBtreeKeys::UniqueEncoded(entries, deleted) => { - if descending { - for row_id in entries.values().rev() { - if !deleted.contains(row_id) && visitor(*row_id)? { - return Ok(true); - } - } - } else { - for row_id in entries.values() { - if !deleted.contains(row_id) && visitor(*row_id)? { - return Ok(true); - } - } - } - } - RuntimeBtreeKeys::NonUniqueEncoded(entries, deleted) => { - if descending { - for row_ids in entries.values().rev() { - for row_id in row_ids { - if !deleted.contains(row_id) && visitor(*row_id)? { - return Ok(true); - } - } - } - } else { - for row_ids in entries.values() { - for row_id in row_ids { - if !deleted.contains(row_id) && visitor(*row_id)? { - return Ok(true); - } - } - } - } - } - RuntimeBtreeKeys::UniqueUuid(entries, deleted) => { - if descending { - for row_id in entries.values().rev() { - if !deleted.contains(row_id) && visitor(*row_id)? { - return Ok(true); - } - } - } else { - for row_id in entries.values() { - if !deleted.contains(row_id) && visitor(*row_id)? { - return Ok(true); - } - } - } - } - RuntimeBtreeKeys::NonUniqueUuid(entries, deleted) => { - if descending { - for row_ids in entries.values().rev() { - for row_id in row_ids { - if !deleted.contains(row_id) && visitor(*row_id)? { - return Ok(true); - } - } - } - } else { - for row_ids in entries.values() { - for row_id in row_ids { - if !deleted.contains(row_id) && visitor(*row_id)? { - return Ok(true); - } - } - } - } - } - RuntimeBtreeKeys::UniqueInt64(entries, deleted) => { - let mut ordered = entries - .iter() - .filter(|(_, row_id)| !deleted.contains(row_id)) - .collect::>(); - ordered.sort_unstable_by_key(|(key, _)| *key); - if descending { - ordered.reverse(); - } - for (_, row_id) in ordered { - if visitor(row_id)? { - return Ok(true); - } - } - } - RuntimeBtreeKeys::NonUniqueInt64(entries, deleted) => { - let mut ordered = entries - .iter() - .map(|(key, row_ids)| (key, row_ids.to_vec())) - .collect::>(); - ordered.sort_unstable_by_key(|(key, _)| *key); - if descending { - ordered.reverse(); - } - for (_, mut row_ids) in ordered { - row_ids.sort_unstable(); - for row_id in row_ids { - if !deleted.contains(&row_id) && visitor(row_id)? { - return Ok(true); - } - } - } - } + fn read_slice(&mut self, len: usize) -> Result<&'a [u8]> { + let end = self + .offset + .checked_add(len) + .ok_or_else(|| DbError::corruption("cursor overflow"))?; + let bytes = self + .bytes + .get(self.offset..end) + .ok_or_else(|| DbError::corruption("truncated catalog state"))?; + self.offset = end; + Ok(bytes) } - Ok(false) -} -fn indexed_join_limit_rows_for_value( - source: VisibleTableRowSource<'_>, - keys: Option<&RuntimeBtreeKeys>, - value: &Value, -) -> Result>> { - if matches!(value, Value::Null) { - return Ok(Vec::new()); + fn read_u8(&mut self) -> Result { + let value = *self + .bytes + .get(self.offset) + .ok_or_else(|| DbError::corruption("truncated catalog state"))?; + self.offset += 1; + Ok(value) } - let Some(keys) = keys else { - let Value::Int64(row_id) = value else { - return Ok(Vec::new()); - }; - return Ok(source - .row_by_id(*row_id)? - .map(|row| vec![row.values().to_vec()]) - .unwrap_or_default()); - }; - let row_ids = keys.row_ids_for_value_set(value)?; - let mut rows = Vec::with_capacity(row_ids.len()); - let mut row_error = None; - row_ids.for_each(|row_id| { - if row_error.is_some() { - return; - } - match source.row_by_id(row_id) { - Ok(Some(row)) => rows.push(row.values().to_vec()), - Ok(None) => {} - Err(error) => row_error = Some(error), + + fn read_bool(&mut self) -> Result { + Ok(self.read_u8()? != 0) + } + + fn read_u32(&mut self) -> Result { + let bytes = self.read_slice(4)?; + Ok(u32::from_le_bytes(bytes.try_into().expect("u32"))) + } + + fn read_u64(&mut self) -> Result { + let bytes = self.read_slice(8)?; + Ok(u64::from_le_bytes(bytes.try_into().expect("u64"))) + } + + fn read_i64(&mut self) -> Result { + let bytes = self.read_slice(8)?; + Ok(i64::from_le_bytes(bytes.try_into().expect("i64"))) + } + + fn read_string(&mut self) -> Result { + let len = self.read_u32()? as usize; + let bytes = self.read_slice(len)?; + std::str::from_utf8(bytes) + .map(|s| s.to_owned()) + .map_err(|error| { + DbError::corruption(format!("catalog state string is not valid UTF-8: {error}")) + }) + } + + fn read_optional_string(&mut self) -> Result> { + if self.read_bool()? { + Ok(Some(self.read_string()?)) + } else { + Ok(None) } - }); - if let Some(error) = row_error { - return Err(error); } - Ok(rows) -} -fn indexed_join_row_ids_for_value( - keys: Option<&RuntimeBtreeKeys>, - value: &Value, -) -> Result> { - if matches!(value, Value::Null) { - return Ok(Vec::new()); + fn read_strings(&mut self) -> Result> { + let len = self.read_u32()? as usize; + (0..len).map(|_| self.read_string()).collect() } - let Some(keys) = keys else { - return Ok(match value { - Value::Int64(row_id) => vec![*row_id], - _ => Vec::new(), - }); - }; - let row_ids = keys.row_ids_for_value_set(value)?; - let mut values = Vec::with_capacity(row_ids.len()); - row_ids.for_each(|row_id| values.push(row_id)); - Ok(values) } -fn sort_join_row_ids_by_column( - source: VisibleTableRowSource<'_>, - row_ids: Vec, - column_index: usize, -) -> Result> { - let mut keyed = Vec::with_capacity(row_ids.len()); - for row_id in row_ids { - let Some(row) = source.row_by_id(row_id)? else { - continue; - }; - let Some(value) = row.values().get(column_index) else { - return Err(DbError::internal( - "indexed join order row is shorter than schema", - )); - }; - keyed.push((row_id, value.clone())); +fn dataset_to_result(dataset: Dataset) -> QueryResult { + let Dataset { columns, rows } = dataset; + QueryResult::with_rows( + columns.into_iter().map(|binding| binding.name).collect(), + Arc::unwrap_or_clone(rows) + .into_iter() + .map(QueryRow::new) + .collect(), + ) +} + +pub(crate) fn projection_has_aggregate_items(items: &[SelectItem]) -> bool { + items.iter().any(|item| match item { + SelectItem::Expr { expr, .. } => expr_contains_aggregate(expr), + SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => false, + }) +} + +fn select_requires_grouped_evaluation(runtime: &EngineRuntime, select: &Select) -> Result { + if !select.group_by.is_empty() || projection_has_aggregate_items(&select.projection) { + return Ok(true); } - let mut sort_error = None; - keyed.sort_by(|(_, left), (_, right)| match compare_values(left, right) { - Ok(ordering) => ordering, - Err(error) => { - if sort_error.is_none() { - sort_error = Some(error); + if select.having.as_ref().is_some_and(expr_contains_aggregate) { + return Ok(true); + } + projection_has_runtime_extension_aggregate_items(runtime, &select.projection).and_then( + |has_projection_aggregate| { + if has_projection_aggregate { + return Ok(true); + } + select + .having + .as_ref() + .map(|expr| expr_contains_runtime_extension_aggregate(runtime, expr)) + .transpose() + .map(Option::unwrap_or_default) + }, + ) +} + +fn projection_has_runtime_extension_aggregate_items( + runtime: &EngineRuntime, + items: &[SelectItem], +) -> Result { + for item in items { + if let SelectItem::Expr { expr, .. } = item { + if expr_contains_runtime_extension_aggregate(runtime, expr)? { + return Ok(true); } - std::cmp::Ordering::Equal } - }); - if let Some(error) = sort_error { - return Err(error); } - Ok(keyed.into_iter().map(|(row_id, _)| row_id).collect()) + Ok(false) } -fn project_indexed_join_row( - current_rows: &[&[Value]], - projections: &[IndexedJoinLimitProjection], -) -> Result { - let mut output = Vec::with_capacity(projections.len()); - for projection in projections { - let Some(row) = current_rows.get(projection.table_index) else { +fn ordered_lookup_terms_for_index<'a>( + index: &IndexSchema, + lookup_terms: &[(Option<&'a str>, &'a str, &'a Expr)], +) -> Result, &'a str, &'a Expr)>> { + let mut ordered = Vec::with_capacity(lookup_terms.len()); + for index_column in index.columns.iter().take(lookup_terms.len()) { + let Some(column_name) = index_column.column_name.as_deref() else { return Err(DbError::internal( - "indexed join projection table index is out of range", + "compound indexed projection matched expression index column", )); }; - let Some(value) = row.get(projection.column_index) else { + let Some(term) = lookup_terms + .iter() + .copied() + .find(|(_, lookup_column, _)| identifiers_equal(column_name, lookup_column)) + else { return Err(DbError::internal( - "indexed join projection row is shorter than schema", + "compound indexed projection matched non-prefix lookup terms", )); }; - output.push(value.clone()); + ordered.push(term); } - Ok(QueryRow::new(output)) + Ok(ordered) } -fn push_indexed_join_limit_projection( - current_rows: &[&[Value]], - projections: &[IndexedJoinLimitProjection], - offset_remaining: &mut usize, - limit_remaining: &mut usize, - rows: &mut Vec, -) -> bool { - if *offset_remaining > 0 { - *offset_remaining -= 1; - return false; - } - if *limit_remaining == 0 { - return true; - } - let mut output = Vec::with_capacity(projections.len()); - for projection in projections { - let Some(row) = current_rows.get(projection.table_index) else { - return true; - }; - let Some(value) = row.get(projection.column_index) else { - return true; - }; - output.push(value.clone()); +fn row_ids_for_simple_indexed_projection_lookup<'a>( + keys: &'a RuntimeBtreeKeys, + plan: &SimpleIndexedProjectionPlan<'_>, +) -> Result> { + if plan.extra_lookup_terms.is_empty() { + return keys + .row_ids_for_value_set(&plan.lookup_value) + .map(SimpleIndexedProjectionRowIds::Borrowed); } - rows.push(QueryRow::new(output)); - *limit_remaining = (*limit_remaining).saturating_sub(1); - *limit_remaining == 0 -} - -fn indexed_join_limit_result(plan: &IndexedJoinLimitPlan<'_>, rows: Vec) -> QueryResult { - QueryResult::with_rows( - plan.projections - .iter() - .map(|projection| projection.column_name.clone()) - .collect(), - rows, - ) + let values = std::iter::once(plan.lookup_value.clone()) + .chain( + plan.extra_lookup_terms + .iter() + .map(|(_, value)| value.clone()), + ) + .collect::>(); + Ok(SimpleIndexedProjectionRowIds::Owned(keys.row_ids_for_key( + &RuntimeBtreeKey::Encoded(RuntimeEncodedKey::from_vec(Row::new(values).encode()?)), + ))) } -fn row_id_alias_column_name(table: &TableSchema) -> Option<&str> { - if table.primary_key_columns.len() != 1 { +fn indexed_projection_row_id_order( + plan: &SimpleIndexedProjectionPlan<'_>, +) -> Option<(bool, usize)> { + let order_by = plan.order_by.as_ref()?; + if order_by.len() != 1 { return None; } - let primary_key_column = &table.primary_key_columns[0]; - table - .columns - .iter() - .find(|column| identifiers_equal(&column.name, primary_key_column) && column.auto_increment) - .map(|column| column.name.as_str()) -} - -/// Returns the schema column index of the table's row-id alias column, when the -/// table has a single-column auto-increment `INTEGER PRIMARY KEY`. The stored -/// `row_id` of every row equals the value of this column. -fn rowid_alias_column_index(table: &TableSchema) -> Option { - let alias = row_id_alias_column_name(table)?; - table - .columns - .iter() - .position(|column| identifiers_equal(&column.name, alias)) + let row_id_alias = row_id_alias_column_name(plan.table_schema)?; + let row_id_order = &order_by[0]; + if row_id_order.collation.is_some() { + return None; + } + let projection_index = plan + .projection_indexes + .get(row_id_order.projection_index) + .copied()?; + let order_column = plan.table_schema.columns.get(projection_index)?; + if !identifiers_equal(&order_column.name, row_id_alias) { + return None; + } + let limit_with_offset = plan + .limit + .map(|limit| limit.saturating_add(plan.offset)) + .unwrap_or(usize::MAX); + Some((row_id_order.descending, limit_with_offset)) } -fn project_simple_projection_row(stored_row: &StoredRow, projection_indexes: &[usize]) -> QueryRow { - project_simple_projection_values(&stored_row.values, projection_indexes) +fn view_projection_expr_for_output_column(items: &[SelectItem], column: &str) -> Option { + for (index, item) in items.iter().enumerate() { + let SelectItem::Expr { expr, alias } = item else { + continue; + }; + let output_name = alias + .as_deref() + .map(std::borrow::Cow::Borrowed) + .unwrap_or_else(|| std::borrow::Cow::Owned(infer_expr_name(expr, index + 1))); + if identifiers_equal(output_name.as_ref(), column) { + return Some(expr.clone()); + } + } + None } -fn project_simple_projection_value_vec( - values: &[Value], - projection_indexes: &[usize], -) -> Vec { - let mut projected = Vec::with_capacity(projection_indexes.len()); - for index in projection_indexes { - projected.push(values[*index].clone()); - } - projected +#[derive(Clone, Copy, Debug)] +struct SimpleRangeBound<'a> { + inclusive: bool, + value_expr: &'a Expr, } -fn push_projection_index(indexes: &mut Vec, index: usize) -> usize { - if let Some(position) = indexes.iter().position(|candidate| *candidate == index) { - position - } else { - indexes.push(index); - indexes.len() - 1 - } +#[derive(Clone, Debug)] +pub(crate) struct SimpleRangeBoundValue { + pub(crate) inclusive: bool, + pub(crate) value: Value, } -fn project_simple_projection_values(values: &[Value], projection_indexes: &[usize]) -> QueryRow { - let mut projected = SmallVec::<[Value; 4]>::with_capacity(projection_indexes.len()); - for index in projection_indexes { - projected.push(values[*index].clone()); - } - QueryRow::from_small_values(projected) +#[derive(Clone, Debug)] +struct SimpleGroupedNumericState { + numeric_count: i64, + total_int: i64, + total_float: f64, + saw_float: bool, + saw_value: bool, } -fn project_resolved_simple_join_row( - projections: &[ResolvedSimpleJoinProjection], - left_values: &[Value], - right_values: &[Value], -) -> Result { - let mut projected = Vec::with_capacity(projections.len()); - for projection in projections { - let values = match projection.side { - SimpleJoinProjectionSide::Left => left_values, - SimpleJoinProjectionSide::Right => right_values, - }; - let value = values - .get(projection.index) - .ok_or_else(|| DbError::internal("prepared join projection index out of bounds"))?; - projected.push(value.clone()); +impl SimpleGroupedNumericState { + fn add(&mut self, value: &Value) -> Result<()> { + match value { + Value::Null => Ok(()), + Value::Int64(value) => { + self.numeric_count += 1; + self.total_int += value; + self.total_float += *value as f64; + self.saw_value = true; + Ok(()) + } + Value::Float64(value) => { + self.numeric_count += 1; + self.total_float += *value; + self.saw_float = true; + self.saw_value = true; + Ok(()) + } + Value::Decimal { scaled, scale } => { + self.numeric_count += 1; + self.total_float += decimal_to_f64(*scaled, *scale); + self.saw_float = true; + self.saw_value = true; + Ok(()) + } + other => Err(DbError::sql(format!( + "numeric aggregate does not support {other:?}" + ))), + } } - Ok(QueryRow::new(projected)) -} -fn project_resolved_simple_join_row_from_full_values( - projections: &[ResolvedSimpleJoinProjection], - left_projection_indexes: &[usize], - right_projection_indexes: &[usize], - left_values: &[Value], - right_values: &[Value], -) -> Result { - let mut projected = Vec::with_capacity(projections.len()); - for projection in projections { - let (projection_indexes, values) = match projection.side { - SimpleJoinProjectionSide::Left => (left_projection_indexes, left_values), - SimpleJoinProjectionSide::Right => (right_projection_indexes, right_values), - }; - let original_index = projection_indexes - .get(projection.index) - .ok_or_else(|| DbError::internal("prepared join projection index out of bounds"))?; - let value = values - .get(*original_index) - .ok_or_else(|| DbError::internal("prepared join projection index out of bounds"))?; - projected.push(value.clone()); + fn value(&self, kind: SimpleGroupedNumericAggregateKind) -> Value { + match kind { + SimpleGroupedNumericAggregateKind::Sum => { + if !self.saw_value { + Value::Null + } else if self.saw_float { + Value::Float64(self.total_float) + } else { + Value::Int64(self.total_int) + } + } + SimpleGroupedNumericAggregateKind::Avg => { + if self.numeric_count == 0 { + Value::Null + } else { + Value::Float64(self.total_float / self.numeric_count as f64) + } + } + SimpleGroupedNumericAggregateKind::SumDistinct => { + if !self.saw_value { + Value::Null + } else if self.saw_float { + Value::Float64(self.total_float) + } else { + Value::Int64(self.total_int) + } + } + SimpleGroupedNumericAggregateKind::AvgDistinct => { + if self.numeric_count == 0 { + Value::Null + } else { + Value::Float64(self.total_float / self.numeric_count as f64) + } + } + SimpleGroupedNumericAggregateKind::Total + | SimpleGroupedNumericAggregateKind::TotalDistinct => { + if self.numeric_count == 0 { + Value::Float64(0.0) + } else { + Value::Float64(self.total_float) + } + } + _ => Value::Null, + } } - Ok(QueryRow::new(projected)) } -fn covering_projection_offsets( - covering: &RuntimeCoveringPayloads, - table_schema: &TableSchema, - projection_indexes: &[usize], -) -> Option> { - projection_indexes - .iter() - .map(|projection_index| { - table_schema - .columns - .get(*projection_index) - .and_then(|column| covering.column_position(&column.name)) - }) - .collect() +#[derive(Clone, Debug, Default)] +struct SimpleGroupedVarianceState { + count: u64, + mean: f64, + m2: f64, } -fn simple_expression_projection_plan<'a>( - table_schema: &'a TableSchema, - table_name: &str, - binding_name: &str, - projection: &'a [SelectItem], -) -> Option> { - let mut sources = Vec::new(); - let mut column_names = Vec::new(); - for (item_index, item) in projection.iter().enumerate() { - match item { - SelectItem::Expr { expr, alias } => { - if let Expr::Column { table, column } = expr { - let column_index = simple_expression_projection_column_index( - table_schema, - table_name, - binding_name, - table.as_deref(), - column, - )?; - sources.push(SimpleExpressionProjectionSource::Column(column_index)); - column_names.push(alias.clone().unwrap_or_else(|| column.clone())); - } else { - sources.push(SimpleExpressionProjectionSource::Expr(expr)); - column_names.push( - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, item_index + 1)), - ); - } +impl SimpleGroupedVarianceState { + fn add(&mut self, value: &Value) -> Result<()> { + let number = match value { + Value::Null => return Ok(()), + Value::Int64(value) => *value as f64, + Value::Float64(value) => *value, + Value::Decimal { scaled, scale } => decimal_to_f64(*scaled, *scale), + other => { + return Err(DbError::sql(format!( + "variance aggregate does not support {other:?}" + ))) } - SelectItem::Wildcard => { - for (column_index, column) in table_schema.columns.iter().enumerate() { - sources.push(SimpleExpressionProjectionSource::Column(column_index)); - column_names.push(column.name.clone()); + }; + self.count += 1; + let delta = number - self.mean; + self.mean += delta / (self.count as f64); + let delta2 = number - self.mean; + self.m2 += delta * delta2; + Ok(()) + } + + fn value(&self, kind: SimpleGroupedNumericAggregateKind) -> Value { + if self.count == 0 { + return Value::Null; + } + let denominator = match kind { + SimpleGroupedNumericAggregateKind::StddevPop + | SimpleGroupedNumericAggregateKind::StddevPopDistinct + | SimpleGroupedNumericAggregateKind::VarPop + | SimpleGroupedNumericAggregateKind::VarPopDistinct => self.count as f64, + SimpleGroupedNumericAggregateKind::StddevSamp + | SimpleGroupedNumericAggregateKind::StddevSampDistinct + | SimpleGroupedNumericAggregateKind::VarSamp + | SimpleGroupedNumericAggregateKind::VarSampDistinct => { + if self.count < 2 { + return Value::Null; } + (self.count - 1) as f64 } - SelectItem::QualifiedWildcard(qualified_name) => { - if !identifiers_equal(qualified_name, table_name) - && !identifiers_equal(qualified_name, binding_name) - { - return None; - } - for (column_index, column) in table_schema.columns.iter().enumerate() { - sources.push(SimpleExpressionProjectionSource::Column(column_index)); - column_names.push(column.name.clone()); - } + _ => return Value::Null, + }; + let variance = self.m2 / denominator; + match kind { + SimpleGroupedNumericAggregateKind::StddevPop + | SimpleGroupedNumericAggregateKind::StddevPopDistinct + | SimpleGroupedNumericAggregateKind::StddevSamp + | SimpleGroupedNumericAggregateKind::StddevSampDistinct => { + Value::Float64(variance.sqrt()) } + SimpleGroupedNumericAggregateKind::VarPop + | SimpleGroupedNumericAggregateKind::VarPopDistinct + | SimpleGroupedNumericAggregateKind::VarSamp + | SimpleGroupedNumericAggregateKind::VarSampDistinct => Value::Float64(variance), + _ => Value::Null, } } - Some(SimpleExpressionProjectionPlan { - sources, - column_names, - }) } -fn simple_expression_projection_column_index( - table_schema: &TableSchema, - table_name: &str, - binding_name: &str, - qualifier: Option<&str>, - column_name: &str, -) -> Option { - if let Some(qualifier) = qualifier { - if !identifiers_equal(qualifier, table_name) && !identifiers_equal(qualifier, binding_name) - { - return None; +#[derive(Clone, Debug)] +struct SimpleGroupedBoolState { + saw_non_null: bool, + and_value: bool, + or_value: bool, +} + +impl Default for SimpleGroupedBoolState { + fn default() -> Self { + Self { + saw_non_null: false, + and_value: true, + or_value: false, } } - table_schema - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, column_name)) } -fn apply_simple_projection_postprocessing_with_order( - runtime: Option<&EngineRuntime>, - mut rows: Vec, - column_names: Vec, - order_by: Option<&[SimpleOrderByPlan]>, - limit: Option, - offset: usize, -) -> Result { - if let Some(order_by) = order_by { - if let Some(limit) = limit { - let bounded_row_count = offset.saturating_add(limit); - if bounded_row_count == 0 { - return Ok(QueryResult::with_rows(column_names, Vec::new())); - } - let mut bounded_rows = Vec::with_capacity(rows.len().min(bounded_row_count)); - for row in rows { - push_bounded_projection_ordered_query_row( - runtime, - &mut bounded_rows, - row, - order_by, - bounded_row_count, - )?; +impl SimpleGroupedBoolState { + fn add(&mut self, value: &Value) -> Result<()> { + let boolean = match value { + Value::Null => return Ok(()), + Value::Bool(value) => *value, + other => { + return Err(DbError::sql(format!( + "boolean aggregate does not support {other:?}" + ))) } - sort_query_rows_by_projection_order(runtime, &mut bounded_rows, order_by)?; - let rows = bounded_rows.into_iter().skip(offset).take(limit).collect(); - return Ok(QueryResult::with_rows(column_names, rows)); - } - sort_query_rows_by_projection_order(runtime, &mut rows, order_by)?; + }; + self.saw_non_null = true; + self.and_value &= boolean; + self.or_value |= boolean; + Ok(()) } - let rows = rows - .into_iter() - .skip(offset) - .take(limit.unwrap_or(usize::MAX)) - .collect(); - Ok(QueryResult::with_rows(column_names, rows)) -} -fn dedup_query_rows(rows: Vec) -> Result> { - let mut seen = BTreeSet::new(); - let mut distinct_rows = Vec::with_capacity(rows.len()); - for row in rows { - if seen.insert(row_identity(row.values())?) { - distinct_rows.push(row); + fn value(&self, kind: SimpleGroupedNumericAggregateKind) -> Value { + if !self.saw_non_null { + return Value::Null; + } + match kind { + SimpleGroupedNumericAggregateKind::BoolAnd + | SimpleGroupedNumericAggregateKind::BoolAndDistinct => Value::Bool(self.and_value), + SimpleGroupedNumericAggregateKind::BoolOr + | SimpleGroupedNumericAggregateKind::BoolOrDistinct => Value::Bool(self.or_value), + _ => Value::Null, } } - Ok(distinct_rows) } -fn compare_query_row_order_values( - runtime: Option<&EngineRuntime>, - left_order: &[Value], - right_order: &[Value], - order_by: &[crate::sql::ast::OrderBy], -) -> Result { - for (index, order) in order_by.iter().enumerate() { - let ordering = compare_values_with_runtime_collation( - runtime, - &left_order[index], - &right_order[index], - order.collation.clone(), - )?; - if ordering == std::cmp::Ordering::Equal { - continue; - } - return Ok(if order.descending { - ordering.reverse() - } else { - ordering - }); - } - Ok(std::cmp::Ordering::Equal) +#[derive(Clone, Debug)] +struct SimpleGroupedNumericAggregate { + group_values: Vec, + count: i64, + value_counts: Vec, + distinct_values: Vec>>, + numeric_states: Vec, + variance_states: Vec, + bool_states: Vec, + extreme_values: Vec, } -fn sort_query_rows_by_order_values( - runtime: Option<&EngineRuntime>, - rows: &mut [(QueryRow, Vec)], - order_by: &[crate::sql::ast::OrderBy], -) -> Result<()> { - let mut sort_error = None; - rows.sort_by(|(_, left_order), (_, right_order)| { - match compare_query_row_order_values(runtime, left_order, right_order, order_by) { - Ok(ordering) => ordering, - Err(error) => { - if sort_error.is_none() { - sort_error = Some(error); - } - std::cmp::Ordering::Equal - } +impl SimpleGroupedNumericAggregate { + fn new(group_values: Vec, aggregate_count: usize) -> Self { + Self { + group_values, + count: 0, + value_counts: vec![0; aggregate_count], + distinct_values: vec![BTreeSet::new(); aggregate_count], + numeric_states: vec![ + SimpleGroupedNumericState { + numeric_count: 0, + total_int: 0, + total_float: 0.0, + saw_float: false, + saw_value: false, + }; + aggregate_count + ], + variance_states: vec![SimpleGroupedVarianceState::default(); aggregate_count], + bool_states: vec![SimpleGroupedBoolState::default(); aggregate_count], + extreme_values: vec![Value::Null; aggregate_count], } - }); - if let Some(error) = sort_error { - return Err(error); } - Ok(()) -} -fn push_bounded_ordered_query_row( - runtime: Option<&EngineRuntime>, - rows: &mut Vec<(QueryRow, Vec)>, - row: (QueryRow, Vec), - order_by: &[crate::sql::ast::OrderBy], - bounded_row_count: usize, -) -> Result<()> { - if bounded_row_count == 0 { - return Ok(()); - } - if rows.len() < bounded_row_count { - rows.push(row); - return Ok(()); + fn add_numeric(&mut self, aggregate_index: usize, value: &Value) -> Result<()> { + self.numeric_states[aggregate_index].add(value) } - let mut worst_index = 0; - for index in 1..rows.len() { - if compare_query_row_order_values(runtime, &rows[index].1, &rows[worst_index].1, order_by)? - == std::cmp::Ordering::Greater - { - worst_index = index; + + fn count_non_null(&mut self, aggregate_index: usize, value: &Value) { + if !matches!(value, Value::Null) { + self.value_counts[aggregate_index] += 1; } } - if compare_query_row_order_values(runtime, &row.1, &rows[worst_index].1, order_by)? - == std::cmp::Ordering::Less - { - rows[worst_index] = row; + + fn count_distinct(&mut self, aggregate_index: usize, value: &Value) -> Result<()> { + if matches!(value, Value::Null) { + return Ok(()); + } + let key = row_identity(std::slice::from_ref(value))?; + if self.distinct_values[aggregate_index].insert(key) { + self.value_counts[aggregate_index] += 1; + } + Ok(()) } - Ok(()) -} -fn sort_query_rows_by_projection_order( - runtime: Option<&EngineRuntime>, - rows: &mut [QueryRow], - order_by: &[SimpleOrderByPlan], -) -> Result<()> { - let mut sort_error = None; - rows.sort_by(|left, right| { - for order in order_by { - let ordering = compare_values_with_runtime_collation( - runtime, - &left.values()[order.projection_index], - &right.values()[order.projection_index], - order.collation.clone(), - ); - match ordering { - Ok(std::cmp::Ordering::Equal) => continue, - Ok(ordering) => { - return if order.descending { - ordering.reverse() - } else { - ordering - }; - } - Err(error) => { - if sort_error.is_none() { - sort_error = Some(error); - } - return std::cmp::Ordering::Equal; - } - } + fn add_numeric_distinct(&mut self, aggregate_index: usize, value: &Value) -> Result<()> { + if matches!(value, Value::Null) { + return Ok(()); } - std::cmp::Ordering::Equal - }); - if let Some(error) = sort_error { - return Err(error); + let key = row_identity(std::slice::from_ref(value))?; + if self.distinct_values[aggregate_index].insert(key) { + self.numeric_states[aggregate_index].add(value)?; + } + Ok(()) } - Ok(()) -} -fn compare_query_rows_by_projection_order( - runtime: Option<&EngineRuntime>, - left: &QueryRow, - right: &QueryRow, - order_by: &[SimpleOrderByPlan], -) -> Result { - for order in order_by { - let ordering = compare_values_with_runtime_collation( - runtime, - &left.values()[order.projection_index], - &right.values()[order.projection_index], - order.collation.clone(), - )?; - if ordering == std::cmp::Ordering::Equal { - continue; + fn add_variance(&mut self, aggregate_index: usize, value: &Value) -> Result<()> { + self.variance_states[aggregate_index].add(value) + } + + fn add_variance_distinct(&mut self, aggregate_index: usize, value: &Value) -> Result<()> { + if matches!(value, Value::Null) { + return Ok(()); } - return Ok(if order.descending { - ordering.reverse() - } else { - ordering - }); + let key = row_identity(std::slice::from_ref(value))?; + if self.distinct_values[aggregate_index].insert(key) { + self.variance_states[aggregate_index].add(value)?; + } + Ok(()) } - Ok(std::cmp::Ordering::Equal) -} -fn push_bounded_projection_ordered_query_row( - runtime: Option<&EngineRuntime>, - rows: &mut Vec, - row: QueryRow, - order_by: &[SimpleOrderByPlan], - bounded_row_count: usize, -) -> Result<()> { - if bounded_row_count == 0 { - return Ok(()); + fn add_bool(&mut self, aggregate_index: usize, value: &Value) -> Result<()> { + self.bool_states[aggregate_index].add(value) } - if rows.len() < bounded_row_count { - rows.push(row); - return Ok(()); + + fn add_bool_distinct(&mut self, aggregate_index: usize, value: &Value) -> Result<()> { + if matches!(value, Value::Null) { + return Ok(()); + } + let key = row_identity(std::slice::from_ref(value))?; + if self.distinct_values[aggregate_index].insert(key) { + self.bool_states[aggregate_index].add(value)?; + } + Ok(()) } - let mut worst_index = 0; - for index in 1..rows.len() { - if compare_query_rows_by_projection_order( - runtime, - &rows[index], - &rows[worst_index], - order_by, - )? == std::cmp::Ordering::Greater - { - worst_index = index; + + fn aggregate_value( + &self, + kind: SimpleGroupedNumericAggregateKind, + aggregate_index: usize, + ) -> Value { + match kind { + SimpleGroupedNumericAggregateKind::CountRows => Value::Int64(self.count), + SimpleGroupedNumericAggregateKind::CountNonNull + | SimpleGroupedNumericAggregateKind::CountDistinct => { + Value::Int64(self.value_counts[aggregate_index]) + } + SimpleGroupedNumericAggregateKind::Sum + | SimpleGroupedNumericAggregateKind::SumDistinct + | SimpleGroupedNumericAggregateKind::Avg + | SimpleGroupedNumericAggregateKind::AvgDistinct + | SimpleGroupedNumericAggregateKind::Total + | SimpleGroupedNumericAggregateKind::TotalDistinct => { + self.numeric_states[aggregate_index].value(kind) + } + SimpleGroupedNumericAggregateKind::StddevSamp + | SimpleGroupedNumericAggregateKind::StddevSampDistinct + | SimpleGroupedNumericAggregateKind::StddevPop + | SimpleGroupedNumericAggregateKind::StddevPopDistinct + | SimpleGroupedNumericAggregateKind::VarSamp + | SimpleGroupedNumericAggregateKind::VarSampDistinct + | SimpleGroupedNumericAggregateKind::VarPop + | SimpleGroupedNumericAggregateKind::VarPopDistinct => { + self.variance_states[aggregate_index].value(kind) + } + SimpleGroupedNumericAggregateKind::BoolAnd + | SimpleGroupedNumericAggregateKind::BoolAndDistinct + | SimpleGroupedNumericAggregateKind::BoolOr + | SimpleGroupedNumericAggregateKind::BoolOrDistinct => { + self.bool_states[aggregate_index].value(kind) + } + SimpleGroupedNumericAggregateKind::Min | SimpleGroupedNumericAggregateKind::Max => { + self.extreme_values[aggregate_index].clone() + } } } - if compare_query_rows_by_projection_order(runtime, &row, &rows[worst_index], order_by)? - == std::cmp::Ordering::Less - { - rows[worst_index] = row; + + fn into_row(self, aggregate_bindings: &[SimpleGroupedNumericAggregateBinding]) -> QueryRow { + let mut group_values = self.group_values.clone(); + for (aggregate_index, aggregate) in aggregate_bindings.iter().enumerate() { + group_values.push(self.aggregate_value(aggregate.kind, aggregate_index)); + } + QueryRow::new(group_values) + } + + fn aggregate_values( + &self, + aggregate_bindings: &[SimpleGroupedNumericAggregateBinding], + ) -> Vec { + aggregate_bindings + .iter() + .enumerate() + .map(|(aggregate_index, aggregate)| { + self.aggregate_value(aggregate.kind, aggregate_index) + }) + .collect() } - Ok(()) } -fn simple_grouped_having_bindings(column_count: usize) -> Vec { - (0..column_count) - .map(|index| ColumnBinding::visible(None, format!("__grouped_having_col_{index}"))) - .collect() +#[derive(Clone, Debug)] +struct SimpleGroupedCountAggregate { + group_values: Vec, + count: i64, } -fn simple_grouped_having_column_name( - select: &Select, - table_name: &str, - binding_name: &str, - column_names: &[String], - synthetic_names: &[String], - table: Option<&str>, - column: &str, -) -> Option { - if let Some(table) = table { - if !identifiers_equal(table, table_name) && !identifiers_equal(table, binding_name) { - return None; +impl SimpleGroupedCountAggregate { + fn new(group_values: Vec) -> Self { + Self { + group_values, + count: 0, } - return unique_grouped_having_group_index(select, column) - .map(|index| synthetic_names[index].clone()); } - let alias_index = unique_grouped_having_column_index(column_names, column); - let group_index = unique_grouped_having_group_index(select, column); - match (alias_index, group_index) { - (Some(alias_index), None) => Some(synthetic_names[alias_index].clone()), - (None, Some(group_index)) => Some(synthetic_names[group_index].clone()), - (Some(alias_index), Some(group_index)) if alias_index == group_index => { - Some(synthetic_names[alias_index].clone()) - } - _ => None, + fn into_row(self) -> QueryRow { + let mut row = self.group_values; + row.push(Value::Int64(self.count)); + QueryRow::new(row) } -} -fn unique_grouped_having_column_index(column_names: &[String], column: &str) -> Option { - let mut matched = None; - for (index, candidate) in column_names.iter().enumerate() { - if !candidate.eq_ignore_ascii_case(column) { - continue; - } - if matched.replace(index).is_some() { - return None; - } + fn aggregate_values(&self) -> Vec { + vec![Value::Int64(self.count)] } - matched } -fn unique_grouped_having_group_index(select: &Select, column: &str) -> Option { - let mut matched = None; - for (index, expr) in select.group_by.iter().enumerate() { - let Expr::Column { - column: group_column, - .. - } = expr - else { - continue; - }; - if !identifiers_equal(group_column, column) { - continue; - } - if matched.replace(index).is_some() { - return None; +fn evaluate_simple_grouped_values( + runtime: &EngineRuntime, + exprs: &[Expr], + dataset: &Dataset, + row: &[Value], + params: &[Value], +) -> Result> { + exprs + .iter() + .map(|expr| runtime.eval_expr(expr, dataset, row, params, &BTreeMap::new(), None)) + .collect() +} + +fn render_simple_grouped_numeric_aggregate_groups( + runtime: &EngineRuntime, + groups: Vec, + plan: &SimpleGroupedNumericAggregatePlan<'_>, + params: &[Value], +) -> Result { + if let (Some(projection_exprs), Some(raw_projection_bindings)) = + (&plan.projection_exprs, &plan.raw_projection_bindings) + { + let raw_dataset = Dataset::with_rows(raw_projection_bindings.clone(), Vec::new()); + let mut rows = Vec::with_capacity(groups.len()); + for group in groups { + let mut raw_values = group.group_values.clone(); + raw_values.extend(group.aggregate_values(&plan.aggregate_bindings)); + if let Some(having) = plan.having.as_ref() { + if !matches!( + runtime.eval_expr( + having, + &raw_dataset, + &raw_values, + params, + &BTreeMap::new(), + None, + )?, + Value::Bool(true) + ) { + continue; + } + } + let output = projection_exprs + .iter() + .map(|expr| { + runtime.eval_expr( + expr, + &raw_dataset, + &raw_values, + params, + &BTreeMap::new(), + None, + ) + }) + .collect::>>()?; + rows.push(QueryRow::new(output)); } + return runtime.apply_simple_grouped_postprocessing( + rows, + plan.column_names.clone(), + &[], + None, + params, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + ); } - matched -} - -fn encode_row_id_locator_key(row_id: i64) -> u64 { - (row_id as u64) ^ SIGNED_ROW_ID_BIAS -} -fn decode_row_id_locator_key(key: u64) -> i64 { - (key ^ SIGNED_ROW_ID_BIAS) as i64 -} - -fn first_persistent_pk_row_id( - store: &S, - table_schema: &TableSchema, -) -> Result> { - let Some(pk_index_root) = table_schema.pk_index_root else { - return Ok(None); - }; - let Some(position) = btree_first_position(store, Some(pk_index_root))? else { - return Ok(None); - }; - let (key, _) = btree_materialize_current(store, &position)?; - Ok(Some(decode_row_id_locator_key(key))) + runtime.apply_simple_grouped_postprocessing( + groups + .into_iter() + .map(|group| group.into_row(&plan.aggregate_bindings)), + plan.column_names.clone(), + &plan.having_bindings, + plan.having.as_ref(), + params, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + ) } -#[allow(clippy::too_many_arguments)] -fn try_persistent_pk_ordered_projection_result( - store: &S, - state: PersistedTableState, - table_schema: &TableSchema, - projection_indexes: &[usize], - column_names: Vec, - limit: Option, - offset: usize, - descending: bool, -) -> Result> { - let Some(pk_index_root) = table_schema.pk_index_root else { - return Ok(None); - }; - let take = limit.unwrap_or(usize::MAX); - if take == 0 { - return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); - } - - let mut cursor = if descending { - BtreeCursor::from_end(store, Some(pk_index_root))? - } else { - BtreeCursor::from_start(store, Some(pk_index_root))? - }; - let mut skipped = 0usize; - let mut rows = Vec::with_capacity(take.min(64)); - while let Some((_, payload)) = if descending { - cursor.prev()? - } else { - cursor.next()? - } { - if skipped < offset { - skipped += 1; - continue; - } - let locator = decode_row_locator(&payload)?; - if let Some(values) = - read_deferred_projected_values_by_locator(store, state, locator, projection_indexes)? - { - rows.push(QueryRow::new(values)); - if rows.len() == take { - break; +fn render_simple_grouped_count_groups( + runtime: &EngineRuntime, + groups: Vec, + plan: &SimpleGroupedCountPlan<'_>, + params: &[Value], +) -> Result { + if let (Some(projection_exprs), Some(raw_projection_bindings)) = + (&plan.projection_exprs, &plan.raw_projection_bindings) + { + let raw_dataset = Dataset::with_rows(raw_projection_bindings.clone(), Vec::new()); + let mut rows = Vec::with_capacity(groups.len()); + for group in groups { + let mut raw_values = group.group_values.clone(); + raw_values.extend(group.aggregate_values()); + if let Some(having) = plan.having.as_ref() { + if !matches!( + runtime.eval_expr( + having, + &raw_dataset, + &raw_values, + params, + &BTreeMap::new(), + None, + )?, + Value::Bool(true) + ) { + continue; + } } + let output = projection_exprs + .iter() + .map(|expr| { + runtime.eval_expr( + expr, + &raw_dataset, + &raw_values, + params, + &BTreeMap::new(), + None, + ) + }) + .collect::>>()?; + rows.push(QueryRow::new(output)); } + return runtime.apply_simple_grouped_postprocessing( + rows, + plan.column_names.clone(), + &[], + None, + params, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + ); } - Ok(Some(QueryResult::with_rows(column_names, rows))) -} - -fn encode_row_locator(locator: RowLocatorV1) -> Vec { - let mut bytes = Vec::with_capacity(8); - bytes.extend_from_slice(&locator.byte_offset.to_le_bytes()); - bytes.extend_from_slice(&locator.byte_len.to_le_bytes()); - bytes -} - -fn encode_paged_row_locator(locator: RowLocatorV2) -> Vec { - let mut bytes = Vec::with_capacity(13); - bytes.extend_from_slice(&locator.chunk_index.to_le_bytes()); - bytes.extend_from_slice(&locator.byte_offset.to_le_bytes()); - bytes.extend_from_slice(&locator.byte_len.to_le_bytes()); - bytes.push(if locator.is_overlay { 1 } else { 0 }); - bytes -} - -fn decode_row_locator(bytes: &[u8]) -> Result { - match bytes.len() { - 8 => Ok(DecodedRowLocator::V1(RowLocatorV1 { - byte_offset: u32::from_le_bytes(bytes[0..4].try_into().expect("row locator offset")), - byte_len: u32::from_le_bytes(bytes[4..8].try_into().expect("row locator len")), - })), - 12 => Ok(DecodedRowLocator::V2(RowLocatorV2 { - chunk_index: u32::from_le_bytes(bytes[0..4].try_into().expect("row locator chunk")), - byte_offset: u32::from_le_bytes(bytes[4..8].try_into().expect("row locator offset")), - byte_len: u32::from_le_bytes(bytes[8..12].try_into().expect("row locator len")), - is_overlay: false, - })), - 13 => Ok(DecodedRowLocator::V2(RowLocatorV2 { - chunk_index: u32::from_le_bytes(bytes[0..4].try_into().expect("row locator chunk")), - byte_offset: u32::from_le_bytes(bytes[4..8].try_into().expect("row locator offset")), - byte_len: u32::from_le_bytes(bytes[8..12].try_into().expect("row locator len")), - is_overlay: bytes[12] != 0, - })), - _ => Err(DbError::corruption("row locator payload length is invalid")), - } + runtime.apply_simple_grouped_postprocessing( + groups + .into_iter() + .map(SimpleGroupedCountAggregate::into_row), + plan.column_names.clone(), + &plan.having_bindings, + plan.having.as_ref(), + params, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + ) } -fn append_paged_row_locator_entries( - entries: &mut BTreeMap>, - payload: &[u8], - chunk_index: u32, - is_overlay: bool, - skip: &BTreeSet, -) -> Result<()> { - if payload.is_empty() { +fn update_simple_min_max_value(best: &mut Value, candidate: Value, is_max: bool) -> Result<()> { + if matches!(candidate, Value::Null) { return Ok(()); } - if !payload.starts_with(TABLE_PAYLOAD_MAGIC) { - return Err(DbError::corruption("table payload magic is invalid")); - } - let mut cursor = Cursor::new(payload); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); + if matches!(best, Value::Null) { + *best = candidate; + return Ok(()); } - let row_count = cursor.read_u32()? as usize; - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes_offset = cursor.offset; - cursor.read_slice(row_bytes_len)?; - if is_tombstone || skip.contains(&row_id) { - continue; - } - let locator = RowLocatorV2 { - chunk_index, - byte_offset: u32::try_from(row_bytes_offset) - .map_err(|_| DbError::constraint("row locator offset exceeds u32"))?, - byte_len: u32::try_from(row_bytes_len) - .map_err(|_| DbError::constraint("row locator length exceeds u32"))?, - is_overlay, - }; - entries.insert( - encode_row_id_locator_key(row_id), - encode_paged_row_locator(locator), - ); + let ordering = compare_values(&candidate, best)?; + let should_replace = if is_max { + ordering == std::cmp::Ordering::Greater + } else { + ordering == std::cmp::Ordering::Less + }; + if should_replace { + *best = candidate; } Ok(()) } -fn append_cached_paged_row_locators( - locators: &mut Int64Map, - payload: &[u8], - pointer: OverflowPointer, - checksum: u32, - skip: &BTreeSet, -) -> Result<()> { - if payload.is_empty() { - return Ok(()); - } - let mut cursor = Cursor::new(payload); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes_offset = cursor.offset; - cursor.read_slice(row_bytes_len)?; - if is_tombstone || skip.contains(&row_id) { - continue; +fn expr_references_only_binding(expr: &Expr, binding: TableBindingRef<'_>) -> bool { + match expr { + Expr::Literal(_) | Expr::Parameter(_) => true, + Expr::Column { table, .. } => table + .as_deref() + .is_none_or(|qualifier| identifiers_equal(qualifier, binding.binding_name())), + Expr::Unary { expr, .. } + | Expr::Cast { expr, .. } + | Expr::IsNull { expr, .. } + | Expr::Collate { expr, .. } => expr_references_only_binding(expr, binding), + Expr::Binary { left, right, .. } => { + expr_references_only_binding(left, binding) + && expr_references_only_binding(right, binding) + } + Expr::Between { + expr, low, high, .. + } => { + expr_references_only_binding(expr, binding) + && expr_references_only_binding(low, binding) + && expr_references_only_binding(high, binding) + } + Expr::InList { expr, items, .. } => { + expr_references_only_binding(expr, binding) + && items + .iter() + .all(|item| expr_references_only_binding(item, binding)) + } + Expr::Like { + expr, + pattern, + escape, + .. + } => { + expr_references_only_binding(expr, binding) + && expr_references_only_binding(pattern, binding) + && escape + .as_ref() + .is_none_or(|expr| expr_references_only_binding(expr, binding)) + } + Expr::Function { args, .. } => args + .iter() + .all(|arg| expr_references_only_binding(arg, binding)), + Expr::Case { + operand, + branches, + else_expr, + } => { + operand + .as_ref() + .is_none_or(|expr| expr_references_only_binding(expr, binding)) + && branches.iter().all(|(condition, value)| { + expr_references_only_binding(condition, binding) + && expr_references_only_binding(value, binding) + }) + && else_expr + .as_ref() + .is_none_or(|expr| expr_references_only_binding(expr, binding)) } - locators.insert( - row_id, - CachedPagedRowLocator { - pointer, - checksum, - locator: RowLocatorV1 { - byte_offset: u32::try_from(row_bytes_offset) - .map_err(|_| DbError::constraint("row locator offset exceeds u32"))?, - byte_len: u32::try_from(row_bytes_len) - .map_err(|_| DbError::constraint("row locator length exceeds u32"))?, - }, - }, - ); + Expr::Row(items) => items + .iter() + .all(|item| expr_references_only_binding(item, binding)), + Expr::Aggregate { .. } + | Expr::RowNumber { .. } + | Expr::WindowFunction { .. } + | Expr::InSubquery { .. } + | Expr::CompareSubquery { .. } + | Expr::ScalarSubquery(_) + | Expr::Exists(_) => false, } - Ok(()) } -fn maybe_cache_verified_paged_chunk_payload( - payloads: &mut HashMap>>, - cached_payload_bytes: &mut usize, - pointer: OverflowPointer, - checksum: u32, - payload: &Arc>, -) { - let payload_len = payload.len(); - if payload_len == 0 || payload_len > DEFERRED_PAGED_ROW_PAYLOAD_CACHE_LIMIT_BYTES { - return; - } - if cached_payload_bytes.saturating_add(payload_len) - > DEFERRED_PAGED_ROW_PAYLOAD_CACHE_LIMIT_BYTES - { - return; - } - let key = CachedPagedChunkPayloadKey::new(pointer, checksum); - if payloads.contains_key(&key) { - return; +fn expr_references_binding_names(expr: &Expr, table_name: &str, binding_name: &str) -> bool { + match expr { + Expr::Literal(_) | Expr::Parameter(_) => true, + Expr::Column { table, .. } => table.as_deref().is_none_or(|qualifier| { + identifiers_equal(qualifier, table_name) || identifiers_equal(qualifier, binding_name) + }), + Expr::Unary { expr, .. } + | Expr::Cast { expr, .. } + | Expr::IsNull { expr, .. } + | Expr::Collate { expr, .. } => { + expr_references_binding_names(expr, table_name, binding_name) + } + Expr::Binary { left, right, .. } => { + expr_references_binding_names(left, table_name, binding_name) + && expr_references_binding_names(right, table_name, binding_name) + } + Expr::Between { + expr, low, high, .. + } => { + expr_references_binding_names(expr, table_name, binding_name) + && expr_references_binding_names(low, table_name, binding_name) + && expr_references_binding_names(high, table_name, binding_name) + } + Expr::InList { expr, items, .. } => { + expr_references_binding_names(expr, table_name, binding_name) + && items + .iter() + .all(|item| expr_references_binding_names(item, table_name, binding_name)) + } + Expr::Like { + expr, + pattern, + escape, + .. + } => { + expr_references_binding_names(expr, table_name, binding_name) + && expr_references_binding_names(pattern, table_name, binding_name) + && escape.as_ref().is_none_or(|expr| { + expr_references_binding_names(expr, table_name, binding_name) + }) + } + Expr::Function { args, .. } => args + .iter() + .all(|arg| expr_references_binding_names(arg, table_name, binding_name)), + Expr::Case { + operand, + branches, + else_expr, + } => { + operand + .as_ref() + .is_none_or(|expr| expr_references_binding_names(expr, table_name, binding_name)) + && branches.iter().all(|(condition, value)| { + expr_references_binding_names(condition, table_name, binding_name) + && expr_references_binding_names(value, table_name, binding_name) + }) + && else_expr.as_ref().is_none_or(|expr| { + expr_references_binding_names(expr, table_name, binding_name) + }) + } + Expr::Row(items) => items + .iter() + .all(|item| expr_references_binding_names(item, table_name, binding_name)), + Expr::Aggregate { .. } + | Expr::RowNumber { .. } + | Expr::WindowFunction { .. } + | Expr::InSubquery { .. } + | Expr::CompareSubquery { .. } + | Expr::ScalarSubquery(_) + | Expr::Exists(_) => false, } - *cached_payload_bytes = cached_payload_bytes.saturating_add(payload_len); - payloads.insert(key, Arc::clone(payload)); } -fn build_deferred_paged_row_locator_cache( - state: PersistedTableState, - chunks: &[TablePageManifestChunk], -) -> Result { - let mut locators = if let Some(directory) = try_build_dense_paged_row_directory(chunks)? { - let mut sources = Vec::new(); - try_reserve_paged_directory(&mut sources, chunks.len(), "deferred paged chunk sources")?; - sources.extend(chunks.iter().map(|chunk| CachedPagedChunkSource { - pointer: chunk.pointer, - checksum: chunk.checksum, - })); - DeferredPagedRowLocators::Dense { - directory, - chunks: sources, +fn expr_resolves_against_dataset(expr: &Expr, dataset: &Dataset) -> bool { + match expr { + Expr::Literal(_) | Expr::Parameter(_) => true, + Expr::Column { table, column } => { + simple_select_item_column_index(dataset, table.as_deref(), column).is_some() } - } else { - let expected_locators = chunks.iter().try_fold(0usize, |total, chunk| { - total - .checked_add(chunk.row_count) - .ok_or_else(|| DbError::constraint("paged table row count overflow")) - })?; - let mut sparse = Int64Map::with_hasher(Int64HashBuilder::default()); - sparse.try_reserve(expected_locators).map_err(|error| { - DbError::internal(format!( - "failed to reserve {expected_locators} deferred paged row locators: {error}" - )) - })?; - DeferredPagedRowLocators::Sparse(sparse) - }; - let mut verified_payloads = HashMap::new(); - let mut cached_payload_bytes = 0usize; - for chunk in chunks { - maybe_cache_verified_paged_chunk_payload( - &mut verified_payloads, - &mut cached_payload_bytes, - chunk.pointer, - chunk.checksum, - &chunk.payload, - ); - if let DeferredPagedRowLocators::Sparse(sparse) = &mut locators { - append_cached_paged_row_locators( - sparse, - chunk.payload.as_slice(), - chunk.pointer, - chunk.checksum, - &chunk.tombstoned_row_ids, - )?; + Expr::Unary { expr, .. } + | Expr::Cast { expr, .. } + | Expr::IsNull { expr, .. } + | Expr::Collate { expr, .. } => expr_resolves_against_dataset(expr, dataset), + Expr::Binary { left, right, .. } => { + expr_resolves_against_dataset(left, dataset) + && expr_resolves_against_dataset(right, dataset) } - if let (Some(overlay_pointer), Some(overlay_checksum), Some(overlay_payload)) = ( - chunk.overlay_pointer, - chunk.overlay_checksum, - chunk.overlay_payload.as_ref(), - ) { - maybe_cache_verified_paged_chunk_payload( - &mut verified_payloads, - &mut cached_payload_bytes, - overlay_pointer, - overlay_checksum, - overlay_payload, - ); - if let DeferredPagedRowLocators::Sparse(sparse) = &mut locators { - append_cached_paged_row_locators( - sparse, - overlay_payload.as_slice(), - overlay_pointer, - overlay_checksum, - &BTreeSet::new(), - )?; - } + Expr::Between { + expr, low, high, .. + } => { + expr_resolves_against_dataset(expr, dataset) + && expr_resolves_against_dataset(low, dataset) + && expr_resolves_against_dataset(high, dataset) + } + Expr::InList { expr, items, .. } => { + expr_resolves_against_dataset(expr, dataset) + && items + .iter() + .all(|item| expr_resolves_against_dataset(item, dataset)) + } + Expr::Like { + expr, + pattern, + escape, + .. + } => { + expr_resolves_against_dataset(expr, dataset) + && expr_resolves_against_dataset(pattern, dataset) + && escape + .as_ref() + .is_none_or(|expr| expr_resolves_against_dataset(expr, dataset)) + } + Expr::Function { args, .. } => args + .iter() + .all(|arg| expr_resolves_against_dataset(arg, dataset)), + Expr::Case { + operand, + branches, + else_expr, + } => { + operand + .as_ref() + .is_none_or(|expr| expr_resolves_against_dataset(expr, dataset)) + && branches.iter().all(|(condition, value)| { + expr_resolves_against_dataset(condition, dataset) + && expr_resolves_against_dataset(value, dataset) + }) + && else_expr + .as_ref() + .is_none_or(|expr| expr_resolves_against_dataset(expr, dataset)) } + Expr::Row(items) => items + .iter() + .all(|item| expr_resolves_against_dataset(item, dataset)), + Expr::Aggregate { .. } + | Expr::RowNumber { .. } + | Expr::WindowFunction { .. } + | Expr::InSubquery { .. } + | Expr::CompareSubquery { .. } + | Expr::ScalarSubquery(_) + | Expr::Exists(_) => false, } - Ok(DeferredPagedRowLocatorCache { - manifest_pointer: state.pointer, - manifest_checksum: state.checksum, - locators, - verified_payloads, - }) } -fn build_row_locator_entries(payload: &[u8]) -> Result>> { - if payload.is_empty() { - return Ok(BTreeMap::new()); - } - if !payload.starts_with(TABLE_PAYLOAD_MAGIC) { - return Err(DbError::corruption("table payload magic is invalid")); - } - let mut cursor = Cursor::new(payload); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - let mut entries = BTreeMap::new(); - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes_offset = cursor.offset; - cursor.read_slice(row_bytes_len)?; - if is_tombstone { - continue; - } - let locator = RowLocatorV1 { - byte_offset: u32::try_from(row_bytes_offset) - .map_err(|_| DbError::constraint("row locator offset exceeds u32"))?, - byte_len: u32::try_from(row_bytes_len) - .map_err(|_| DbError::constraint("row locator length exceeds u32"))?, - }; - entries.insert( - encode_row_id_locator_key(row_id), - encode_row_locator(locator), - ); +fn grouped_projection_expr_matches_group_expr( + projection_expr: &Expr, + group_expr: &Expr, + binding: TableBindingRef<'_>, +) -> bool { + if projection_expr == group_expr { + return true; } - Ok(entries) + matches!(group_expr, Expr::Column { column, .. } if expr_matches_binding_column(projection_expr, binding, column)) } -fn build_paged_row_locator_entries_from_chunk_payloads( - chunk_payloads: &[TablePageManifestChunk], -) -> Result>> { - let mut entries = BTreeMap::new(); - for (chunk_index, chunk) in chunk_payloads.iter().enumerate() { - let skip = chunk.tombstoned_row_ids.iter().copied().collect(); - append_paged_row_locator_entries( - &mut entries, - chunk.payload.as_slice(), - u32::try_from(chunk_index) - .map_err(|_| DbError::constraint("paged table chunk index exceeds u32"))?, - false, - &skip, - )?; - if let Some(overlay) = &chunk.overlay_payload { - append_paged_row_locator_entries( - &mut entries, - overlay.as_slice(), - u32::try_from(chunk_index) - .map_err(|_| DbError::constraint("paged table chunk index exceeds u32"))?, - true, - &BTreeSet::new(), - )?; +#[derive(Clone, Debug)] +struct BenchmarkReportAggregate { + item_name: String, + quantity_total: i64, + revenue_total: f64, +} + +impl BenchmarkReportAggregate { + fn new(item_name: String) -> Self { + Self { + item_name, + quantity_total: 0, + revenue_total: 0.0, } } - Ok(entries) } -fn deferred_compressed_lookup_cache() -> &'static Mutex { - DEFERRED_COMPRESSED_LOOKUP_CACHE - .get_or_init(|| Mutex::new(DeferredCompressedLookupCache::default())) +#[derive(Clone, Debug)] +pub(crate) struct SimpleOrderByPlan { + projection_index: usize, + descending: bool, + collation: Option, } -fn deferred_runtime_btree_index_cache() -> &'static Mutex { - DEFERRED_RUNTIME_BTREE_INDEX_CACHE - .get_or_init(|| Mutex::new(DeferredRuntimeBtreeIndexCache::default())) +#[derive(Clone, Copy, Debug)] +enum SimpleExpressionProjectionSource<'a> { + Column(usize), + Expr(&'a Expr), } -fn cached_deferred_runtime_btree_index( - key: &DeferredRuntimeBtreeIndexCacheKey, -) -> Result>> { - let cache = deferred_runtime_btree_index_cache() - .lock() - .map_err(|_| DbError::internal("deferred runtime index cache lock poisoned"))?; - Ok(cache.entries.get(key).cloned()) +#[derive(Clone, Debug)] +pub(crate) struct SimpleExpressionProjectionPlan<'a> { + sources: Vec>, + column_names: Vec, } -fn cache_deferred_runtime_btree_index( - key: DeferredRuntimeBtreeIndexCacheKey, - entry: DeferredRuntimeBtreeIndexCacheEntry, -) -> Result<()> { - let mut cache = deferred_runtime_btree_index_cache() - .lock() - .map_err(|_| DbError::internal("deferred runtime index cache lock poisoned"))?; - if !cache.entries.contains_key(&key) { - if cache.entries.len() >= DEFERRED_RUNTIME_BTREE_INDEX_CACHE_LIMIT { - if let Some(evicted) = cache.insertion_order.pop_front() { - cache.entries.remove(&evicted); - } - } - cache.insertion_order.push_back(key.clone()); - } - cache.entries.insert(key, Arc::new(entry)); - Ok(()) +#[derive(Clone, Debug)] +pub(crate) enum SimpleJoinProjectionSource { + Left(usize), + Right(usize), + Expr(Expr), } -fn deferred_compressed_lookup_cache_key( - state: PersistedTableState, -) -> DeferredCompressedLookupCacheKey { - DeferredCompressedLookupCacheKey { - head_page_id: state.pointer.head_page_id, - logical_len: state.pointer.logical_len, - flags: state.pointer.flags, - checksum: state.checksum, - } +#[derive(Clone, Debug)] +pub(crate) struct SimpleRangeProjectionFilter<'a> { + table: Option<&'a str>, + column: &'a str, + lower: Option>, + upper: Option>, + residual: Vec>, } -fn decode_compressed_table_payload_lookup_entry( - payload: Arc>, -) -> Result { - let mut cursor = Cursor::new(payload.as_slice()); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - let mut row_locators = HashMap::with_capacity(row_count); - for _ in 0..row_count { - let row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes_offset = cursor.offset; - let _ = cursor.read_slice(row_bytes_len)?; - if is_tombstone { - continue; - } - row_locators.insert( - row_id, - RowLocatorV1 { - byte_offset: u32::try_from(row_bytes_offset) - .map_err(|_| DbError::constraint("row locator offset exceeds u32"))?, - byte_len: u32::try_from(row_bytes_len) - .map_err(|_| DbError::constraint("row locator length exceeds u32"))?, - }, - ); - } - Ok(DeferredCompressedLookupCacheEntry { - payload, - row_locators, - }) +#[derive(Clone, Copy, Debug)] +struct SimpleResidualFilterTerm<'a> { + table: Option<&'a str>, + column: &'a str, + op: BinaryOp, + value_expr: &'a Expr, } -fn read_deferred_compressed_table_lookup_entry( - store: &S, - state: PersistedTableState, -) -> Result> { - let cache_key = deferred_compressed_lookup_cache_key(state); - { - let cache = deferred_compressed_lookup_cache() - .lock() - .map_err(|_| DbError::internal("deferred compressed lookup cache lock poisoned"))?; - if let Some(entry) = cache.entries.get(&cache_key) { - return Ok(entry.clone()); - } - } +#[derive(Clone, Debug)] +pub(crate) struct SimpleResidualPlan { + column_index: usize, + op: BinaryOp, + value: Value, +} - let payload = Arc::new(read_overflow(store, state.pointer)?); - if crc32c_parts(&[payload.as_slice()]) != state.checksum { - return Err(DbError::corruption( - "deferred table payload checksum mismatch", - )); - } - let entry = Arc::new(decode_compressed_table_payload_lookup_entry(payload)?); +fn residual_like_filter_can_use_direct_scan(filter: &Expr) -> bool { + simple_contains_like_projection_filter(filter).is_some() +} - let mut cache = deferred_compressed_lookup_cache() - .lock() - .map_err(|_| DbError::internal("deferred compressed lookup cache lock poisoned"))?; - if let Some(existing) = cache.entries.get(&cache_key) { - return Ok(existing.clone()); - } - if cache.entries.len() >= DEFERRED_COMPRESSED_LOOKUP_CACHE_LIMIT { - if let Some(evicted) = cache.insertion_order.pop_front() { - cache.entries.remove(&evicted); - } +#[derive(Clone, Debug, Default)] +pub(crate) struct SimpleRangeFilterState<'a> { + table: Option<&'a str>, + column: Option<&'a str>, + lower: Option>, + upper: Option>, + residual: Vec>, +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum SimpleRangeBoundKind { + Lower(bool), + Upper(bool), +} + +fn reverse_binary_op(op: BinaryOp) -> Option { + match op { + BinaryOp::Gt => Some(BinaryOp::Lt), + BinaryOp::GtEq => Some(BinaryOp::LtEq), + BinaryOp::Lt => Some(BinaryOp::Gt), + BinaryOp::LtEq => Some(BinaryOp::GtEq), + _ => None, } - cache.insertion_order.push_back(cache_key); - cache.entries.insert(cache_key, entry.clone()); - Ok(entry) } -fn decode_row_by_locator_from_payload( - payload: &[u8], - row_id: i64, - locator: RowLocatorV1, -) -> Result { - let start = locator.byte_offset as usize; - let end = start - .checked_add(locator.byte_len as usize) - .ok_or_else(|| DbError::corruption("row locator exceeded payload length"))?; - let row_bytes = payload - .get(start..end) - .ok_or_else(|| DbError::corruption("row locator exceeded payload length"))?; - let row = Row::decode(row_bytes)?; - Ok(StoredRow { - row_id, - values: row.into_values(), - }) +#[derive(Clone, Copy, Debug)] +pub(crate) struct QualifiedColumnRef<'a> { + table: Option<&'a str>, + column: &'a str, } -fn decode_projected_values_by_locator_from_payload( - store: Option<&S>, - payload: &[u8], - locator: RowLocatorV1, - projection_indexes: &[usize], -) -> Result> { - let start = locator.byte_offset as usize; - let end = start - .checked_add(locator.byte_len as usize) - .ok_or_else(|| DbError::corruption("row locator exceeded payload length"))?; - let row_bytes = payload - .get(start..end) - .ok_or_else(|| DbError::corruption("row locator exceeded payload length"))?; - Row::decode_projection_sorted_unique_with_overflow(row_bytes, store, projection_indexes) +#[derive(Clone, Copy, Debug)] +pub(crate) struct TableBindingRef<'a> { + name: &'a str, + alias: &'a Option, } -fn read_deferred_row_by_locator_from_table_payload( - store: &S, - state: PersistedTableState, - row_id: i64, - locator: RowLocatorV1, -) -> Result> { - let pointer = state.pointer; - if pointer.head_page_id == 0 { - return Ok(None); - } - if pointer.is_compressed() { - let entry = read_deferred_compressed_table_lookup_entry(store, state)?; - return decode_row_by_locator_from_payload(entry.payload.as_slice(), row_id, locator) - .map(Some); +impl<'a> TableBindingRef<'a> { + fn binding_name(self) -> &'a str { + self.alias.as_deref().unwrap_or(self.name) } - let mut cursor = OverflowPayloadCursor::new(store, pointer); - cursor.skip(locator.byte_offset as usize)?; - let row_bytes = cursor.read_vec(locator.byte_len as usize)?; - let row = Row::decode(&row_bytes)?; - Ok(Some(StoredRow { - row_id, - values: row.into_values(), - })) } -fn read_deferred_row_by_locator_from_paged_table_payload( - store: &S, - state: PersistedTableState, - row_id: i64, - locator: RowLocatorV2, -) -> Result> { - let manifest_payload = read_overflow(store, state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); - } - let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - let chunk = manifest - .chunks - .get(locator.chunk_index as usize) - .ok_or_else(|| DbError::corruption("paged table locator chunk index is invalid"))?; - let pointer = if locator.is_overlay { - chunk.overlay_pointer.ok_or_else(|| { - DbError::corruption("paged table overlay pointer missing for overlay locator") - })? - } else { - chunk.pointer - }; - let payload = read_overflow(store, pointer)?; - let start = locator.byte_offset as usize; - let end = start.saturating_add(locator.byte_len as usize); - let row_bytes = payload - .get(start..end) - .ok_or_else(|| DbError::corruption("paged row locator exceeded payload length"))?; - let row = Row::decode(row_bytes)?; - Ok(Some(StoredRow { - row_id, - values: row.into_values(), - })) +#[derive(Clone, Copy, Debug)] +struct SpatialJoinOrientation<'a> { + indexed_table: TableBindingRef<'a>, + indexed_ref: QualifiedColumnRef<'a>, + probe_table: TableBindingRef<'a>, + probe_ref: QualifiedColumnRef<'a>, + indexed_on_left: bool, + left_alias: &'a Option, + right_alias: &'a Option, + constraint: &'a JoinConstraint, + radius_expr: Option<&'a Expr>, + params: &'a [Value], + ctes: &'a BTreeMap, } -fn read_deferred_row_by_cached_paged_locator( - store: &S, - row_id: i64, - cached: CachedPagedRowLocator, - verified_payload: Option<&[u8]>, -) -> Result> { - let owned_payload; - let payload = if let Some(payload) = verified_payload { - payload - } else { - owned_payload = read_overflow(store, cached.pointer)?; - if crc32c_parts(&[owned_payload.as_slice()]) != cached.checksum { - return Err(DbError::corruption("paged table chunk checksum mismatch")); - } - owned_payload.as_slice() - }; - decode_row_by_locator_from_payload(payload, row_id, cached.locator).map(Some) +#[derive(Clone, Debug)] +pub(crate) struct IndexedJoinPlan<'a> { + filtered_table: TableBindingRef<'a>, + filtered_dataset: &'a Dataset, + filtered_join_columns: Vec<&'a str>, + probe_table: TableBindingRef<'a>, + probe_join_columns: Vec<&'a str>, + filtered_on_left: bool, } -fn read_deferred_projected_values_by_cached_paged_locator( - store: &S, - cached: CachedPagedRowLocator, - verified_payload: Option<&[u8]>, - projection_indexes: &[usize], -) -> Result> { - let owned_payload; - let payload = if let Some(payload) = verified_payload { - payload - } else { - owned_payload = read_overflow(store, cached.pointer)?; - if crc32c_parts(&[owned_payload.as_slice()]) != cached.checksum { - return Err(DbError::corruption("paged table chunk checksum mismatch")); - } - owned_payload.as_slice() - }; - decode_projected_values_by_locator_from_payload( - Some(store), - payload, - cached.locator, - projection_indexes, - ) +#[derive(Clone, Copy, Debug)] +struct DeferredViewProjection { + table_index: usize, + column_index: usize, + is_rowid_alias: bool, } -fn read_deferred_projected_values_by_cached_paged_locator_with_query_cache( - store: &S, - cached: CachedPagedRowLocator, - verified_payload: Option<&Arc>>, - chunk_payload_cache: &mut HashMap>>, - projection_indexes: &[usize], -) -> Result> { - if let Some(payload) = verified_payload { - return decode_projected_values_by_locator_from_payload( - Some(store), - payload.as_slice(), - cached.locator, - projection_indexes, - ); - } - let key = CachedPagedChunkPayloadKey::new(cached.pointer, cached.checksum); - if let Some(payload) = chunk_payload_cache.get(&key) { - return decode_projected_values_by_locator_from_payload( - Some(store), - payload.as_slice(), - cached.locator, - projection_indexes, - ); - } - - let payload = Arc::new(read_overflow(store, cached.pointer)?); - if crc32c_parts(&[payload.as_slice()]) != cached.checksum { - return Err(DbError::corruption("paged table chunk checksum mismatch")); - } - let values = decode_projected_values_by_locator_from_payload( - Some(store), - payload.as_slice(), - cached.locator, - projection_indexes, - )?; - chunk_payload_cache.insert(key, payload); - Ok(values) +#[derive(Clone, Debug)] +pub(crate) struct DeferredViewJoinStep { + previous_table_index: usize, + previous_column_index: usize, + current_table_index: usize, + current_index_name: String, + /// True when `previous_column_index` is the previous table's row-id alias + /// (single-column `INTEGER PRIMARY KEY` auto-increment column). In that case + /// the join key value is exactly the previous row's `row_id`, so it never + /// needs to be decoded from the projected row and is omitted from the + /// projection entirely. + previous_is_rowid_alias: bool, } -fn read_deferred_projected_values_by_locator_from_table_payload( - store: &S, - state: PersistedTableState, - locator: RowLocatorV1, - projection_indexes: &[usize], -) -> Result>> { - let pointer = state.pointer; - if pointer.head_page_id == 0 { - return Ok(None); - } - if pointer.is_compressed() { - let entry = read_deferred_compressed_table_lookup_entry(store, state)?; - return decode_projected_values_by_locator_from_payload( - Some(store), - entry.payload.as_slice(), - locator, - projection_indexes, - ) - .map(Some); - } - - let mut cursor = OverflowPayloadCursor::new(store, pointer); - cursor.skip(locator.byte_offset as usize)?; - let row_bytes = cursor.read_vec(locator.byte_len as usize)?; - Row::decode_projection_sorted_unique_with_overflow( - row_bytes.as_slice(), - Some(store), - projection_indexes, - ) - .map(Some) +#[derive(Clone)] +pub(crate) enum DeferredViewTableRowReader<'a> { + Source(VisibleTableRowSource<'a>), + Deferred { + state: PersistedTableState, + schema: &'a TableSchema, + paged_locator_cache: Option<&'a DeferredPagedRowLocatorCache>, + }, } -fn read_deferred_projected_values_by_locator_from_paged_table_payload( - store: &S, - state: PersistedTableState, - locator: RowLocatorV2, - projection_indexes: &[usize], -) -> Result>> { - let manifest_payload = read_overflow(store, state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); +impl<'a> DeferredViewTableRowReader<'a> { + /// Reads from data already owned by the observed-current runtime. + /// + /// The outer `Option` reports whether the lookup can be completed without + /// storage; the inner `Option` distinguishes an absent row from a row that + /// was decoded successfully. Verified paged payloads were checksummed when + /// the immutable locator cache was built. + fn read_projected_from_observed_cache( + &self, + row_id: i64, + projection_indexes: &[usize], + ) -> Result>> { + match *self { + Self::Source(source) => source + .projected_values_by_id(row_id, projection_indexes) + .map(|values| Some(values.map(|values| StoredRow { row_id, values }))), + Self::Deferred { + state, + paged_locator_cache, + .. + } => { + let Some(cache) = paged_locator_cache.filter(|cache| cache.matches_state(state)) + else { + return Ok(None); + }; + let Some(cached) = cache.locators.get(row_id) else { + return Ok(Some(None)); + }; + let Some(payload) = cache.verified_payload(cached.pointer, cached.checksum) else { + return Ok(None); + }; + decode_projected_values_by_locator_from_payload::( + None, + payload, + cached.locator, + projection_indexes, + ) + .map(|values| Some(Some(StoredRow { row_id, values }))) + } + } } - let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - let chunk = manifest - .chunks - .get(locator.chunk_index as usize) - .ok_or_else(|| DbError::corruption("paged table locator chunk index is invalid"))?; - let pointer = if locator.is_overlay { - chunk.overlay_pointer.ok_or_else(|| { - DbError::corruption("paged table overlay pointer missing for overlay locator") - })? - } else { - chunk.pointer - }; - let payload = read_overflow(store, pointer)?; - decode_projected_values_by_locator_from_payload( - Some(store), - payload.as_slice(), - RowLocatorV1 { - byte_offset: locator.byte_offset, - byte_len: locator.byte_len, - }, - projection_indexes, - ) - .map(Some) -} -fn read_deferred_projected_values_by_locator( - store: &S, - state: PersistedTableState, - locator: DecodedRowLocator, - projection_indexes: &[usize], -) -> Result>> { - if state.pointer.is_table_paged_manifest() { - return match locator { - DecodedRowLocator::V2(locator) => { - read_deferred_projected_values_by_locator_from_paged_table_payload( + fn read_projected_with_chunk_cache( + &self, + store: &S, + row_id: i64, + use_persistent_pk_index: bool, + projection_indexes: &[usize], + chunk_payload_cache: &mut HashMap>>, + ) -> Result> { + match *self { + Self::Source(source) => source + .projected_values_by_id( + row_id, + if projection_indexes.is_empty() { + &[] + } else { + projection_indexes + }, + ) + .map(|values| values.map(|values| StoredRow { row_id, values })), + Self::Deferred { + state, + schema, + paged_locator_cache, + } => { + if !projection_indexes.is_empty() && state.pointer.is_table_paged_manifest() { + if let Some(cache) = + paged_locator_cache.filter(|cache| cache.matches_state(state)) + { + return cache + .locators + .get(row_id) + .map(|cached| { + read_deferred_projected_values_by_cached_paged_locator_with_query_cache( + store, + cached, + cache.verified_payload_arc(cached.pointer, cached.checksum), + chunk_payload_cache, + projection_indexes, + ) + }) + .transpose() + .map(|values| values.map(|values| StoredRow { row_id, values })); + } + } + read_deferred_projected_values_by_id( store, state, - locator, + schema, + row_id, + use_persistent_pk_index, + paged_locator_cache, projection_indexes, ) + .map(|values| values.map(|values| StoredRow { row_id, values })) } - DecodedRowLocator::V1(_) => Err(DbError::corruption( - "paged table persistent pk locator payload is invalid", - )), - }; - } - - match locator { - DecodedRowLocator::V1(locator) => { - read_deferred_projected_values_by_locator_from_table_payload( - store, - state, - locator, - projection_indexes, - ) - } - DecodedRowLocator::V2(locator) => { - read_deferred_projected_values_by_locator_from_table_payload( - store, - state, - RowLocatorV1 { - byte_offset: locator.byte_offset, - byte_len: locator.byte_len, - }, - projection_indexes, - ) } } } -fn read_deferred_row_by_id_from_paged_chunk( - store: &S, - chunk: &PersistedTableChunkState, - row_id: i64, -) -> Result> { - if let Some(overlay_pointer) = chunk.overlay_pointer { - let overlay_payload = read_overflow(store, overlay_pointer)?; - if Some(crc32c_parts(&[overlay_payload.as_slice()])) != chunk.overlay_checksum { - return Err(DbError::corruption( - "paged table overlay chunk checksum mismatch", - )); - } - if let Some(row) = read_row_from_table_payload_by_id(overlay_payload.as_slice(), row_id)? { - return Ok(Some(row)); - } - } - - if !chunk.tombstoned_row_ids.is_empty() && chunk.tombstoned_row_ids.contains(&row_id) { - return Ok(None); - } - - let payload = read_overflow(store, chunk.pointer)?; - if crc32c_parts(&[payload.as_slice()]) != chunk.checksum { - return Err(DbError::corruption("paged table chunk checksum mismatch")); - } - read_row_from_table_payload_by_id(payload.as_slice(), row_id) +#[derive(Clone, Debug)] +pub(crate) struct DeferredViewTableProjection { + projection_indexes: Vec, + projected_positions: Vec>, } -fn read_row_from_table_payload_by_id(payload: &[u8], row_id: i64) -> Result> { - if payload.is_empty() { - return Ok(None); - } - let mut cursor = Cursor::new(payload); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - let row_count = cursor.read_u32()? as usize; - for _ in 0..row_count { - let candidate_row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - let row_bytes = cursor.read_slice(row_bytes_len)?; - if is_tombstone { - continue; +impl DeferredViewTableProjection { + fn new(schema: &TableSchema, required_columns: BTreeSet) -> Self { + let projection_indexes = required_columns.into_iter().collect::>(); + let mut projected_positions = vec![None; schema.columns.len()]; + for (position, column_index) in projection_indexes.iter().copied().enumerate() { + projected_positions[column_index] = Some(position); } - if candidate_row_id == row_id { - let row = Row::decode(row_bytes)?; - return Ok(Some(StoredRow { - row_id: candidate_row_id, - values: row.into_values(), - })); + Self { + projection_indexes, + projected_positions, } } - Ok(None) -} -fn manifest_chunk_index_for_row_position( - chunks: &[PersistedTableChunkState], - row_position: usize, -) -> Option { - let mut start = 0usize; - for (index, chunk) in chunks.iter().enumerate() { - let end = start.saturating_add(chunk.row_count); - if row_position < end { - return Some(index); - } - start = end; + fn position(&self, column_index: usize) -> Option { + self.projected_positions + .get(column_index) + .copied() + .flatten() } - None } -fn read_deferred_row_by_id_from_paged_table_manifest( - store: &S, - state: PersistedTableState, - row_id: i64, -) -> Result> { - let manifest_payload = read_overflow(store, state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); - } - let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - let total_row_count = manifest - .chunks +fn build_deferred_view_table_projections( + table_schemas: &[&TableSchema], + projections: &[DeferredViewProjection], + join_steps: &[DeferredViewJoinStep], +) -> Vec { + let mut required_columns = table_schemas .iter() - .fold(0usize, |total, chunk| total.saturating_add(chunk.row_count)); - if state.row_count != 0 && total_row_count != state.row_count { - return Err(DbError::corruption( - "paged table manifest row count mismatch", - )); - } - - let mut checked_chunks = BTreeSet::new(); - for position in [ - row_id - .checked_sub(1) - .and_then(|position| usize::try_from(position).ok()), - usize::try_from(row_id).ok(), - ] - .into_iter() - .flatten() - { - let Some(chunk_index) = manifest_chunk_index_for_row_position(&manifest.chunks, position) - else { - continue; - }; - if !checked_chunks.insert(chunk_index) { + .map(|_| BTreeSet::new()) + .collect::>(); + for projection in projections { + if projection.is_rowid_alias { continue; } - if let Some(row) = - read_deferred_row_by_id_from_paged_chunk(store, &manifest.chunks[chunk_index], row_id)? - { - return Ok(Some(row)); + required_columns[projection.table_index].insert(projection.column_index); + } + for step in join_steps { + if step.previous_is_rowid_alias { + continue; } + required_columns[step.previous_table_index].insert(step.previous_column_index); } + table_schemas + .iter() + .zip(required_columns) + .map(|(schema, columns)| DeferredViewTableProjection::new(schema, columns)) + .collect() +} - for (chunk_index, chunk) in manifest.chunks.iter().enumerate() { - if checked_chunks.contains(&chunk_index) { +#[derive(Clone, Copy, Debug)] +pub(crate) enum DeferredViewProjectionSource { + RowId { + table_index: usize, + }, + Projected { + table_index: usize, + projected_index: usize, + }, +} + +fn build_deferred_view_projection_indexes( + projections: &[DeferredViewProjection], + table_projections: &[DeferredViewTableProjection], + context: &str, +) -> Result> { + let mut projection_indexes = Vec::with_capacity(projections.len()); + for projection in projections { + if projection.is_rowid_alias { + projection_indexes.push(DeferredViewProjectionSource::RowId { + table_index: projection.table_index, + }); continue; } - if let Some(row) = read_deferred_row_by_id_from_paged_chunk(store, chunk, row_id)? { - return Ok(Some(row)); - } + let projected_index = table_projections[projection.table_index] + .position(projection.column_index) + .ok_or_else(|| { + DbError::internal(format!( + "{context} projection is missing required column index {}", + projection.column_index + )) + })?; + projection_indexes.push(DeferredViewProjectionSource::Projected { + table_index: projection.table_index, + projected_index, + }); } - Ok(None) + Ok(projection_indexes) } -fn read_deferred_stored_row_by_id( - store: &S, - state: PersistedTableState, - table_schema: &TableSchema, - row_id: i64, - use_persistent_pk_index: bool, - paged_locator_cache: Option<&DeferredPagedRowLocatorCache>, -) -> Result> { - let locator = if use_persistent_pk_index { - if let Some(pk_index_root) = table_schema.pk_index_root { - btree_find_exact( - store, - Some(pk_index_root), - encode_row_id_locator_key(row_id), - )? - .map(|payload| decode_row_locator(&payload)) - .transpose()? - } else { - None - } - } else { - None - }; - - if state.pointer.is_table_paged_manifest() { - if let Some(cache) = paged_locator_cache.filter(|cache| cache.matches_state(state)) { - return cache - .locators - .get(row_id) - .map(|cached| { - read_deferred_row_by_cached_paged_locator( - store, - row_id, - cached, - cache.verified_payload(cached.pointer, cached.checksum), - ) - }) - .unwrap_or(Ok(None)); - } - return match locator { - Some(DecodedRowLocator::V2(locator)) => { - read_deferred_row_by_locator_from_paged_table_payload(store, state, row_id, locator) +fn collect_deferred_view_projection_values( + partial_rows: &[StoredRow], + projection_indexes: &[DeferredViewProjectionSource], + context: &str, +) -> Result> { + let mut values = Vec::with_capacity(projection_indexes.len()); + if projection_indexes.is_empty() { + return Ok(values); + } + for source in projection_indexes.iter().copied() { + let (table_index, projected_index) = match source { + DeferredViewProjectionSource::RowId { table_index } => { + let Some(row) = partial_rows.get(table_index) else { + return Err(DbError::internal(format!( + "{context} row is shorter than planned schema", + ))); + }; + values.push(Value::Int64(row.row_id)); + continue; } - _ => read_deferred_row_by_id_from_paged_table_manifest(store, state, row_id), + DeferredViewProjectionSource::Projected { + table_index, + projected_index, + } => (table_index, projected_index), + }; + let Some(row) = partial_rows.get(table_index) else { + return Err(DbError::internal(format!( + "{context} row is shorter than planned schema", + ))); }; + let Some(value) = row.values.get(projected_index) else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + values.push(value.clone()); } + Ok(values) +} - if let Some(locator) = locator { - return match locator { - DecodedRowLocator::V1(locator) => { - read_deferred_row_by_locator_from_table_payload(store, state, row_id, locator) - } - DecodedRowLocator::V2(locator) => read_deferred_row_by_locator_from_table_payload( - store, - state, - row_id, - RowLocatorV1 { - byte_offset: locator.byte_offset, - byte_len: locator.byte_len, - }, - ), +fn deferred_view_linear_tail_projection_can_move( + projection_indexes: &[DeferredViewProjectionSource], +) -> bool { + let mut previous = None; + for source in projection_indexes { + let DeferredViewProjectionSource::Projected { + table_index: 2, + projected_index, + } = *source + else { + continue; }; + if previous.is_some_and(|previous| projected_index <= previous) { + return false; + } + previous = Some(projected_index); } - - read_deferred_row_by_id_from_table_payload(store, state, row_id) + true } -fn read_deferred_projected_values_by_id( - store: &S, - state: PersistedTableState, - table_schema: &TableSchema, - row_id: i64, - use_persistent_pk_index: bool, - paged_locator_cache: Option<&DeferredPagedRowLocatorCache>, - projection_indexes: &[usize], -) -> Result>> { - if projection_indexes.is_empty() { - if state.pointer.is_compressed() { - let entry = read_deferred_compressed_table_lookup_entry(store, state)?; - if entry.row_locators.contains_key(&row_id) { - return Ok(Some(Vec::new())); - } - } - if use_persistent_pk_index { - if let Some(pk_index_root) = table_schema.pk_index_root { - if btree_find_exact( - store, - Some(pk_index_root), - encode_row_id_locator_key(row_id), - )? - .is_some() - { - return Ok(Some(Vec::new())); - } - } - } - if state.pointer.is_table_paged_manifest() - && paged_locator_cache - .filter(|cache| cache.matches_state(state)) - .and_then(|cache| cache.locators.get(row_id)) - .is_some() +fn collect_deferred_view_query_row_from_linear_tail( + root_row: &StoredRow, + row1: &StoredRow, + row2: StoredRow, + projection_indexes: &[DeferredViewProjectionSource], + context: &str, + row2_can_move: bool, +) -> Result { + if row2_can_move && projection_indexes.len() == 4 { + if let [DeferredViewProjectionSource::RowId { table_index: 0 }, DeferredViewProjectionSource::Projected { + table_index: 0, + projected_index: 0, + }, DeferredViewProjectionSource::Projected { + table_index: 1, + projected_index: 0, + }, DeferredViewProjectionSource::Projected { + table_index: 2, + projected_index: 0, + }] = projection_indexes { - return Ok(Some(Vec::new())); + let Some(root_value) = root_row.values.first() else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + let Some(row1_value) = row1.values.first() else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + let mut row2_values = row2.values.into_iter(); + let Some(row2_value) = row2_values.next() else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + return Ok(QueryRow::from_small_values(smallvec![ + Value::Int64(root_row.row_id), + root_value.clone(), + row1_value.clone(), + row2_value, + ])); } - - return read_deferred_stored_row_by_id( - store, - state, - table_schema, - row_id, - use_persistent_pk_index, - paged_locator_cache, - ) - .map(|row| row.map(|_| Vec::new())); } - if state.pointer.is_table_paged_manifest() { - if let Some(cache) = paged_locator_cache.filter(|cache| cache.matches_state(state)) { - return cache - .locators - .get(row_id) - .map(|cached| { - read_deferred_projected_values_by_cached_paged_locator( - store, - cached, - cache.verified_payload(cached.pointer, cached.checksum), - projection_indexes, - ) - }) - .transpose(); + if row2_can_move && projection_indexes.len() == 3 { + if let [DeferredViewProjectionSource::Projected { + table_index: 1, + projected_index: 0, + }, DeferredViewProjectionSource::Projected { + table_index: 2, + projected_index: 0, + }, DeferredViewProjectionSource::Projected { + table_index: 2, + projected_index: 1, + }] = projection_indexes + { + let Some(row1_value) = row1.values.first() else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + let mut row2_values = row2.values.into_iter(); + let Some(row2_first) = row2_values.next() else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + let Some(row2_second) = row2_values.next() else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + return Ok(QueryRow::from_small_values(smallvec![ + row1_value.clone(), + row2_first, + row2_second, + ])); } } - read_deferred_stored_row_by_id( - store, - state, - table_schema, - row_id, - use_persistent_pk_index, - paged_locator_cache, + collect_deferred_view_projection_values_from_linear_tail( + root_row, + row1, + row2, + projection_indexes, + context, + row2_can_move, ) - .map(|row| row.map(|row| project_simple_projection_value_vec(&row.values, projection_indexes))) -} - -fn deferred_rowid_lookup_available( - state: PersistedTableState, - table_schema: &TableSchema, - use_persistent_pk_index: bool, - paged_locator_cache: Option<&DeferredPagedRowLocatorCache>, -) -> bool { - state.pointer.is_compressed() - || (use_persistent_pk_index && table_schema.pk_index_root.is_some()) - || paged_locator_cache.is_some_and(|cache| cache.matches_state(state)) + .map(QueryRow::new) } -fn read_table_payload_row_count( - store: &S, - pointer: OverflowPointer, -) -> Result { - if pointer.head_page_id == 0 || pointer.logical_len == 0 { - return Ok(0); - } - if pointer.is_compressed() { - let payload = read_overflow(store, pointer)?; - let mut cursor = Cursor::new(&payload); - let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; - if magic != TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); +fn collect_deferred_view_projection_values_from_linear_tail( + root_row: &StoredRow, + row1: &StoredRow, + row2: StoredRow, + projection_indexes: &[DeferredViewProjectionSource], + context: &str, + row2_can_move: bool, +) -> Result> { + if row2_can_move && projection_indexes.len() == 4 { + if let [DeferredViewProjectionSource::RowId { table_index: 0 }, DeferredViewProjectionSource::Projected { + table_index: 0, + projected_index: 0, + }, DeferredViewProjectionSource::Projected { + table_index: 1, + projected_index: 0, + }, DeferredViewProjectionSource::Projected { + table_index: 2, + projected_index: 0, + }] = projection_indexes + { + let Some(root_value) = root_row.values.first() else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + let Some(row1_value) = row1.values.first() else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + let mut row2_values = row2.values.into_iter(); + let Some(row2_value) = row2_values.next() else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + return Ok(vec![ + Value::Int64(root_row.row_id), + root_value.clone(), + row1_value.clone(), + row2_value, + ]); } - return Ok(cursor.read_u32()? as usize); - } - - let mut cursor = OverflowPayloadCursor::new(store, pointer); - let mut magic = [0_u8; TABLE_PAYLOAD_MAGIC.len()]; - cursor.read_exact(&mut magic)?; - if magic != *TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); - } - Ok(cursor.read_u32()? as usize) -} - -pub(crate) fn read_persisted_table_row_count( - store: &S, - state: PersistedTableState, -) -> Result { - if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { - return Ok(0); - } - if !state.pointer.is_table_paged_manifest() { - let payload = read_overflow(store, state.pointer)?; - return read_table_payload_live_row_count_from_bytes(&payload); } - let manifest_payload = read_overflow(store, state.pointer)?; - if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { - return Err(DbError::corruption( - "paged table manifest checksum mismatch", - )); - } - let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; - let mut row_count = 0usize; - for chunk in manifest.chunks { - if chunk.tombstoned_row_ids.is_empty() && chunk.overlay_pointer.is_none() { - row_count = row_count.saturating_add(chunk.row_count); - continue; + if row2_can_move && projection_indexes.len() == 3 { + if let [DeferredViewProjectionSource::Projected { + table_index: 1, + projected_index: 0, + }, DeferredViewProjectionSource::Projected { + table_index: 2, + projected_index: 0, + }, DeferredViewProjectionSource::Projected { + table_index: 2, + projected_index: 1, + }] = projection_indexes + { + let Some(row1_value) = row1.values.first() else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + let mut row2_values = row2.values.into_iter(); + let Some(row2_first) = row2_values.next() else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + let Some(row2_second) = row2_values.next() else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + return Ok(vec![row1_value.clone(), row2_first, row2_second]); } - let base_count = read_table_payload_row_count(store, chunk.pointer)?; - let overlay_count = match chunk.overlay_pointer { - Some(pointer) => read_table_payload_row_count(store, pointer)?, - None => 0, - }; - row_count = row_count.saturating_add( - base_count - .saturating_sub(chunk.tombstoned_row_ids.len()) - .saturating_add(overlay_count), - ); - } - Ok(row_count) -} - -fn read_deferred_row_by_id_from_table_payload( - store: &S, - state: PersistedTableState, - row_id: i64, -) -> Result> { - let pointer = state.pointer; - if pointer.head_page_id == 0 { - return Ok(None); - } - if pointer.is_compressed() { - let entry = read_deferred_compressed_table_lookup_entry(store, state)?; - let Some(locator) = entry.row_locators.get(&row_id).copied() else { - return Ok(None); - }; - return decode_row_by_locator_from_payload(entry.payload.as_slice(), row_id, locator) - .map(Some); - } - let mut cursor = OverflowPayloadCursor::new(store, pointer); - let mut magic = [0_u8; TABLE_PAYLOAD_MAGIC.len()]; - cursor.read_exact(&mut magic)?; - if magic != *TABLE_PAYLOAD_MAGIC { - return Err(DbError::corruption("table payload magic is invalid")); } - let row_count = cursor.read_u32()? as usize; - for _ in 0..row_count { - let candidate_row_id = cursor.read_i64()?; - let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); - if candidate_row_id == row_id && !is_tombstone { - let row_bytes = cursor.read_vec(row_bytes_len)?; - let row = Row::decode(&row_bytes)?; - return Ok(Some(StoredRow { - row_id: candidate_row_id, - values: row.into_values(), - })); + let row2_row_id = row2.row_id; + let mut row2_values = row2.values; + let mut row2_removed = 0usize; + let mut values = Vec::with_capacity(projection_indexes.len()); + for source in projection_indexes.iter().copied() { + match source { + DeferredViewProjectionSource::RowId { table_index } => { + let row_id = match table_index { + 0 => root_row.row_id, + 1 => row1.row_id, + 2 => row2_row_id, + _ => { + return Err(DbError::internal(format!( + "{context} row is shorter than planned schema", + ))); + } + }; + values.push(Value::Int64(row_id)); + } + DeferredViewProjectionSource::Projected { + table_index, + projected_index, + } => match table_index { + 0 => { + let Some(value) = root_row.values.get(projected_index) else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + values.push(value.clone()); + } + 1 => { + let Some(value) = row1.values.get(projected_index) else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + values.push(value.clone()); + } + 2 if row2_can_move => { + let Some(adjusted_index) = projected_index.checked_sub(row2_removed) else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + if adjusted_index >= row2_values.len() { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + } + values.push(row2_values.remove(adjusted_index)); + row2_removed = row2_removed.saturating_add(1); + } + 2 => { + let Some(value) = row2_values.get(projected_index) else { + return Err(DbError::internal(format!( + "{context} projection row is shorter than planned schema", + ))); + }; + values.push(value.clone()); + } + _ => { + return Err(DbError::internal(format!( + "{context} row is shorter than planned schema", + ))); + } + }, } - cursor.skip(row_bytes_len)?; } - Ok(None) + Ok(values) } -fn matches_table_binding(table: TableBindingRef<'_>, qualifier: Option<&str>) -> bool { - qualifier.is_some_and(|qualifier| identifiers_equal(qualifier, table.binding_name())) +fn table_output_columns(table: &TableSchema, alias: &Option) -> Vec { + let table_name = alias.clone().unwrap_or_else(|| table.name.clone()); + table + .columns + .iter() + .map(|column| ColumnBinding::visible(Some(table_name.clone()), column.name.clone())) + .collect() } -fn matches_filter_binding( - table_name: &str, - alias: &Option, - qualifier: Option<&str>, +fn flatten_inner_join_chain<'a>( + item: &'a FromItem, + tables: &mut Vec>, + constraints: &mut Vec<&'a Expr>, ) -> bool { - match qualifier { - Some(qualifier) => identifiers_equal(qualifier, alias.as_deref().unwrap_or(table_name)), - None => true, - } -} - -fn dataset_column_index(dataset: &Dataset, qualifier: Option<&str>, column: &str) -> Option { - let matches = dataset - .columns - .iter() - .enumerate() - .filter(|(_, binding)| { - if !identifiers_equal(&binding.name, column) { - return false; - } - if let Some(qualifier) = qualifier { - binding - .table - .as_deref() - .is_some_and(|table| identifiers_equal(table, qualifier)) - } else { - !binding.hidden - } - }) - .map(|(index, _)| index) - .collect::>(); - match matches.as_slice() { - [index] => Some(*index), - _ => None, + match item { + FromItem::Table { name, alias } => { + tables.push(TableBindingRef { name, alias }); + true + } + FromItem::Join { + left, + right, + kind: JoinKind::Inner, + constraint: JoinConstraint::On(on), + } => { + flatten_inner_join_chain(left, tables, constraints) + && flatten_inner_join_chain(right, tables, constraints) + && { + constraints.push(on); + true + } + } + _ => false, } } -fn projected_dataset_order_column_index(dataset: &Dataset, expr: &Expr) -> Option { - let Expr::Column { table, column } = expr else { - return None; - }; - if let Some(index) = dataset_column_index(dataset, table.as_deref(), column) { - return Some(index); - } - if table.is_none() { - return None; - } - let matches = dataset - .columns - .iter() - .enumerate() - .filter(|(_, binding)| !binding.hidden && identifiers_equal(&binding.name, column)) - .map(|(index, _)| index) - .collect::>(); - match matches.as_slice() { - [index] => Some(*index), +fn spatial_join_argument_orientation<'a>( + left_binding: TableBindingRef<'a>, + right_binding: TableBindingRef<'a>, + indexed_ref: QualifiedColumnRef<'a>, + probe_ref: QualifiedColumnRef<'a>, +) -> Option<(TableBindingRef<'a>, TableBindingRef<'a>, bool)> { + let indexed_on_left = matches_table_binding(left_binding, indexed_ref.table); + let indexed_on_right = matches_table_binding(right_binding, indexed_ref.table); + let probe_on_left = matches_table_binding(left_binding, probe_ref.table); + let probe_on_right = matches_table_binding(right_binding, probe_ref.table); + match ( + indexed_on_left, + indexed_on_right, + probe_on_left, + probe_on_right, + ) { + (true, false, false, true) => Some((left_binding, right_binding, true)), + (false, true, true, false) => Some((right_binding, left_binding, false)), _ => None, } } -#[derive(Debug)] -enum MembershipValue { - Scalar(Value), - Row(Vec), -} - -fn membership_value_has_nulls(value: &MembershipValue) -> bool { - match value { - MembershipValue::Scalar(value) => matches!(value, Value::Null), - MembershipValue::Row(values) => values.iter().any(|value| matches!(value, Value::Null)), +pub(crate) fn orient_join_equalities<'a>( + equalities: &[(QualifiedColumnRef<'a>, QualifiedColumnRef<'a>)], + filtered_table: TableBindingRef<'a>, + probe_table: TableBindingRef<'a>, +) -> Option<(Vec<&'a str>, Vec<&'a str>)> { + let mut filtered_columns = Vec::with_capacity(equalities.len()); + let mut probe_columns = Vec::with_capacity(equalities.len()); + for (left_ref, right_ref) in equalities { + if matches_table_binding(filtered_table, left_ref.table) + && matches_table_binding(probe_table, right_ref.table) + { + filtered_columns.push(left_ref.column); + probe_columns.push(right_ref.column); + } else if matches_table_binding(filtered_table, right_ref.table) + && matches_table_binding(probe_table, left_ref.table) + { + filtered_columns.push(right_ref.column); + probe_columns.push(left_ref.column); + } else { + return None; + } } + Some((filtered_columns, probe_columns)) } -fn compare_membership_values( - left: &MembershipValue, - right: &MembershipValue, -) -> Result> { - match (left, right) { - (MembershipValue::Scalar(left), MembershipValue::Scalar(right)) => { - if matches!(left, Value::Null) || matches!(right, Value::Null) { - Ok(None) - } else { - Ok(Some( - compare_values(left, right)? == std::cmp::Ordering::Equal, - )) +fn flatten_left_deep_inner_join_tables<'a>( + item: &'a FromItem, + tables: &mut Vec>, + constraints: &mut Vec<&'a JoinConstraint>, +) -> bool { + match item { + FromItem::Table { name, alias } => { + if !tables.is_empty() { + return false; } + tables.push(IndexedJoinLimitTablePlan { + name: name.as_str(), + alias, + }); + true } - (MembershipValue::Row(left), MembershipValue::Row(right)) => { - if left.len() != right.len() { - return Err(DbError::sql(format!( - "row-value comparison expected {} columns but got {}", - left.len(), - right.len() - ))); - } - let mut saw_null = false; - for (left_value, right_value) in left.iter().zip(right) { - if matches!(left_value, Value::Null) || matches!(right_value, Value::Null) { - saw_null = true; - continue; - } - if compare_values(left_value, right_value)? != std::cmp::Ordering::Equal { - return Ok(Some(false)); - } - } - if saw_null { - Ok(None) - } else { - Ok(Some(true)) + FromItem::Join { + left, + right, + kind: JoinKind::Inner, + constraint, + } => { + if !flatten_left_deep_inner_join_tables(left, tables, constraints) { + return false; } + let FromItem::Table { name, alias } = &**right else { + return false; + }; + tables.push(IndexedJoinLimitTablePlan { + name: name.as_str(), + alias, + }); + constraints.push(constraint); + true } - (MembershipValue::Scalar(_), MembershipValue::Row(right)) - | (MembershipValue::Row(right), MembershipValue::Scalar(_)) => Err(DbError::sql(format!( - "row-value comparison expected {} columns but got 1", - right.len() - ))), + _ => false, } } -fn schema_column_index(schema: &TableSchema, column: &str) -> Option { - schema +fn indexed_join_group_column_indexes( + group_by: &[Expr], + binding: TableBindingRef<'_>, + schema: &TableSchema, +) -> Option> { + group_by + .iter() + .map(|expr| { + let Expr::Column { table, column } = expr else { + return None; + }; + if !matches_table_binding(binding, table.as_deref()) { + return None; + } + schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, column)) + }) + .collect() +} + +fn indexed_join_grouped_count_is_safe( + expr: &Expr, + child_binding: TableBindingRef<'_>, + child_schema: &TableSchema, +) -> bool { + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return false; + }; + if !name.eq_ignore_ascii_case("count") || *distinct || !order_by.is_empty() || *within_group { + return false; + } + if *star { + return args.is_empty(); + } + if args.len() != 1 { + return false; + } + let Expr::Column { table, column } = &args[0] else { + return false; + }; + if !matches_table_binding(child_binding, table.as_deref()) { + return false; + } + child_schema .columns .iter() - .position(|candidate| identifiers_equal(&candidate.name, column)) + .find(|candidate| identifiers_equal(&candidate.name, column)) + .is_some_and(|column| column.primary_key || !column.nullable) } -fn showdown_window_projection_column_names(select: &Select) -> Result> { - let mut column_names = Vec::with_capacity(select.projection.len()); - for (index, item) in select.projection.iter().enumerate() { - let SelectItem::Expr { expr, alias } = item else { - return Err(DbError::internal( - "showdown window fast path expected expression projection", - )); +fn indexed_join_limit_projection_column( + expr: &Expr, + tables: &[IndexedJoinLimitTablePlan<'_>], + runtime: &EngineRuntime, +) -> Option<(usize, usize)> { + let Expr::Column { + table: qualifier, + column, + } = expr + else { + return None; + }; + let mut matched = None; + let unqualified_unique = qualifier.is_none() + && tables + .iter() + .filter(|candidate| { + runtime + .table_schema(candidate.name) + .is_some_and(|schema| schema_column_index(schema, column).is_some()) + }) + .count() + == 1; + for (table_index, table_plan) in tables.iter().enumerate() { + let binding = TableBindingRef { + name: table_plan.name, + alias: table_plan.alias, }; - column_names.push( - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - ); + let qualifier_matches = qualifier + .as_deref() + .is_some_and(|qualifier| matches_table_binding(binding, Some(qualifier))); + let schema = runtime.table_schema(table_plan.name)?; + let Some(column_index) = schema_column_index(schema, column) else { + continue; + }; + if (qualifier_matches || unqualified_unique) + && matched.replace((table_index, column_index)).is_some() + { + return None; + } } - Ok(column_names) + matched } -fn append_showdown_review_ranking_group( - group: &mut Vec, - rows: &mut Vec, -) { - let mut buckets: [Vec; 11] = std::array::from_fn(|_| Vec::new()); - for item in group.drain(..) { - buckets[item.score as usize].push(item); - } - let mut current_rank = 1_i64; - let mut current_dense_rank = 1_i64; - let mut ordinal = 0_usize; - let mut seen_score = false; - for score in (1..buckets.len()).rev() { - let bucket = &mut buckets[score]; - if bucket.is_empty() { - continue; - } - if seen_score { - current_rank = (ordinal + 1) as i64; - current_dense_rank += 1; - } else { - seen_score = true; - } - for item in bucket.drain(..) { - rows.push(QueryRow::new(vec![ - Value::Int64(item.movie_id), - Value::Int64(item.score), - item.author, - Value::Int64(current_rank), - Value::Int64(current_dense_rank), - ])); - ordinal += 1; +fn pushed_view_projection_for_outer_projection( + outer_projection: &[SelectItem], + view_select: &Select, + view_name: &str, + view_binding: &str, + view_column_names: &[String], +) -> Option> { + let mut pushed = Vec::new(); + for item in outer_projection { + match item { + SelectItem::Wildcard => { + append_all_view_projection_items(&mut pushed, view_select, view_column_names)?; + } + SelectItem::QualifiedWildcard(qualifier) + if identifiers_equal(qualifier, view_binding) + || identifiers_equal(qualifier, view_name) => + { + append_all_view_projection_items(&mut pushed, view_select, view_column_names)?; + } + SelectItem::QualifiedWildcard(_) => return None, + SelectItem::Expr { expr, alias } => { + let Expr::Column { table, column } = expr else { + return None; + }; + if table.as_deref().is_some_and(|qualifier| { + !identifiers_equal(qualifier, view_binding) + && !identifiers_equal(qualifier, view_name) + }) { + return None; + } + let view_expr = view_projection_expr_for_output_column_with_names( + &view_select.projection, + view_column_names, + column, + )?; + pushed.push(SelectItem::Expr { + expr: view_expr, + alias: Some(alias.clone().unwrap_or_else(|| infer_expr_name(expr, 1))), + }); + } } } + Some(pushed) } -fn showdown_window_projection_column_matches( - item: &SelectItem, - table_name: &str, - binding_name: &str, - column_name: &str, -) -> bool { - matches!( - item, - SelectItem::Expr { expr, .. } - if showdown_column_expr_matches(expr, table_name, binding_name, column_name) - ) +fn append_all_view_projection_items( + pushed: &mut Vec, + view_select: &Select, + view_column_names: &[String], +) -> Option<()> { + for (index, item) in view_select.projection.iter().enumerate() { + let SelectItem::Expr { expr, .. } = item else { + return None; + }; + pushed.push(SelectItem::Expr { + expr: expr.clone(), + alias: Some(view_output_column_name( + &view_select.projection, + view_column_names, + index, + )?), + }); + } + Some(()) } -fn showdown_column_expr_matches( - expr: &Expr, - table_name: &str, - binding_name: &str, - column_name: &str, -) -> bool { - let Expr::Column { table, column } = expr else { - return false; - }; - if !identifiers_equal(column, column_name) { - return false; - } - match table.as_deref() { - Some(qualifier) => { - identifiers_equal(qualifier, table_name) || identifiers_equal(qualifier, binding_name) +fn view_projection_expr_for_output_column_with_names( + items: &[SelectItem], + view_column_names: &[String], + column: &str, +) -> Option { + for (index, item) in items.iter().enumerate() { + if identifiers_equal( + &view_output_column_name(items, view_column_names, index)?, + column, + ) { + let SelectItem::Expr { expr, .. } = item else { + return None; + }; + return Some(expr.clone()); } - None => true, } + None } -fn showdown_window_column_order_matches( - order_by: &crate::sql::ast::OrderBy, - table_name: &str, - binding_name: &str, - column_name: &str, - descending: bool, -) -> bool { - order_by.descending == descending - && order_by.collation.is_none() - && showdown_column_expr_matches(&order_by.expr, table_name, binding_name, column_name) +fn view_output_column_name( + items: &[SelectItem], + view_column_names: &[String], + index: usize, +) -> Option { + if let Some(name) = view_column_names.get(index) { + return Some(name.clone()); + } + let SelectItem::Expr { expr, alias } = items.get(index)? else { + return None; + }; + Some( + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + ) } -fn showdown_window_alias_order_matches( - order_by: &crate::sql::ast::OrderBy, - alias: &str, - descending: bool, -) -> bool { - if order_by.descending != descending || order_by.collation.is_some() { - return false; +fn indexed_join_table_eval_columns( + tables: &[IndexedJoinLimitTablePlan<'_>], + runtime: &EngineRuntime, +) -> Option> { + let mut columns = Vec::new(); + for table in tables { + let schema = runtime.table_schema(table.name)?; + let binding_name = table.alias.as_deref().unwrap_or(table.name); + columns.extend(schema.columns.iter().map(|column| { + ColumnBinding::visible_source( + Some(binding_name.to_string()), + Some(schema.name.clone()), + column.name.clone(), + ) + })); } - matches!( - &order_by.expr, - Expr::Column { table: None, column } if identifiers_equal(column, alias) - ) + Some(columns) } -fn showdown_window_partition_order_matches( - partition_by: &[Expr], - order_by: &[crate::sql::ast::OrderBy], +fn single_plain_btree_index_matches_column( + index: &IndexSchema, table_name: &str, - binding_name: &str, - partition_column: &str, - order_column: &str, - order_descending: bool, + column_name: &str, ) -> bool { - partition_by.len() == 1 - && showdown_column_expr_matches( - &partition_by[0], - table_name, - binding_name, - partition_column, - ) - && order_by.len() == 1 - && showdown_window_column_order_matches( - &order_by[0], - table_name, - binding_name, - order_column, - order_descending, - ) + identifiers_equal(&index.table_name, table_name) + && index.fresh + && index.kind == IndexKind::Btree + && index.columns.len() == 1 + && index.columns[0].expression_sql.is_none() + && index.columns[0] + .column_name + .as_deref() + .is_some_and(|index_column| identifiers_equal(index_column, column_name)) } -fn showdown_rank_window_projection_matches( - item: &SelectItem, - table_name: &str, - binding_name: &str, - function_name: &str, - alias_name: &str, +fn filter_contains_partial_index_predicate( + filter: &Expr, + predicate: &Expr, + root_binding: &str, ) -> bool { - let SelectItem::Expr { - expr: - Expr::WindowFunction { - name, - args, - partition_by, - order_by, - frame, - distinct, - star, - }, - alias, - } = item - else { - return false; - }; - alias - .as_deref() - .is_some_and(|alias| identifiers_equal(alias, alias_name)) - && name.eq_ignore_ascii_case(function_name) - && args.is_empty() - && !*distinct - && !*star - && frame.is_none() - && showdown_window_partition_order_matches( - partition_by, - order_by, - table_name, - binding_name, - "movie_id", - "score", - true, - ) + match filter { + Expr::Binary { + left, + op: BinaryOp::And, + right, + } => { + filter_contains_partial_index_predicate(left, predicate, root_binding) + || filter_contains_partial_index_predicate(right, predicate, root_binding) + } + _ => partial_index_predicate_expr_matches(filter, predicate, root_binding), + } } -fn showdown_row_number_projection_matches( - item: &SelectItem, - table_name: &str, - binding_name: &str, - partition_column: &str, - order_column: &str, - alias_name: &str, -) -> bool { - let SelectItem::Expr { - expr: - Expr::RowNumber { - partition_by, - order_by, - frame, +fn partial_index_predicate_expr_matches(left: &Expr, right: &Expr, root_binding: &str) -> bool { + match (left, right) { + ( + Expr::Column { + table: left_table, + column: left_column, }, - alias, - } = item - else { - return false; - }; - alias - .as_deref() - .is_some_and(|alias| identifiers_equal(alias, alias_name)) - && frame.is_none() - && showdown_window_partition_order_matches( - partition_by, - order_by, - table_name, - binding_name, - partition_column, - order_column, - false, - ) + Expr::Column { + table: right_table, + column: right_column, + }, + ) => { + identifiers_equal(left_column, right_column) + && partial_index_predicate_qualifier_matches(left_table.as_deref(), root_binding) + && partial_index_predicate_qualifier_matches(right_table.as_deref(), root_binding) + } + (Expr::Literal(left), Expr::Literal(right)) => left == right, + ( + Expr::Binary { + left: left_left, + op: left_op, + right: left_right, + }, + Expr::Binary { + left: right_left, + op: right_op, + right: right_right, + }, + ) if left_op == right_op => { + let direct = partial_index_predicate_expr_matches(left_left, right_left, root_binding) + && partial_index_predicate_expr_matches(left_right, right_right, root_binding); + direct + || binary_op_is_commutative_for_partial_predicate(*left_op) + && partial_index_predicate_expr_matches(left_left, right_right, root_binding) + && partial_index_predicate_expr_matches(left_right, right_left, root_binding) + } + ( + Expr::Unary { + op: left_op, + expr: left_expr, + }, + Expr::Unary { + op: right_op, + expr: right_expr, + }, + ) if left_op == right_op => { + partial_index_predicate_expr_matches(left_expr, right_expr, root_binding) + } + ( + Expr::Cast { + expr: left_expr, + target_type: left_type, + }, + Expr::Cast { + expr: right_expr, + target_type: right_type, + }, + ) if left_type == right_type => { + partial_index_predicate_expr_matches(left_expr, right_expr, root_binding) + } + ( + Expr::IsNull { + expr: left_expr, + negated: left_not, + }, + Expr::IsNull { + expr: right_expr, + negated: right_not, + }, + ) if left_not == right_not => { + partial_index_predicate_expr_matches(left_expr, right_expr, root_binding) + } + _ => left == right, + } } -fn showdown_lag_projection_matches( - item: &SelectItem, - table_name: &str, - binding_name: &str, - partition_column: &str, - order_column: &str, - alias_name: &str, -) -> bool { - let SelectItem::Expr { - expr: - Expr::WindowFunction { - name, - args, - partition_by, - order_by, - frame, - distinct, - star, - }, - alias, - } = item - else { - return false; - }; - alias - .as_deref() - .is_some_and(|alias| identifiers_equal(alias, alias_name)) - && name.eq_ignore_ascii_case("lag") - && args.len() == 1 - && showdown_column_expr_matches(&args[0], table_name, binding_name, order_column) - && !*distinct - && !*star - && frame.is_none() - && showdown_window_partition_order_matches( - partition_by, - order_by, - table_name, - binding_name, - partition_column, - order_column, - false, - ) +fn partial_index_predicate_qualifier_matches(qualifier: Option<&str>, root_binding: &str) -> bool { + qualifier.is_none_or(|qualifier| identifiers_equal(qualifier, root_binding)) } -fn showdown_avg_window_projection_matches( - item: &SelectItem, - table_name: &str, - binding_name: &str, - order_column: &str, - value_column: &str, - alias_name: &str, -) -> bool { - let SelectItem::Expr { - expr: - Expr::WindowFunction { - name, - args, - partition_by, - order_by, - frame, - distinct, - star, - }, - alias, - } = item - else { - return false; - }; - alias - .as_deref() - .is_some_and(|alias| identifiers_equal(alias, alias_name)) - && name.eq_ignore_ascii_case("avg") - && args.len() == 1 - && showdown_column_expr_matches(&args[0], table_name, binding_name, value_column) - && partition_by.is_empty() - && order_by.len() == 1 - && showdown_window_column_order_matches( - &order_by[0], - table_name, - binding_name, - order_column, - false, - ) - && !*distinct - && !*star - && rows_preceding_current_frame(frame.as_ref()) == Some(2) +fn binary_op_is_commutative_for_partial_predicate(op: BinaryOp) -> bool { + matches!( + op, + BinaryOp::Eq + | BinaryOp::NotEq + | BinaryOp::And + | BinaryOp::Or + | BinaryOp::Add + | BinaryOp::Mul + | BinaryOp::IsDistinctFrom + | BinaryOp::IsNotDistinctFrom + ) +} + +fn visit_runtime_btree_row_ids_in_order( + keys: &RuntimeBtreeKeys, + descending: bool, + mut visitor: F, +) -> Result +where + F: FnMut(i64) -> Result, +{ + match keys { + RuntimeBtreeKeys::UniqueEncoded(entries, deleted) => { + if descending { + for row_id in entries.values().rev() { + if !deleted.contains(row_id) && visitor(*row_id)? { + return Ok(true); + } + } + } else { + for row_id in entries.values() { + if !deleted.contains(row_id) && visitor(*row_id)? { + return Ok(true); + } + } + } + } + RuntimeBtreeKeys::NonUniqueEncoded(entries, deleted) => { + if descending { + for row_ids in entries.values().rev() { + for row_id in row_ids { + if !deleted.contains(row_id) && visitor(*row_id)? { + return Ok(true); + } + } + } + } else { + for row_ids in entries.values() { + for row_id in row_ids { + if !deleted.contains(row_id) && visitor(*row_id)? { + return Ok(true); + } + } + } + } + } + RuntimeBtreeKeys::UniqueUuid(entries, deleted) => { + if descending { + for row_id in entries.values().rev() { + if !deleted.contains(row_id) && visitor(*row_id)? { + return Ok(true); + } + } + } else { + for row_id in entries.values() { + if !deleted.contains(row_id) && visitor(*row_id)? { + return Ok(true); + } + } + } + } + RuntimeBtreeKeys::NonUniqueUuid(entries, deleted) => { + if descending { + for row_ids in entries.values().rev() { + for row_id in row_ids { + if !deleted.contains(row_id) && visitor(*row_id)? { + return Ok(true); + } + } + } + } else { + for row_ids in entries.values() { + for row_id in row_ids { + if !deleted.contains(row_id) && visitor(*row_id)? { + return Ok(true); + } + } + } + } + } + RuntimeBtreeKeys::UniqueInt64(entries, deleted) => { + let mut ordered = entries + .iter() + .filter(|(_, row_id)| !deleted.contains(row_id)) + .collect::>(); + ordered.sort_unstable_by_key(|(key, _)| *key); + if descending { + ordered.reverse(); + } + for (_, row_id) in ordered { + if visitor(row_id)? { + return Ok(true); + } + } + } + RuntimeBtreeKeys::NonUniqueInt64(entries, deleted) => { + let mut ordered = entries + .iter() + .map(|(key, row_ids)| (key, row_ids.to_vec())) + .collect::>(); + ordered.sort_unstable_by_key(|(key, _)| *key); + if descending { + ordered.reverse(); + } + for (_, mut row_ids) in ordered { + row_ids.sort_unstable(); + for row_id in row_ids { + if !deleted.contains(&row_id) && visitor(row_id)? { + return Ok(true); + } + } + } + } + } + Ok(false) } -fn showdown_text_eq_filter_matches( - filter: Option<&Expr>, - table_name: &str, - binding_name: &str, - column_name: &str, - expected_text: &str, -) -> bool { - let Some(Expr::Binary { left, op, right }) = filter else { - return false; +fn indexed_join_limit_rows_for_value( + source: VisibleTableRowSource<'_>, + keys: Option<&RuntimeBtreeKeys>, + value: &Value, +) -> Result>> { + if matches!(value, Value::Null) { + return Ok(Vec::new()); + } + let Some(keys) = keys else { + let Value::Int64(row_id) = value else { + return Ok(Vec::new()); + }; + return Ok(source + .row_by_id(*row_id)? + .map(|row| vec![row.values().to_vec()]) + .unwrap_or_default()); }; - if *op != BinaryOp::Eq { - return false; + let row_ids = keys.row_ids_for_value_set(value)?; + let mut rows = Vec::with_capacity(row_ids.len()); + let mut row_error = None; + row_ids.for_each(|row_id| { + if row_error.is_some() { + return; + } + match source.row_by_id(row_id) { + Ok(Some(row)) => rows.push(row.values().to_vec()), + Ok(None) => {} + Err(error) => row_error = Some(error), + } + }); + if let Some(error) = row_error { + return Err(error); } - (showdown_column_expr_matches(left, table_name, binding_name, column_name) - && matches!(&**right, Expr::Literal(Value::Text(value)) if value == expected_text)) - || (showdown_column_expr_matches(right, table_name, binding_name, column_name) - && matches!(&**left, Expr::Literal(Value::Text(value)) if value == expected_text)) + Ok(rows) } -fn showdown_fast_int64_value(values: &[Value], index: usize, context: &str) -> Result { - match values.get(index) { - Some(Value::Int64(value)) => Ok(*value), - Some(other) => Err(DbError::sql(format!( - "{context} expected INT64, got {other:?}" - ))), - None => Err(DbError::internal(format!( - "{context} column missing from row" - ))), +fn indexed_join_row_ids_for_value( + keys: Option<&RuntimeBtreeKeys>, + value: &Value, +) -> Result> { + if matches!(value, Value::Null) { + return Ok(Vec::new()); } + let Some(keys) = keys else { + return Ok(match value { + Value::Int64(row_id) => vec![*row_id], + _ => Vec::new(), + }); + }; + let row_ids = keys.row_ids_for_value_set(value)?; + let mut values = Vec::with_capacity(row_ids.len()); + row_ids.for_each(|row_id| values.push(row_id)); + Ok(values) } -fn table_has_single_column_foreign_key( - child_schema: &TableSchema, - child_column: &str, - parent_schema: &TableSchema, - parent_column: &str, -) -> bool { - child_schema.foreign_keys.iter().any(|foreign_key| { - if foreign_key.columns.len() != 1 - || !identifiers_equal(&foreign_key.columns[0], child_column) - || !identifiers_equal(&foreign_key.referenced_table, &parent_schema.name) - { - return false; - } - let referenced_columns = if foreign_key.referenced_columns.is_empty() { - parent_schema.primary_key_columns.as_slice() - } else { - foreign_key.referenced_columns.as_slice() +fn sort_join_row_ids_by_column( + source: VisibleTableRowSource<'_>, + row_ids: Vec, + column_index: usize, +) -> Result> { + let mut keyed = Vec::with_capacity(row_ids.len()); + for row_id in row_ids { + let Some(row) = source.row_by_id(row_id)? else { + continue; }; - referenced_columns.len() == 1 && identifiers_equal(&referenced_columns[0], parent_column) - }) -} - -fn value_as_int64(value: &Value) -> Option { - match value { - Value::Int64(value) => Some(*value), - _ => None, + let Some(value) = row.values().get(column_index) else { + return Err(DbError::internal( + "indexed join order row is shorter than schema", + )); + }; + keyed.push((row_id, value.clone())); } -} - -fn value_as_f64(value: &Value) -> Option { - match value { - Value::Int64(value) => Some(*value as f64), - Value::Float64(value) => Some(*value), - _ => None, + let mut sort_error = None; + keyed.sort_by(|(_, left), (_, right)| match compare_values(left, right) { + Ok(ordering) => ordering, + Err(error) => { + if sort_error.is_none() { + sort_error = Some(error); + } + std::cmp::Ordering::Equal + } + }); + if let Some(error) = sort_error { + return Err(error); } + Ok(keyed.into_iter().map(|(row_id, _)| row_id).collect()) } -fn value_as_text(value: &Value) -> Option<&str> { - match value { - Value::Text(value) => Some(value.as_str()), - _ => None, +fn project_indexed_join_row( + current_rows: &[&[Value]], + projections: &[IndexedJoinLimitProjection], +) -> Result { + let mut output = Vec::with_capacity(projections.len()); + for projection in projections { + let Some(row) = current_rows.get(projection.table_index) else { + return Err(DbError::internal( + "indexed join projection table index is out of range", + )); + }; + let Some(value) = row.get(projection.column_index) else { + return Err(DbError::internal( + "indexed join projection row is shorter than schema", + )); + }; + output.push(value.clone()); } + Ok(QueryRow::new(output)) } -fn expr_matches_binding_column(expr: &Expr, binding: TableBindingRef<'_>, column: &str) -> bool { - let Expr::Column { - table, - column: expr_column, - } = expr - else { - return false; - }; - matches_table_binding(binding, table.as_deref()) && identifiers_equal(expr_column, column) -} - -fn join_constraint_matches_columns( - on: &Expr, - left_binding: TableBindingRef<'_>, - left_column: &str, - right_binding: TableBindingRef<'_>, - right_column: &str, +fn push_indexed_join_limit_projection( + current_rows: &[&[Value]], + projections: &[IndexedJoinLimitProjection], + offset_remaining: &mut usize, + limit_remaining: &mut usize, + rows: &mut Vec, ) -> bool { - let Some((left_ref, right_ref)) = simple_join_equality(on) else { + if *offset_remaining > 0 { + *offset_remaining -= 1; return false; - }; - (matches_table_binding(left_binding, left_ref.table) - && identifiers_equal(left_ref.column, left_column) - && matches_table_binding(right_binding, right_ref.table) - && identifiers_equal(right_ref.column, right_column)) - || (matches_table_binding(left_binding, right_ref.table) - && identifiers_equal(right_ref.column, left_column) - && matches_table_binding(right_binding, left_ref.table) - && identifiers_equal(left_ref.column, right_column)) + } + if *limit_remaining == 0 { + return true; + } + let mut output = Vec::with_capacity(projections.len()); + for projection in projections { + let Some(row) = current_rows.get(projection.table_index) else { + return true; + }; + let Some(value) = row.get(projection.column_index) else { + return true; + }; + output.push(value.clone()); + } + rows.push(QueryRow::new(output)); + *limit_remaining = (*limit_remaining).saturating_sub(1); + *limit_remaining == 0 } -fn aggregate_matches_single_binding_column( - expr: &Expr, - aggregate_name: &str, - binding: TableBindingRef<'_>, - column: &str, -) -> bool { - let Expr::Aggregate { - name, - args, - distinct, - star, - order_by, - within_group, - } = expr - else { - return false; - }; - if !name.eq_ignore_ascii_case(aggregate_name) - || *distinct - || *star - || !order_by.is_empty() - || *within_group - || args.len() != 1 - { - return false; - } - expr_matches_binding_column(&args[0], binding, column) +fn indexed_join_limit_result(plan: &IndexedJoinLimitPlan<'_>, rows: Vec) -> QueryResult { + QueryResult::with_rows( + plan.projections + .iter() + .map(|projection| projection.column_name.clone()) + .collect(), + rows, + ) } -fn aggregate_matches_count_star(expr: &Expr) -> bool { - let Expr::Aggregate { - name, - args, - distinct, - star, - order_by, - within_group, - } = expr - else { - return false; - }; - name.eq_ignore_ascii_case("count") - && *star - && args.is_empty() - && !*distinct - && order_by.is_empty() - && !*within_group -} - -fn join_constraints_match_columns( - constraints: &[&Expr], - left_binding: TableBindingRef<'_>, - left_column: &str, - right_binding: TableBindingRef<'_>, - right_column: &str, -) -> bool { - constraints.iter().any(|constraint| { - simple_join_equalities(constraint).is_some_and(|equalities| { - equalities.iter().any(|(left_ref, right_ref)| { - (matches_table_binding(left_binding, left_ref.table) - && identifiers_equal(left_ref.column, left_column) - && matches_table_binding(right_binding, right_ref.table) - && identifiers_equal(right_ref.column, right_column)) - || (matches_table_binding(left_binding, right_ref.table) - && identifiers_equal(right_ref.column, left_column) - && matches_table_binding(right_binding, left_ref.table) - && identifiers_equal(left_ref.column, right_column)) - }) - }) - }) +fn row_id_alias_column_name(table: &TableSchema) -> Option<&str> { + if table.primary_key_columns.len() != 1 { + return None; + } + let primary_key_column = &table.primary_key_columns[0]; + table + .columns + .iter() + .find(|column| identifiers_equal(&column.name, primary_key_column) && column.auto_increment) + .map(|column| column.name.as_str()) } -#[allow(clippy::too_many_arguments)] -fn accumulate_genre_popularity_movie( - movie_source: &VisibleTableRowSource<'_>, - movie_index_keys: Option<&RuntimeBtreeKeys>, - movie_id_is_rowid_alias: bool, - movie_id_value: Option<&Value>, - movie_rating_index: usize, - movie_count: &mut i64, - rating_sum: &mut f64, - rating_count: &mut i64, -) -> Result<()> { - let Some(movie_id_value) = movie_id_value else { - return Ok(()); - }; - if matches!(movie_id_value, Value::Null) { - return Ok(()); - } +/// Returns the schema column index of the table's row-id alias column, when the +/// table has a single-column auto-increment `INTEGER PRIMARY KEY`. The stored +/// `row_id` of every row equals the value of this column. +fn rowid_alias_column_index(table: &TableSchema) -> Option { + let alias = row_id_alias_column_name(table)?; + table + .columns + .iter() + .position(|column| identifiers_equal(&column.name, alias)) +} - if movie_id_is_rowid_alias { - if let Some(row_id) = value_as_int64(movie_id_value) { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - accumulate_genre_popularity_rating( - movie_row.values(), - movie_rating_index, - movie_count, - rating_sum, - rating_count, - ); - return Ok(()); - } - } +fn push_projection_index(indexes: &mut Vec, index: usize) -> usize { + if let Some(position) = indexes.iter().position(|candidate| *candidate == index) { + position + } else { + indexes.push(index); + indexes.len() - 1 } +} - let Some(keys) = movie_index_keys else { - return Ok(()); - }; - match keys.row_ids_for_value_set(movie_id_value)? { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(row_id) => { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - accumulate_genre_popularity_rating( - movie_row.values(), - movie_rating_index, - movie_count, - rating_sum, - rating_count, - ); - } - } - RuntimeRowIdSet::Contiguous { start, len } => { - for row_id in contiguous_row_ids(start, len) { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - accumulate_genre_popularity_rating( - movie_row.values(), - movie_rating_index, - movie_count, - rating_sum, - rating_count, - ); - } - } - } - RuntimeRowIdSet::Many(row_ids) => { - for row_id in row_ids { - if let Some(movie_row) = movie_source.row_by_id(*row_id)? { - accumulate_genre_popularity_rating( - movie_row.values(), - movie_rating_index, - movie_count, - rating_sum, - rating_count, - ); - } - } - } - RuntimeRowIdSet::Owned(row_ids) => { - for row_id in row_ids { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - accumulate_genre_popularity_rating( - movie_row.values(), - movie_rating_index, - movie_count, - rating_sum, - rating_count, - ); - } - } - } +fn project_resolved_simple_join_row( + projections: &[ResolvedSimpleJoinProjection], + left_values: &[Value], + right_values: &[Value], +) -> Result { + let mut projected = Vec::with_capacity(projections.len()); + for projection in projections { + let values = match projection.side { + SimpleJoinProjectionSide::Left => left_values, + SimpleJoinProjectionSide::Right => right_values, + }; + let value = values + .get(projection.index) + .ok_or_else(|| DbError::internal("prepared join projection index out of bounds"))?; + projected.push(value.clone()); } - Ok(()) + Ok(QueryRow::new(projected)) } -fn accumulate_genre_popularity_rating( - movie_values: &[Value], - movie_rating_index: usize, - movie_count: &mut i64, - rating_sum: &mut f64, - rating_count: &mut i64, -) { - *movie_count = movie_count.saturating_add(1); - if let Some(value) = movie_values.get(movie_rating_index) { - if let Some(rating) = indexed_join_aggregate_as_f64(value) { - *rating_sum += rating; - *rating_count = rating_count.saturating_add(1); - } +fn project_resolved_simple_join_row_from_full_values( + projections: &[ResolvedSimpleJoinProjection], + left_projection_indexes: &[usize], + right_projection_indexes: &[usize], + left_values: &[Value], + right_values: &[Value], +) -> Result { + let mut projected = Vec::with_capacity(projections.len()); + for projection in projections { + let (projection_indexes, values) = match projection.side { + SimpleJoinProjectionSide::Left => (left_projection_indexes, left_values), + SimpleJoinProjectionSide::Right => (right_projection_indexes, right_values), + }; + let original_index = projection_indexes + .get(projection.index) + .ok_or_else(|| DbError::internal("prepared join projection index out of bounds"))?; + let value = values + .get(*original_index) + .ok_or_else(|| DbError::internal("prepared join projection index out of bounds"))?; + projected.push(value.clone()); } + Ok(QueryRow::new(projected)) } -#[allow(clippy::too_many_arguments)] -fn push_movie_tag_search_movie_rows( - runtime: &EngineRuntime, - movie_source: &VisibleTableRowSource<'_>, - movie_index_keys: Option<&RuntimeBtreeKeys>, - movie_id_is_rowid_alias: bool, - movie_id_value: Option<&Value>, +fn covering_projection_offsets( + covering: &RuntimeCoveringPayloads, + table_schema: &TableSchema, projection_indexes: &[usize], - bounded_order: Option<(&[SimpleOrderByPlan], usize)>, - rows: &mut Vec, -) -> Result<()> { - let Some(movie_id_value) = movie_id_value else { - return Ok(()); - }; - if matches!(movie_id_value, Value::Null) { - return Ok(()); - } +) -> Option> { + projection_indexes + .iter() + .map(|projection_index| { + table_schema + .columns + .get(*projection_index) + .and_then(|column| covering.column_position(&column.name)) + }) + .collect() +} - if movie_id_is_rowid_alias { - if let Some(row_id) = value_as_int64(movie_id_value) { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - push_movie_tag_search_projected_row( - runtime, - movie_row.values(), - projection_indexes, - bounded_order, - rows, - )?; - return Ok(()); +fn apply_simple_projection_postprocessing_with_order( + runtime: Option<&EngineRuntime>, + mut rows: Vec, + column_names: Vec, + order_by: Option<&[SimpleOrderByPlan]>, + limit: Option, + offset: usize, +) -> Result { + if let Some(order_by) = order_by { + if let Some(limit) = limit { + let bounded_row_count = offset.saturating_add(limit); + if bounded_row_count == 0 { + return Ok(QueryResult::with_rows(column_names, Vec::new())); } - } - } - - let Some(keys) = movie_index_keys else { - return Ok(()); - }; - match keys.row_ids_for_value_set(movie_id_value)? { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(row_id) => { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - push_movie_tag_search_projected_row( + let mut bounded_rows = Vec::with_capacity(rows.len().min(bounded_row_count)); + for row in rows { + push_bounded_projection_ordered_query_row( runtime, - movie_row.values(), - projection_indexes, - bounded_order, - rows, - )?; - } - } - RuntimeRowIdSet::Contiguous { start, len } => { - for row_id in contiguous_row_ids(start, len) { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - push_movie_tag_search_projected_row( - runtime, - movie_row.values(), - projection_indexes, - bounded_order, - rows, - )?; - } - } - } - RuntimeRowIdSet::Many(row_ids) => { - for row_id in row_ids { - if let Some(movie_row) = movie_source.row_by_id(*row_id)? { - push_movie_tag_search_projected_row( - runtime, - movie_row.values(), - projection_indexes, - bounded_order, - rows, - )?; - } + &mut bounded_rows, + row, + order_by, + bounded_row_count, + )?; } + sort_query_rows_by_projection_order(runtime, &mut bounded_rows, order_by)?; + let rows = bounded_rows.into_iter().skip(offset).take(limit).collect(); + return Ok(QueryResult::with_rows(column_names, rows)); } - RuntimeRowIdSet::Owned(row_ids) => { - for row_id in row_ids { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - push_movie_tag_search_projected_row( - runtime, - movie_row.values(), - projection_indexes, - bounded_order, - rows, - )?; - } - } + sort_query_rows_by_projection_order(runtime, &mut rows, order_by)?; + } + let rows = rows + .into_iter() + .skip(offset) + .take(limit.unwrap_or(usize::MAX)) + .collect(); + Ok(QueryResult::with_rows(column_names, rows)) +} + +fn dedup_query_rows(rows: Vec) -> Result> { + let mut seen = BTreeSet::new(); + let mut distinct_rows = Vec::with_capacity(rows.len()); + for row in rows { + if seen.insert(row_identity(row.values())?) { + distinct_rows.push(row); } } - Ok(()) + Ok(distinct_rows) } -fn push_movie_tag_search_projected_row( - runtime: &EngineRuntime, - movie_values: &[Value], - projection_indexes: &[usize], - bounded_order: Option<(&[SimpleOrderByPlan], usize)>, - rows: &mut Vec, -) -> Result<()> { - let row = project_simple_projection_values(movie_values, projection_indexes); - if let Some((order_by, limit)) = bounded_order { - push_bounded_projection_ordered_query_row(Some(runtime), rows, row, order_by, limit) - } else { - rows.push(row); - Ok(()) +fn compare_query_row_order_values( + runtime: Option<&EngineRuntime>, + left_order: &[Value], + right_order: &[Value], + order_by: &[crate::sql::ast::OrderBy], +) -> Result { + for (index, order) in order_by.iter().enumerate() { + let ordering = compare_values_with_runtime_collation( + runtime, + &left_order[index], + &right_order[index], + order.collation.clone(), + )?; + if ordering == std::cmp::Ordering::Equal { + continue; + } + return Ok(if order.descending { + ordering.reverse() + } else { + ordering + }); } + Ok(std::cmp::Ordering::Equal) } -#[allow(clippy::too_many_arguments)] -fn insert_movie_watchlist_group_rows( - movie_source: &VisibleTableRowSource<'_>, - movie_index_keys: Option<&RuntimeBtreeKeys>, - movie_id_is_rowid_alias: bool, - movie_id_value: &Value, - priority: &Value, - review_source: &VisibleTableRowSource<'_>, - review_movie_keys: &RuntimeBtreeKeys, - movie_id_index: usize, - movie_title_index: usize, - review_score_index: usize, - groups: &mut BTreeMap, QueryRow>, +fn sort_query_rows_by_order_values( + runtime: Option<&EngineRuntime>, + rows: &mut [(QueryRow, Vec)], + order_by: &[crate::sql::ast::OrderBy], ) -> Result<()> { - if movie_id_is_rowid_alias { - if let Some(row_id) = value_as_int64(movie_id_value) { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - insert_movie_watchlist_group_row( - movie_row.values(), - priority, - review_source, - review_movie_keys, - movie_id_index, - movie_title_index, - review_score_index, - groups, - )?; - return Ok(()); + let mut sort_error = None; + rows.sort_by(|(_, left_order), (_, right_order)| { + match compare_query_row_order_values(runtime, left_order, right_order, order_by) { + Ok(ordering) => ordering, + Err(error) => { + if sort_error.is_none() { + sort_error = Some(error); + } + std::cmp::Ordering::Equal } } + }); + if let Some(error) = sort_error { + return Err(error); } + Ok(()) +} - let Some(keys) = movie_index_keys else { +fn push_bounded_ordered_query_row( + runtime: Option<&EngineRuntime>, + rows: &mut Vec<(QueryRow, Vec)>, + row: (QueryRow, Vec), + order_by: &[crate::sql::ast::OrderBy], + bounded_row_count: usize, +) -> Result<()> { + if bounded_row_count == 0 { return Ok(()); - }; - match keys.row_ids_for_value_set(movie_id_value)? { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(row_id) => { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - insert_movie_watchlist_group_row( - movie_row.values(), - priority, - review_source, - review_movie_keys, - movie_id_index, - movie_title_index, - review_score_index, - groups, - )?; - } - } - RuntimeRowIdSet::Contiguous { start, len } => { - for row_id in contiguous_row_ids(start, len) { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - insert_movie_watchlist_group_row( - movie_row.values(), - priority, - review_source, - review_movie_keys, - movie_id_index, - movie_title_index, - review_score_index, - groups, - )?; - } - } - } - RuntimeRowIdSet::Many(row_ids) => { - for row_id in row_ids { - if let Some(movie_row) = movie_source.row_by_id(*row_id)? { - insert_movie_watchlist_group_row( - movie_row.values(), - priority, - review_source, - review_movie_keys, - movie_id_index, - movie_title_index, - review_score_index, - groups, - )?; - } - } - } - RuntimeRowIdSet::Owned(row_ids) => { - for row_id in row_ids { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - insert_movie_watchlist_group_row( - movie_row.values(), - priority, - review_source, - review_movie_keys, - movie_id_index, - movie_title_index, - review_score_index, - groups, - )?; - } - } + } + if rows.len() < bounded_row_count { + rows.push(row); + return Ok(()); + } + let mut worst_index = 0; + for index in 1..rows.len() { + if compare_query_row_order_values(runtime, &rows[index].1, &rows[worst_index].1, order_by)? + == std::cmp::Ordering::Greater + { + worst_index = index; } } + if compare_query_row_order_values(runtime, &row.1, &rows[worst_index].1, order_by)? + == std::cmp::Ordering::Less + { + rows[worst_index] = row; + } Ok(()) } -#[allow(clippy::too_many_arguments)] -fn insert_movie_watchlist_group_row( - movie_values: &[Value], - priority: &Value, - review_source: &VisibleTableRowSource<'_>, - review_movie_keys: &RuntimeBtreeKeys, - movie_id_index: usize, - movie_title_index: usize, - review_score_index: usize, - groups: &mut BTreeMap, QueryRow>, +pub(crate) fn sort_query_rows_by_projection_order( + runtime: Option<&EngineRuntime>, + rows: &mut [QueryRow], + order_by: &[SimpleOrderByPlan], ) -> Result<()> { - let Some(movie_id) = movie_values.get(movie_id_index) else { - return Err(DbError::internal( - "movie watchlist id column missing from movie row", - )); - }; - let group_key = row_identity(std::slice::from_ref(movie_id))?; - if groups.contains_key(&group_key) { - return Ok(()); + let mut sort_error = None; + rows.sort_by(|left, right| { + for order in order_by { + let ordering = compare_values_with_runtime_collation( + runtime, + &left.values()[order.projection_index], + &right.values()[order.projection_index], + order.collation.clone(), + ); + match ordering { + Ok(std::cmp::Ordering::Equal) => continue, + Ok(ordering) => { + return if order.descending { + ordering.reverse() + } else { + ordering + }; + } + Err(error) => { + if sort_error.is_none() { + sort_error = Some(error); + } + return std::cmp::Ordering::Equal; + } + } + } + std::cmp::Ordering::Equal + }); + if let Some(error) = sort_error { + return Err(error); } - let Some(title) = movie_values.get(movie_title_index) else { - return Err(DbError::internal( - "movie watchlist title column missing from movie row", - )); - }; - let avg = movie_watchlist_review_avg( - review_source, - review_movie_keys, - movie_id, - review_score_index, - )?; - groups.insert( - group_key, - QueryRow::new(vec![movie_id.clone(), title.clone(), priority.clone(), avg]), - ); Ok(()) } -fn movie_watchlist_review_avg( - review_source: &VisibleTableRowSource<'_>, - review_movie_keys: &RuntimeBtreeKeys, - movie_id: &Value, - review_score_index: usize, -) -> Result { - let (count, sum) = movie_review_score_stats( - review_source, - review_movie_keys, - movie_id, - review_score_index, - )?; - if count == 0 { - Ok(Value::Null) - } else { - Ok(Value::Float64(sum / count as f64)) +fn compare_query_rows_by_projection_order( + runtime: Option<&EngineRuntime>, + left: &QueryRow, + right: &QueryRow, + order_by: &[SimpleOrderByPlan], +) -> Result { + for order in order_by { + let ordering = compare_values_with_runtime_collation( + runtime, + &left.values()[order.projection_index], + &right.values()[order.projection_index], + order.collation.clone(), + )?; + if ordering == std::cmp::Ordering::Equal { + continue; + } + return Ok(if order.descending { + ordering.reverse() + } else { + ordering + }); } + Ok(std::cmp::Ordering::Equal) } -fn push_bounded_movie_busiest_people_count( - counts: &mut Vec, - candidate: MovieBusiestPeopleCount, - bounded_count: usize, -) { - if bounded_count == 0 { - return; +fn push_bounded_projection_ordered_query_row( + runtime: Option<&EngineRuntime>, + rows: &mut Vec, + row: QueryRow, + order_by: &[SimpleOrderByPlan], + bounded_row_count: usize, +) -> Result<()> { + if bounded_row_count == 0 { + return Ok(()); } - if counts.len() < bounded_count { - counts.push(candidate); - return; + if rows.len() < bounded_row_count { + rows.push(row); + return Ok(()); } let mut worst_index = 0; - for index in 1..counts.len() { - if compare_movie_busiest_people_counts(&counts[index], &counts[worst_index]) - == std::cmp::Ordering::Greater + for index in 1..rows.len() { + if compare_query_rows_by_projection_order( + runtime, + &rows[index], + &rows[worst_index], + order_by, + )? == std::cmp::Ordering::Greater { worst_index = index; } } - if compare_movie_busiest_people_counts(&candidate, &counts[worst_index]) + if compare_query_rows_by_projection_order(runtime, &row, &rows[worst_index], order_by)? == std::cmp::Ordering::Less { - counts[worst_index] = candidate; + rows[worst_index] = row; } + Ok(()) } -fn sort_movie_busiest_people_counts(counts: &mut [MovieBusiestPeopleCount]) { - counts.sort_by(compare_movie_busiest_people_counts); +fn unique_grouped_having_column_index(column_names: &[String], column: &str) -> Option { + let mut matched = None; + for (index, candidate) in column_names.iter().enumerate() { + if !candidate.eq_ignore_ascii_case(column) { + continue; + } + if matched.replace(index).is_some() { + return None; + } + } + matched } -fn compare_movie_busiest_people_counts( - left: &MovieBusiestPeopleCount, - right: &MovieBusiestPeopleCount, -) -> std::cmp::Ordering { - right - .role_count - .cmp(&left.role_count) - .then_with(|| compare_runtime_btree_keys(&left.person_key, &right.person_key)) +fn unique_grouped_having_group_index(select: &Select, column: &str) -> Option { + let mut matched = None; + for (index, expr) in select.group_by.iter().enumerate() { + let Expr::Column { + column: group_column, + .. + } = expr + else { + continue; + }; + if !identifiers_equal(group_column, column) { + continue; + } + if matched.replace(index).is_some() { + return None; + } + } + matched } -fn compare_runtime_btree_keys( - left: &RuntimeBtreeKey, - right: &RuntimeBtreeKey, -) -> std::cmp::Ordering { - match (left, right) { - (RuntimeBtreeKey::Encoded(left), RuntimeBtreeKey::Encoded(right)) => left.cmp(right), - (RuntimeBtreeKey::Int64(left), RuntimeBtreeKey::Int64(right)) => left.cmp(right), - (RuntimeBtreeKey::Uuid(left), RuntimeBtreeKey::Uuid(right)) => left.cmp(right), - (RuntimeBtreeKey::Encoded(_), RuntimeBtreeKey::Int64(_)) => std::cmp::Ordering::Less, - (RuntimeBtreeKey::Encoded(_), RuntimeBtreeKey::Uuid(_)) => std::cmp::Ordering::Less, - (RuntimeBtreeKey::Int64(_), RuntimeBtreeKey::Encoded(_)) => std::cmp::Ordering::Greater, - (RuntimeBtreeKey::Int64(_), RuntimeBtreeKey::Uuid(_)) => std::cmp::Ordering::Less, - (RuntimeBtreeKey::Uuid(_), RuntimeBtreeKey::Encoded(_)) => std::cmp::Ordering::Greater, - (RuntimeBtreeKey::Uuid(_), RuntimeBtreeKey::Int64(_)) => std::cmp::Ordering::Greater, - } +fn first_persistent_pk_row_id( + store: &S, + table_schema: &TableSchema, +) -> Result> { + let Some(pk_index_root) = table_schema.pk_index_root else { + return Ok(None); + }; + let Some(position) = btree_first_position(store, Some(pk_index_root))? else { + return Ok(None); + }; + let (key, _) = btree_materialize_current(store, &position)?; + Ok(Some(decode_row_id_locator_key(key))) } -fn push_movie_busiest_people_row( - people_source: &VisibleTableRowSource<'_>, - people_index_keys: Option<&RuntimeBtreeKeys>, - people_id_is_rowid_alias: bool, - candidate: &MovieBusiestPeopleCount, +#[allow(clippy::too_many_arguments)] +fn try_persistent_pk_ordered_projection_result( + store: &S, + state: PersistedTableState, + table_schema: &TableSchema, projection_indexes: &[usize], - rows: &mut Vec, -) -> Result<()> { - if people_id_is_rowid_alias { - if let RuntimeBtreeKey::Int64(row_id) = &candidate.person_key { - if let Some(people_row) = people_source.row_by_id(*row_id)? { - push_movie_busiest_people_projected_row( - people_row.values(), - candidate.role_count, - projection_indexes, - rows, - ); - } - return Ok(()); - } + column_names: Vec, + limit: Option, + offset: usize, + descending: bool, +) -> Result> { + let Some(pk_index_root) = table_schema.pk_index_root else { + return Ok(None); + }; + let take = limit.unwrap_or(usize::MAX); + if take == 0 { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); } - let Some(keys) = people_index_keys else { - return Ok(()); + let mut cursor = if descending { + BtreeCursor::from_end(store, Some(pk_index_root))? + } else { + BtreeCursor::from_start(store, Some(pk_index_root))? }; - match keys.row_id_set_for_key(&candidate.person_key) { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(row_id) => { - if let Some(people_row) = people_source.row_by_id(row_id)? { - push_movie_busiest_people_projected_row( - people_row.values(), - candidate.role_count, - projection_indexes, - rows, - ); - } + let mut skipped = 0usize; + let mut rows = Vec::with_capacity(take.min(64)); + while let Some((_, payload)) = if descending { + cursor.prev()? + } else { + cursor.next()? + } { + if skipped < offset { + skipped += 1; + continue; } - RuntimeRowIdSet::Contiguous { start, len } => { - for row_id in contiguous_row_ids(start, len) { - if let Some(people_row) = people_source.row_by_id(row_id)? { - push_movie_busiest_people_projected_row( - people_row.values(), - candidate.role_count, - projection_indexes, - rows, - ); - } + let locator = decode_row_locator(&payload)?; + if let Some(values) = + read_deferred_projected_values_by_locator(store, state, locator, projection_indexes)? + { + rows.push(QueryRow::new(values)); + if rows.len() == take { + break; } } - RuntimeRowIdSet::Many(row_ids) => { - for row_id in row_ids { - if let Some(people_row) = people_source.row_by_id(*row_id)? { - push_movie_busiest_people_projected_row( - people_row.values(), - candidate.role_count, - projection_indexes, - rows, - ); - } - } + } + + Ok(Some(QueryResult::with_rows(column_names, rows))) +} + +fn append_paged_row_locator_entries( + entries: &mut BTreeMap>, + payload: &[u8], + chunk_index: u32, + is_overlay: bool, + skip: &BTreeSet, +) -> Result<()> { + if payload.is_empty() { + return Ok(()); + } + if !payload.starts_with(TABLE_PAYLOAD_MAGIC) { + return Err(DbError::corruption("table payload magic is invalid")); + } + let mut cursor = Cursor::new(payload); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes_offset = cursor.offset; + cursor.read_slice(row_bytes_len)?; + if is_tombstone || skip.contains(&row_id) { + continue; } - RuntimeRowIdSet::Owned(row_ids) => { - for row_id in row_ids { - if let Some(people_row) = people_source.row_by_id(row_id)? { - push_movie_busiest_people_projected_row( - people_row.values(), - candidate.role_count, - projection_indexes, - rows, - ); - } - } + let locator = RowLocatorV2 { + chunk_index, + byte_offset: u32::try_from(row_bytes_offset) + .map_err(|_| DbError::constraint("row locator offset exceeds u32"))?, + byte_len: u32::try_from(row_bytes_len) + .map_err(|_| DbError::constraint("row locator length exceeds u32"))?, + is_overlay, + }; + entries.insert( + encode_row_id_locator_key(row_id), + encode_paged_row_locator(locator), + ); + } + Ok(()) +} + +fn append_cached_paged_row_locators( + locators: &mut Int64Map, + payload: &[u8], + pointer: OverflowPointer, + checksum: u32, + skip: &BTreeSet, +) -> Result<()> { + if payload.is_empty() { + return Ok(()); + } + let mut cursor = Cursor::new(payload); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes_offset = cursor.offset; + cursor.read_slice(row_bytes_len)?; + if is_tombstone || skip.contains(&row_id) { + continue; } + locators.insert( + row_id, + CachedPagedRowLocator { + pointer, + checksum, + locator: RowLocatorV1 { + byte_offset: u32::try_from(row_bytes_offset) + .map_err(|_| DbError::constraint("row locator offset exceeds u32"))?, + byte_len: u32::try_from(row_bytes_len) + .map_err(|_| DbError::constraint("row locator length exceeds u32"))?, + }, + }, + ); } Ok(()) } -fn push_movie_busiest_people_projected_row( - people_values: &[Value], - role_count: i64, - projection_indexes: &[usize], - rows: &mut Vec, +fn maybe_cache_verified_paged_chunk_payload( + payloads: &mut HashMap>>, + cached_payload_bytes: &mut usize, + pointer: OverflowPointer, + checksum: u32, + payload: &Arc>, ) { - let projected = project_simple_projection_values(people_values, projection_indexes); - let mut values = projected.values().to_vec(); - values.push(Value::Int64(role_count)); - rows.push(QueryRow::new(values)); + let payload_len = payload.len(); + if payload_len == 0 || payload_len > DEFERRED_PAGED_ROW_PAYLOAD_CACHE_LIMIT_BYTES { + return; + } + if cached_payload_bytes.saturating_add(payload_len) + > DEFERRED_PAGED_ROW_PAYLOAD_CACHE_LIMIT_BYTES + { + return; + } + let key = CachedPagedChunkPayloadKey::new(pointer, checksum); + if payloads.contains_key(&key) { + return; + } + *cached_payload_bytes = cached_payload_bytes.saturating_add(payload_len); + payloads.insert(key, Arc::clone(payload)); } -fn movie_review_score_stats( - review_source: &VisibleTableRowSource<'_>, - review_movie_keys: &RuntimeBtreeKeys, - movie_id: &Value, - review_score_index: usize, -) -> Result<(i64, f64)> { - let mut sum = 0.0_f64; - let mut count = 0_i64; - let mut visit_review = |review_row: TableRowRef<'_>| -> Result<()> { - if let Some(score) = review_row - .values() - .get(review_score_index) - .and_then(indexed_join_aggregate_as_f64) - { - sum += score; - count = count.saturating_add(1); +fn build_deferred_paged_row_locator_cache( + state: PersistedTableState, + chunks: &[TablePageManifestChunk], +) -> Result { + let mut locators = if let Some(directory) = try_build_dense_paged_row_directory(chunks)? { + let mut sources = Vec::new(); + try_reserve_paged_directory(&mut sources, chunks.len(), "deferred paged chunk sources")?; + sources.extend(chunks.iter().map(|chunk| CachedPagedChunkSource { + pointer: chunk.pointer, + checksum: chunk.checksum, + })); + DeferredPagedRowLocators::Dense { + directory, + chunks: sources, } - Ok(()) + } else { + let expected_locators = chunks.iter().try_fold(0usize, |total, chunk| { + total + .checked_add(chunk.row_count) + .ok_or_else(|| DbError::constraint("paged table row count overflow")) + })?; + let mut sparse = Int64Map::with_hasher(Int64HashBuilder::default()); + sparse.try_reserve(expected_locators).map_err(|error| { + DbError::internal(format!( + "failed to reserve {expected_locators} deferred paged row locators: {error}" + )) + })?; + DeferredPagedRowLocators::Sparse(sparse) }; - - match review_movie_keys.row_ids_for_value_set(movie_id)? { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(row_id) => { - if let Some(review_row) = review_source.row_by_id(row_id)? { - visit_review(review_row)?; - } - } - RuntimeRowIdSet::Contiguous { start, len } => { - for row_id in contiguous_row_ids(start, len) { - if let Some(review_row) = review_source.row_by_id(row_id)? { - visit_review(review_row)?; - } - } - } - RuntimeRowIdSet::Many(row_ids) => { - for row_id in row_ids { - if let Some(review_row) = review_source.row_by_id(*row_id)? { - visit_review(review_row)?; - } - } + let mut verified_payloads = HashMap::new(); + let mut cached_payload_bytes = 0usize; + for chunk in chunks { + maybe_cache_verified_paged_chunk_payload( + &mut verified_payloads, + &mut cached_payload_bytes, + chunk.pointer, + chunk.checksum, + &chunk.payload, + ); + if let DeferredPagedRowLocators::Sparse(sparse) = &mut locators { + append_cached_paged_row_locators( + sparse, + chunk.payload.as_slice(), + chunk.pointer, + chunk.checksum, + &chunk.tombstoned_row_ids, + )?; } - RuntimeRowIdSet::Owned(row_ids) => { - for row_id in row_ids { - if let Some(review_row) = review_source.row_by_id(row_id)? { - visit_review(review_row)?; - } + if let (Some(overlay_pointer), Some(overlay_checksum), Some(overlay_payload)) = ( + chunk.overlay_pointer, + chunk.overlay_checksum, + chunk.overlay_payload.as_ref(), + ) { + maybe_cache_verified_paged_chunk_payload( + &mut verified_payloads, + &mut cached_payload_bytes, + overlay_pointer, + overlay_checksum, + overlay_payload, + ); + if let DeferredPagedRowLocators::Sparse(sparse) = &mut locators { + append_cached_paged_row_locators( + sparse, + overlay_payload.as_slice(), + overlay_pointer, + overlay_checksum, + &BTreeSet::new(), + )?; } } } - Ok((count, sum)) + Ok(DeferredPagedRowLocatorCache { + manifest_pointer: state.pointer, + manifest_checksum: state.checksum, + locators, + verified_payloads, + }) } -fn accumulate_directors_cte_movie( - movie_source: &VisibleTableRowSource<'_>, - movie_index_keys: Option<&RuntimeBtreeKeys>, - movie_id_is_rowid_alias: bool, - movie_id_value: &Value, - movie_title_index: usize, - movie_rating_index: usize, - accumulator: &mut DirectorsCteAccumulator, -) -> Result<()> { - if movie_id_is_rowid_alias { - if let Some(row_id) = value_as_int64(movie_id_value) { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - accumulator.add_movie(movie_row.values(), movie_title_index, movie_rating_index); - return Ok(()); - } - } +fn build_row_locator_entries(payload: &[u8]) -> Result>> { + if payload.is_empty() { + return Ok(BTreeMap::new()); } - - let Some(keys) = movie_index_keys else { - return Ok(()); - }; - match keys.row_ids_for_value_set(movie_id_value)? { - RuntimeRowIdSet::Empty => {} - RuntimeRowIdSet::Single(row_id) => { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - accumulator.add_movie(movie_row.values(), movie_title_index, movie_rating_index); - } - } - RuntimeRowIdSet::Contiguous { start, len } => { - for row_id in contiguous_row_ids(start, len) { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - accumulator.add_movie( - movie_row.values(), - movie_title_index, - movie_rating_index, - ); - } - } - } - RuntimeRowIdSet::Many(row_ids) => { - for row_id in row_ids { - if let Some(movie_row) = movie_source.row_by_id(*row_id)? { - accumulator.add_movie( - movie_row.values(), - movie_title_index, - movie_rating_index, - ); - } - } + if !payload.starts_with(TABLE_PAYLOAD_MAGIC) { + return Err(DbError::corruption("table payload magic is invalid")); + } + let mut cursor = Cursor::new(payload); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + let mut entries = BTreeMap::new(); + for _ in 0..row_count { + let row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes_offset = cursor.offset; + cursor.read_slice(row_bytes_len)?; + if is_tombstone { + continue; } - RuntimeRowIdSet::Owned(row_ids) => { - for row_id in row_ids { - if let Some(movie_row) = movie_source.row_by_id(row_id)? { - accumulator.add_movie( - movie_row.values(), - movie_title_index, - movie_rating_index, - ); - } - } + let locator = RowLocatorV1 { + byte_offset: u32::try_from(row_bytes_offset) + .map_err(|_| DbError::constraint("row locator offset exceeds u32"))?, + byte_len: u32::try_from(row_bytes_len) + .map_err(|_| DbError::constraint("row locator length exceeds u32"))?, + }; + entries.insert( + encode_row_id_locator_key(row_id), + encode_row_locator(locator), + ); + } + Ok(entries) +} + +fn build_paged_row_locator_entries_from_chunk_payloads( + chunk_payloads: &[TablePageManifestChunk], +) -> Result>> { + let mut entries = BTreeMap::new(); + for (chunk_index, chunk) in chunk_payloads.iter().enumerate() { + let skip = chunk.tombstoned_row_ids.iter().copied().collect(); + append_paged_row_locator_entries( + &mut entries, + chunk.payload.as_slice(), + u32::try_from(chunk_index) + .map_err(|_| DbError::constraint("paged table chunk index exceeds u32"))?, + false, + &skip, + )?; + if let Some(overlay) = &chunk.overlay_payload { + append_paged_row_locator_entries( + &mut entries, + overlay.as_slice(), + u32::try_from(chunk_index) + .map_err(|_| DbError::constraint("paged table chunk index exceeds u32"))?, + true, + &BTreeSet::new(), + )?; } } - Ok(()) + Ok(entries) } -fn projection_expr_matches_binding_column( - item: &SelectItem, - binding: TableBindingRef<'_>, - column: &str, -) -> bool { - matches!( - item, - SelectItem::Expr { expr, .. } if expr_matches_binding_column_or_unqualified(expr, binding, column) - ) +fn deferred_compressed_lookup_cache() -> &'static Mutex { + DEFERRED_COMPRESSED_LOOKUP_CACHE + .get_or_init(|| Mutex::new(DeferredCompressedLookupCache::default())) } -fn group_exprs_match_binding_columns( - group_by: &[Expr], - binding: TableBindingRef<'_>, - columns: &[&str], -) -> bool { - group_by.len() == columns.len() - && group_by - .iter() - .zip(columns) - .all(|(expr, column)| expr_matches_binding_column_or_unqualified(expr, binding, column)) +fn deferred_runtime_btree_index_cache() -> &'static Mutex { + DEFERRED_RUNTIME_BTREE_INDEX_CACHE + .get_or_init(|| Mutex::new(DeferredRuntimeBtreeIndexCache::default())) } -fn expr_matches_binding_column_or_unqualified( - expr: &Expr, - binding: TableBindingRef<'_>, - column: &str, -) -> bool { - match expr { - Expr::Column { - table: None, - column: expr_column, - } => identifiers_equal(expr_column, column), - _ => expr_matches_binding_column(expr, binding, column), - } +fn cached_deferred_runtime_btree_index( + key: &DeferredRuntimeBtreeIndexCacheKey, +) -> Result>> { + let cache = deferred_runtime_btree_index_cache() + .lock() + .map_err(|_| DbError::internal("deferred runtime index cache lock poisoned"))?; + Ok(cache.entries.get(key).cloned()) } -fn equality_filter_text_literal<'a>( - expr: &'a Expr, - binding: TableBindingRef<'_>, - column: &str, -) -> Option<&'a str> { - let Expr::Binary { - left, - op: BinaryOp::Eq, - right, - } = expr - else { - return None; - }; - if expr_matches_binding_column(left, binding, column) { - return text_literal_value(right); - } - if expr_matches_binding_column(right, binding, column) { - return text_literal_value(left); +fn cache_deferred_runtime_btree_index( + key: DeferredRuntimeBtreeIndexCacheKey, + entry: DeferredRuntimeBtreeIndexCacheEntry, +) -> Result<()> { + let mut cache = deferred_runtime_btree_index_cache() + .lock() + .map_err(|_| DbError::internal("deferred runtime index cache lock poisoned"))?; + if !cache.entries.contains_key(&key) { + if cache.entries.len() >= DEFERRED_RUNTIME_BTREE_INDEX_CACHE_LIMIT { + if let Some(evicted) = cache.insertion_order.pop_front() { + cache.entries.remove(&evicted); + } + } + cache.insertion_order.push_back(key.clone()); } - None + cache.entries.insert(key, Arc::new(entry)); + Ok(()) } -fn text_literal_value(expr: &Expr) -> Option<&str> { - match expr { - Expr::Literal(Value::Text(value)) => Some(value.as_str()), - _ => None, +fn deferred_compressed_lookup_cache_key( + state: PersistedTableState, +) -> DeferredCompressedLookupCacheKey { + DeferredCompressedLookupCacheKey { + head_page_id: state.pointer.head_page_id, + logical_len: state.pointer.logical_len, + flags: state.pointer.flags, + checksum: state.checksum, } } -fn projection_expr_string_agg_separator<'a>( - item: &'a SelectItem, - binding: TableBindingRef<'_>, - column: &str, -) -> Option<&'a str> { - let SelectItem::Expr { expr, .. } = item else { - return None; - }; - let Expr::Aggregate { - name, - args, - distinct, - star, - order_by, - within_group, - } = expr - else { - return None; - }; - if !(name.eq_ignore_ascii_case("string_agg") || name.eq_ignore_ascii_case("group_concat")) - || *distinct - || *star - || !order_by.is_empty() - || *within_group - || args.len() != 2 - || !expr_matches_binding_column(&args[0], binding, column) +fn read_deferred_compressed_table_lookup_entry( + store: &S, + state: PersistedTableState, +) -> Result> { + let cache_key = deferred_compressed_lookup_cache_key(state); { - return None; + let cache = deferred_compressed_lookup_cache() + .lock() + .map_err(|_| DbError::internal("deferred compressed lookup cache lock poisoned"))?; + if let Some(entry) = cache.entries.get(&cache_key) { + return Ok(entry.clone()); + } } - text_literal_value(&args[1]) + + let payload = Arc::new(read_overflow(store, state.pointer)?); + if crc32c_parts(&[payload.as_slice()]) != state.checksum { + return Err(DbError::corruption( + "deferred table payload checksum mismatch", + )); + } + let entry = Arc::new(decode_compressed_table_payload_lookup_entry(payload)?); + + let mut cache = deferred_compressed_lookup_cache() + .lock() + .map_err(|_| DbError::internal("deferred compressed lookup cache lock poisoned"))?; + if let Some(existing) = cache.entries.get(&cache_key) { + return Ok(existing.clone()); + } + if cache.entries.len() >= DEFERRED_COMPRESSED_LOOKUP_CACHE_LIMIT { + if let Some(evicted) = cache.insertion_order.pop_front() { + cache.entries.remove(&evicted); + } + } + cache.insertion_order.push_back(cache_key); + cache.entries.insert(cache_key, entry.clone()); + Ok(entry) } -fn aggregate_matches_single_binding_column_or_unqualified( - expr: &Expr, - aggregate_name: &str, - binding: TableBindingRef<'_>, - column: &str, -) -> bool { - let Expr::Aggregate { - name, - args, - distinct, - star, - order_by, - within_group, - } = expr - else { - return false; - }; - if !name.eq_ignore_ascii_case(aggregate_name) - || *distinct - || *star - || !order_by.is_empty() - || *within_group - || args.len() != 1 - { - return false; +fn read_deferred_row_by_locator_from_table_payload( + store: &S, + state: PersistedTableState, + row_id: i64, + locator: RowLocatorV1, +) -> Result> { + let pointer = state.pointer; + if pointer.head_page_id == 0 { + return Ok(None); + } + if pointer.is_compressed() { + let entry = read_deferred_compressed_table_lookup_entry(store, state)?; + return decode_row_by_locator_from_payload(entry.payload.as_slice(), row_id, locator) + .map(Some); } - expr_matches_binding_column_or_unqualified(&args[0], binding, column) + let mut cursor = OverflowPayloadCursor::new(store, pointer); + cursor.skip(locator.byte_offset as usize)?; + let row_bytes = cursor.read_vec(locator.byte_len as usize)?; + let row = Row::decode(&row_bytes)?; + Ok(Some(StoredRow { + row_id, + values: row.into_values(), + })) } -fn aggregate_matches_status_case_sum( - expr: &Expr, - aggregate_name: &str, - binding: TableBindingRef<'_>, - status_column: &str, - status_value: &str, -) -> bool { - let Expr::Aggregate { - name, - args, - distinct, - star, - order_by, - within_group, - } = expr - else { - return false; - }; - if !name.eq_ignore_ascii_case(aggregate_name) - || *distinct - || *star - || !order_by.is_empty() - || *within_group - || args.len() != 1 - { - return false; +fn read_deferred_row_by_locator_from_paged_table_payload( + store: &S, + state: PersistedTableState, + row_id: i64, + locator: RowLocatorV2, +) -> Result> { + let manifest_payload = read_overflow(store, state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); } - let Expr::Case { - operand: None, - branches, - else_expr: Some(else_expr), - } = &args[0] - else { - return false; + let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + let chunk = manifest + .chunks + .get(locator.chunk_index as usize) + .ok_or_else(|| DbError::corruption("paged table locator chunk index is invalid"))?; + let pointer = if locator.is_overlay { + chunk.overlay_pointer.ok_or_else(|| { + DbError::corruption("paged table overlay pointer missing for overlay locator") + })? + } else { + chunk.pointer }; - if branches.len() != 1 - || !matches!(&branches[0].1, Expr::Literal(Value::Int64(1))) - || !matches!(&**else_expr, Expr::Literal(Value::Int64(0))) - { - return false; - } - status_case_condition_matches(&branches[0].0, binding, status_column, status_value) + let payload = read_overflow(store, pointer)?; + let start = locator.byte_offset as usize; + let end = start.saturating_add(locator.byte_len as usize); + let row_bytes = payload + .get(start..end) + .ok_or_else(|| DbError::corruption("paged row locator exceeded payload length"))?; + let row = Row::decode(row_bytes)?; + Ok(Some(StoredRow { + row_id, + values: row.into_values(), + })) } -fn status_case_condition_matches( - expr: &Expr, - binding: TableBindingRef<'_>, - status_column: &str, - status_value: &str, -) -> bool { - let Expr::Binary { left, op, right } = expr else { - return false; +fn read_deferred_row_by_cached_paged_locator( + store: &S, + row_id: i64, + cached: CachedPagedRowLocator, + verified_payload: Option<&[u8]>, +) -> Result> { + let owned_payload; + let payload = if let Some(payload) = verified_payload { + payload + } else { + owned_payload = read_overflow(store, cached.pointer)?; + if crc32c_parts(&[owned_payload.as_slice()]) != cached.checksum { + return Err(DbError::corruption("paged table chunk checksum mismatch")); + } + owned_payload.as_slice() }; - if *op != BinaryOp::Eq { - return false; - } - (expr_matches_binding_column(left, binding, status_column) - && matches!(&**right, Expr::Literal(Value::Text(value)) if value == status_value)) - || (expr_matches_binding_column(right, binding, status_column) - && matches!(&**left, Expr::Literal(Value::Text(value)) if value == status_value)) + decode_row_by_locator_from_payload(payload, row_id, cached.locator).map(Some) } -fn aggregate_matches_binding_product( - expr: &Expr, - aggregate_name: &str, - left_binding: TableBindingRef<'_>, - left_column: &str, - right_binding: TableBindingRef<'_>, - right_column: &str, -) -> bool { - let Expr::Aggregate { - name, - args, - distinct, - star, - order_by, - within_group, - } = expr - else { - return false; - }; - if !name.eq_ignore_ascii_case(aggregate_name) - || *distinct - || *star - || !order_by.is_empty() - || *within_group - || args.len() != 1 - { - return false; - } - let Expr::Binary { left, op, right } = &args[0] else { - return false; +fn read_deferred_projected_values_by_cached_paged_locator( + store: &S, + cached: CachedPagedRowLocator, + verified_payload: Option<&[u8]>, + projection_indexes: &[usize], +) -> Result> { + let owned_payload; + let payload = if let Some(payload) = verified_payload { + payload + } else { + owned_payload = read_overflow(store, cached.pointer)?; + if crc32c_parts(&[owned_payload.as_slice()]) != cached.checksum { + return Err(DbError::corruption("paged table chunk checksum mismatch")); + } + owned_payload.as_slice() }; - if *op != BinaryOp::Mul { - return false; - } - (expr_matches_binding_column(left, left_binding, left_column) - && expr_matches_binding_column(right, right_binding, right_column)) - || (expr_matches_binding_column(left, right_binding, right_column) - && expr_matches_binding_column(right, left_binding, left_column)) + decode_projected_values_by_locator_from_payload( + Some(store), + payload, + cached.locator, + projection_indexes, + ) } -fn classify_indexed_join_aggregate( - expr: &Expr, - binding: TableBindingRef<'_>, - schema: &TableSchema, -) -> Option { - let Expr::Aggregate { - name, - args, - distinct, - star, - order_by, - within_group, - } = expr - else { - return None; - }; - if !order_by.is_empty() || *within_group { - return None; - } - if *star && name.eq_ignore_ascii_case("count") && args.is_empty() && !*distinct { - return Some(IndexedJoinAggregateKind::CountRows); - } - if args.len() != 1 { - return None; +fn read_deferred_projected_values_by_cached_paged_locator_with_query_cache( + store: &S, + cached: CachedPagedRowLocator, + verified_payload: Option<&Arc>>, + chunk_payload_cache: &mut HashMap>>, + projection_indexes: &[usize], +) -> Result> { + if let Some(payload) = verified_payload { + return decode_projected_values_by_locator_from_payload( + Some(store), + payload.as_slice(), + cached.locator, + projection_indexes, + ); } - let col = resolved_child_column_index(args.first()?, binding, schema)?; - let name_lower = name.to_lowercase(); - match name_lower.as_str() { - "count" if !*distinct => Some(IndexedJoinAggregateKind::CountNonNull(col)), - "count" if *distinct => Some(IndexedJoinAggregateKind::CountDistinct(col)), - "sum" if !*distinct => Some(IndexedJoinAggregateKind::Sum(col)), - "avg" if !*distinct => Some(IndexedJoinAggregateKind::Avg(col)), - "min" if !*distinct => Some(IndexedJoinAggregateKind::Min(col)), - "max" if !*distinct => Some(IndexedJoinAggregateKind::Max(col)), - _ => None, + let key = CachedPagedChunkPayloadKey::new(cached.pointer, cached.checksum); + if let Some(payload) = chunk_payload_cache.get(&key) { + return decode_projected_values_by_locator_from_payload( + Some(store), + payload.as_slice(), + cached.locator, + projection_indexes, + ); } -} -fn resolved_child_column_index( - expr: &Expr, - binding: TableBindingRef<'_>, - schema: &TableSchema, -) -> Option { - let Expr::Column { table, column } = expr else { - return None; - }; - if let Some(table_ref) = table { - if !identifiers_equal(table_ref, binding.name) - && !binding - .alias - .as_ref() - .is_some_and(|alias| identifiers_equal(table_ref, alias)) - { - return None; - } + let payload = Arc::new(read_overflow(store, cached.pointer)?); + if crc32c_parts(&[payload.as_slice()]) != cached.checksum { + return Err(DbError::corruption("paged table chunk checksum mismatch")); } - schema - .columns - .iter() - .position(|col| identifiers_equal(&col.name, column)) - .or_else(|| { - let lowered = column.to_lowercase(); - schema - .columns - .iter() - .position(|col| col.name.to_lowercase() == lowered) - }) + let values = decode_projected_values_by_locator_from_payload( + Some(store), + payload.as_slice(), + cached.locator, + projection_indexes, + )?; + chunk_payload_cache.insert(key, payload); + Ok(values) } -fn order_by_matches_alias_or_projection( - order_by: &crate::sql::ast::OrderBy, - alias: Option<&str>, - projection_expr: &Expr, - descending: bool, -) -> bool { - if order_by.descending != descending { - return false; - } - if let Some(alias) = alias { - if let Expr::Column { - table: None, - column, - } = &order_by.expr - { - if identifiers_equal(column.as_str(), alias) { - return true; - } - } +fn read_deferred_projected_values_by_locator_from_table_payload( + store: &S, + state: PersistedTableState, + locator: RowLocatorV1, + projection_indexes: &[usize], +) -> Result>> { + let pointer = state.pointer; + if pointer.head_page_id == 0 { + return Ok(None); } - &order_by.expr == projection_expr -} - -fn projection_order_by_plan( - order_by: &[crate::sql::ast::OrderBy], - projection: &[SelectItem], -) -> Option> { - if order_by.is_empty() { - return None; + if pointer.is_compressed() { + let entry = read_deferred_compressed_table_lookup_entry(store, state)?; + return decode_projected_values_by_locator_from_payload( + Some(store), + entry.payload.as_slice(), + locator, + projection_indexes, + ) + .map(Some); } - order_by - .iter() - .map(|entry| { - order_by_projection_index(entry, projection).map(|projection_index| SimpleOrderByPlan { - projection_index, - descending: entry.descending, - collation: entry.collation.clone(), - }) - }) - .collect() + + let mut cursor = OverflowPayloadCursor::new(store, pointer); + cursor.skip(locator.byte_offset as usize)?; + let row_bytes = cursor.read_vec(locator.byte_len as usize)?; + Row::decode_projection_sorted_unique_with_overflow( + row_bytes.as_slice(), + Some(store), + projection_indexes, + ) + .map(Some) } -fn order_by_projection_index( - order_by: &crate::sql::ast::OrderBy, - projection: &[SelectItem], -) -> Option { - let mut matched = None; - for (index, item) in projection.iter().enumerate() { - let item_matches = match item { - SelectItem::Expr { expr, alias } => { - let column_match = if let Expr::Column { table, column } = &order_by.expr { - if table.is_none() - && alias - .as_deref() - .is_some_and(|alias| identifiers_equal(column, alias)) - { - true - } else if let Expr::Column { - table: projection_table, - column: projection_column, - } = expr - { - let qualifier_matches = - match (table.as_deref(), projection_table.as_deref()) { - (Some(order_table), Some(projection_table)) => { - identifiers_equal(order_table, projection_table) - } - (Some(_), None) | (None, _) => true, - }; - qualifier_matches && identifiers_equal(column, projection_column) - } else { - false - } - } else { - false - }; - column_match || &order_by.expr == expr - } - SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => false, - }; - if item_matches && matched.replace(index).is_some() { - return None; - } +fn read_deferred_projected_values_by_locator_from_paged_table_payload( + store: &S, + state: PersistedTableState, + locator: RowLocatorV2, + projection_indexes: &[usize], +) -> Result>> { + let manifest_payload = read_overflow(store, state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); } - matched + let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + let chunk = manifest + .chunks + .get(locator.chunk_index as usize) + .ok_or_else(|| DbError::corruption("paged table locator chunk index is invalid"))?; + let pointer = if locator.is_overlay { + chunk.overlay_pointer.ok_or_else(|| { + DbError::corruption("paged table overlay pointer missing for overlay locator") + })? + } else { + chunk.pointer + }; + let payload = read_overflow(store, pointer)?; + decode_projected_values_by_locator_from_payload( + Some(store), + payload.as_slice(), + RowLocatorV1 { + byte_offset: locator.byte_offset, + byte_len: locator.byte_len, + }, + projection_indexes, + ) + .map(Some) } -fn sort_dataset_by_projection_order( - runtime: Option<&EngineRuntime>, - dataset: &mut Dataset, - order_by: &[SimpleOrderByPlan], -) -> Result<()> { - let mut sort_error = None; - dataset.rows_mut().sort_by(|left, right| { - for order in order_by { - let ordering = compare_values_with_runtime_collation( - runtime, - &left[order.projection_index], - &right[order.projection_index], - order.collation.clone(), - ); - match ordering { - Ok(std::cmp::Ordering::Equal) => continue, - Ok(ordering) => { - return if order.descending { - ordering.reverse() - } else { - ordering - }; - } - Err(error) => { - if sort_error.is_none() { - sort_error = Some(error); - } - return std::cmp::Ordering::Equal; - } +fn read_deferred_projected_values_by_locator( + store: &S, + state: PersistedTableState, + locator: DecodedRowLocator, + projection_indexes: &[usize], +) -> Result>> { + if state.pointer.is_table_paged_manifest() { + return match locator { + DecodedRowLocator::V2(locator) => { + read_deferred_projected_values_by_locator_from_paged_table_payload( + store, + state, + locator, + projection_indexes, + ) } - } - std::cmp::Ordering::Equal - }); - if let Some(error) = sort_error { - return Err(error); + DecodedRowLocator::V1(_) => Err(DbError::corruption( + "paged table persistent pk locator payload is invalid", + )), + }; } - Ok(()) -} -fn analyze_simple_grouped_numeric_aggregate_binding( - expr: &Expr, - projection_index: usize, - table_name: &str, - binding_name: &str, - table_binding: TableBindingRef<'_>, - table_schema: &TableSchema, -) -> Option { - let Expr::Aggregate { - name, - args, - distinct, - star, - order_by, - within_group, - } = expr - else { - return None; - }; - if !order_by.is_empty() || *within_group { - return None; - } - if name.eq_ignore_ascii_case("count") { - if *distinct && *star { - return None; - } - if args.is_empty() && *star { - return Some(SimpleGroupedNumericAggregateBinding { - kind: SimpleGroupedNumericAggregateKind::CountRows, - projection_index, - source_column_name: None, - source_column_index: None, - source_expr: None, - }); - } - if *star || args.len() != 1 || !expr_references_only_binding(&args[0], table_binding) { - return None; + match locator { + DecodedRowLocator::V1(locator) => { + read_deferred_projected_values_by_locator_from_table_payload( + store, + state, + locator, + projection_indexes, + ) } - if let Expr::Column { table, column } = &args[0] { - if let Some(table) = table.as_deref() { - if !identifiers_equal(table, table_name) && !identifiers_equal(table, binding_name) - { - return None; - } - } - let column_index = table_schema - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, column))?; - return Some(SimpleGroupedNumericAggregateBinding { - kind: if *distinct { - SimpleGroupedNumericAggregateKind::CountDistinct - } else { - SimpleGroupedNumericAggregateKind::CountNonNull + DecodedRowLocator::V2(locator) => { + read_deferred_projected_values_by_locator_from_table_payload( + store, + state, + RowLocatorV1 { + byte_offset: locator.byte_offset, + byte_len: locator.byte_len, }, - projection_index, - source_column_name: Some(column.clone()), - source_column_index: Some(column_index), - source_expr: None, - }); + projection_indexes, + ) } - return Some(SimpleGroupedNumericAggregateBinding { - kind: if *distinct { - SimpleGroupedNumericAggregateKind::CountDistinct - } else { - SimpleGroupedNumericAggregateKind::CountNonNull - }, - projection_index, - source_column_name: None, - source_column_index: None, - source_expr: Some(args[0].clone()), - }); } +} - let kind = if name.eq_ignore_ascii_case("sum") { - if *distinct { - SimpleGroupedNumericAggregateKind::SumDistinct - } else { - SimpleGroupedNumericAggregateKind::Sum - } - } else if name.eq_ignore_ascii_case("avg") { - if *distinct { - SimpleGroupedNumericAggregateKind::AvgDistinct - } else { - SimpleGroupedNumericAggregateKind::Avg - } - } else if name.eq_ignore_ascii_case("total") { - if *distinct { - SimpleGroupedNumericAggregateKind::TotalDistinct - } else { - SimpleGroupedNumericAggregateKind::Total - } - } else if name.eq_ignore_ascii_case("stddev") || name.eq_ignore_ascii_case("stddev_samp") { - if *distinct { - SimpleGroupedNumericAggregateKind::StddevSampDistinct - } else { - SimpleGroupedNumericAggregateKind::StddevSamp - } - } else if name.eq_ignore_ascii_case("stddev_pop") { - if *distinct { - SimpleGroupedNumericAggregateKind::StddevPopDistinct - } else { - SimpleGroupedNumericAggregateKind::StddevPop - } - } else if name.eq_ignore_ascii_case("variance") || name.eq_ignore_ascii_case("var_samp") { - if *distinct { - SimpleGroupedNumericAggregateKind::VarSampDistinct - } else { - SimpleGroupedNumericAggregateKind::VarSamp - } - } else if name.eq_ignore_ascii_case("var_pop") { - if *distinct { - SimpleGroupedNumericAggregateKind::VarPopDistinct - } else { - SimpleGroupedNumericAggregateKind::VarPop - } - } else if name.eq_ignore_ascii_case("bool_and") { - if *distinct { - SimpleGroupedNumericAggregateKind::BoolAndDistinct - } else { - SimpleGroupedNumericAggregateKind::BoolAnd - } - } else if name.eq_ignore_ascii_case("bool_or") { - if *distinct { - SimpleGroupedNumericAggregateKind::BoolOrDistinct - } else { - SimpleGroupedNumericAggregateKind::BoolOr - } - } else if name.eq_ignore_ascii_case("min") { - if *distinct { - return None; +fn read_deferred_row_by_id_from_paged_chunk( + store: &S, + chunk: &PersistedTableChunkState, + row_id: i64, +) -> Result> { + if let Some(overlay_pointer) = chunk.overlay_pointer { + let overlay_payload = read_overflow(store, overlay_pointer)?; + if Some(crc32c_parts(&[overlay_payload.as_slice()])) != chunk.overlay_checksum { + return Err(DbError::corruption( + "paged table overlay chunk checksum mismatch", + )); } - SimpleGroupedNumericAggregateKind::Min - } else if name.eq_ignore_ascii_case("max") { - if *distinct { - return None; + if let Some(row) = read_row_from_table_payload_by_id(overlay_payload.as_slice(), row_id)? { + return Ok(Some(row)); } - SimpleGroupedNumericAggregateKind::Max - } else { - return None; - }; - if *star || args.len() != 1 { - return None; - } - if !expr_references_only_binding(&args[0], table_binding) { - return None; } - if matches!( - kind, - SimpleGroupedNumericAggregateKind::Min | SimpleGroupedNumericAggregateKind::Max - ) { - return Some(SimpleGroupedNumericAggregateBinding { - kind, - projection_index, - source_column_name: None, - source_column_index: None, - source_expr: Some(args[0].clone()), - }); + if !chunk.tombstoned_row_ids.is_empty() && chunk.tombstoned_row_ids.contains(&row_id) { + return Ok(None); } - if let Expr::Column { table, column } = &args[0] { - if let Some(table) = table.as_deref() { - if !identifiers_equal(table, table_name) && !identifiers_equal(table, binding_name) { - return None; - } - } - let column_index = table_schema - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, column))?; - Some(SimpleGroupedNumericAggregateBinding { - kind, - projection_index, - source_column_name: Some(column.clone()), - source_column_index: Some(column_index), - source_expr: None, - }) - } else { - Some(SimpleGroupedNumericAggregateBinding { - kind, - projection_index, - source_column_name: None, - source_column_index: None, - source_expr: Some(args[0].clone()), - }) + let payload = read_overflow(store, chunk.pointer)?; + if crc32c_parts(&[payload.as_slice()]) != chunk.checksum { + return Err(DbError::corruption("paged table chunk checksum mismatch")); } + read_row_from_table_payload_by_id(payload.as_slice(), row_id) } -fn simple_aggregate_source_column_index( - aggregate: &SimpleGroupedNumericAggregateBinding, - table_schema: &TableSchema, -) -> Option { - if let Some(column_index) = aggregate.source_column_index { - return Some(column_index); +fn read_row_from_table_payload_by_id(payload: &[u8], row_id: i64) -> Result> { + if payload.is_empty() { + return Ok(None); } - let Some(Expr::Column { column, .. }) = aggregate.source_expr.as_ref() else { - return None; - }; - schema_column_index(table_schema, column) + let mut cursor = Cursor::new(payload); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + let row_count = cursor.read_u32()? as usize; + for _ in 0..row_count { + let candidate_row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + let row_bytes = cursor.read_slice(row_bytes_len)?; + if is_tombstone { + continue; + } + if candidate_row_id == row_id { + let row = Row::decode(row_bytes)?; + return Ok(Some(StoredRow { + row_id: candidate_row_id, + values: row.into_values(), + })); + } + } + Ok(None) } -fn matching_simple_grouped_aggregate_binding<'a>( - expr: &Expr, - table_name: &str, - binding_name: &str, - aggregate_bindings: &'a [SimpleGroupedNumericAggregateBinding], -) -> Option<&'a SimpleGroupedNumericAggregateBinding> { - let Expr::Aggregate { - name, - args, - distinct, - star, - order_by, - within_group, - } = expr - else { - return None; - }; - if !order_by.is_empty() || *within_group { - return None; +fn read_deferred_row_by_id_from_paged_table_manifest( + store: &S, + state: PersistedTableState, + row_id: i64, +) -> Result> { + let manifest_payload = read_overflow(store, state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); } - aggregate_bindings + let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + let total_row_count = manifest + .chunks .iter() - .find(|binding| match binding.kind { - SimpleGroupedNumericAggregateKind::CountRows => { - name.eq_ignore_ascii_case("count") && !*distinct && args.is_empty() && *star - } - SimpleGroupedNumericAggregateKind::CountNonNull => { - if !name.eq_ignore_ascii_case("count") || *distinct || *star || args.len() != 1 { - return false; - } - if let Some(expected) = binding.source_expr.as_ref() { - expected == &args[0] - && expr_references_binding_names(&args[0], table_name, binding_name) - } else { - let Expr::Column { table, column } = &args[0] else { - return false; - }; - if let Some(table) = table.as_deref() { - if !identifiers_equal(table, table_name) - && !identifiers_equal(table, binding_name) - { - return false; - } - } - binding - .source_column_name - .as_deref() - .is_some_and(|expected| identifiers_equal(column, expected)) - } - } - SimpleGroupedNumericAggregateKind::CountDistinct => { - if !name.eq_ignore_ascii_case("count") || !*distinct || *star || args.len() != 1 { - return false; - } - if let Some(expected) = binding.source_expr.as_ref() { - expected == &args[0] - && expr_references_binding_names(&args[0], table_name, binding_name) - } else { - let Expr::Column { table, column } = &args[0] else { - return false; - }; - if let Some(table) = table.as_deref() { - if !identifiers_equal(table, table_name) - && !identifiers_equal(table, binding_name) - { - return false; - } - } - binding - .source_column_name - .as_deref() - .is_some_and(|expected| identifiers_equal(column, expected)) - } - } - SimpleGroupedNumericAggregateKind::Sum - | SimpleGroupedNumericAggregateKind::SumDistinct - | SimpleGroupedNumericAggregateKind::Avg - | SimpleGroupedNumericAggregateKind::AvgDistinct - | SimpleGroupedNumericAggregateKind::Total - | SimpleGroupedNumericAggregateKind::TotalDistinct - | SimpleGroupedNumericAggregateKind::StddevSamp - | SimpleGroupedNumericAggregateKind::StddevSampDistinct - | SimpleGroupedNumericAggregateKind::StddevPop - | SimpleGroupedNumericAggregateKind::StddevPopDistinct - | SimpleGroupedNumericAggregateKind::VarSamp - | SimpleGroupedNumericAggregateKind::VarSampDistinct - | SimpleGroupedNumericAggregateKind::VarPop - | SimpleGroupedNumericAggregateKind::VarPopDistinct - | SimpleGroupedNumericAggregateKind::BoolAnd - | SimpleGroupedNumericAggregateKind::BoolAndDistinct - | SimpleGroupedNumericAggregateKind::BoolOr - | SimpleGroupedNumericAggregateKind::BoolOrDistinct => { - let expected_distinct = binding.kind.uses_distinct(); - if !binding.kind.matches_aggregate_name(name) - || *distinct != expected_distinct - || *star - || args.len() != 1 - { - return false; - } - if let Some(expected) = binding.source_expr.as_ref() { - expected == &args[0] - && expr_references_binding_names(&args[0], table_name, binding_name) - } else { - let Expr::Column { table, column } = &args[0] else { - return false; - }; - if let Some(table) = table.as_deref() { - if !identifiers_equal(table, table_name) - && !identifiers_equal(table, binding_name) - { - return false; - } - } - binding - .source_column_name - .as_deref() - .is_some_and(|expected| identifiers_equal(column, expected)) - } - } - SimpleGroupedNumericAggregateKind::Min | SimpleGroupedNumericAggregateKind::Max => { - if !binding.kind.matches_aggregate_name(name) || *star || args.len() != 1 { - return false; - } - binding.source_expr.as_ref().is_some_and(|expected| { - expected == &args[0] - && expr_references_binding_names(&args[0], table_name, binding_name) - }) - } - }) -} - -#[allow(clippy::too_many_arguments)] -fn collect_simple_grouped_numeric_projection_aggregates( - expr: &Expr, - table_name: &str, - binding_name: &str, - table_binding: TableBindingRef<'_>, - table_schema: &TableSchema, - aggregate_bindings: &mut Vec, - saw_supported_aggregate: &mut bool, -) -> Option<()> { - match expr { - Expr::Literal(_) | Expr::Parameter(_) => Some(()), - Expr::Unary { expr, .. } - | Expr::Cast { expr, .. } - | Expr::IsNull { expr, .. } - | Expr::Collate { expr, .. } => collect_simple_grouped_numeric_projection_aggregates( - expr, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - ), - Expr::Binary { left, right, .. } => { - collect_simple_grouped_numeric_projection_aggregates( - left, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - collect_simple_grouped_numeric_projection_aggregates( - right, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - ) - } - Expr::Between { - expr, low, high, .. - } => { - collect_simple_grouped_numeric_projection_aggregates( - expr, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - collect_simple_grouped_numeric_projection_aggregates( - low, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - collect_simple_grouped_numeric_projection_aggregates( - high, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - ) - } - Expr::InList { expr, items, .. } => { - collect_simple_grouped_numeric_projection_aggregates( - expr, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - for item in items { - collect_simple_grouped_numeric_projection_aggregates( - item, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - } - Some(()) - } - Expr::Like { - expr, - pattern, - escape, - .. - } => { - collect_simple_grouped_numeric_projection_aggregates( - expr, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - collect_simple_grouped_numeric_projection_aggregates( - pattern, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - if let Some(escape) = escape { - collect_simple_grouped_numeric_projection_aggregates( - escape, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - } - Some(()) + .fold(0usize, |total, chunk| total.saturating_add(chunk.row_count)); + if state.row_count != 0 && total_row_count != state.row_count { + return Err(DbError::corruption( + "paged table manifest row count mismatch", + )); + } + + let mut checked_chunks = BTreeSet::new(); + for position in [ + row_id + .checked_sub(1) + .and_then(|position| usize::try_from(position).ok()), + usize::try_from(row_id).ok(), + ] + .into_iter() + .flatten() + { + let Some(chunk_index) = manifest_chunk_index_for_row_position(&manifest.chunks, position) + else { + continue; + }; + if !checked_chunks.insert(chunk_index) { + continue; } - Expr::Function { args, .. } | Expr::Row(args) => { - for arg in args { - collect_simple_grouped_numeric_projection_aggregates( - arg, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - } - Some(()) + if let Some(row) = + read_deferred_row_by_id_from_paged_chunk(store, &manifest.chunks[chunk_index], row_id)? + { + return Ok(Some(row)); } - Expr::Case { - operand, - branches, - else_expr, - } => { - if let Some(operand) = operand { - collect_simple_grouped_numeric_projection_aggregates( - operand, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - } - for (condition, value) in branches { - collect_simple_grouped_numeric_projection_aggregates( - condition, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - collect_simple_grouped_numeric_projection_aggregates( - value, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - } - if let Some(else_expr) = else_expr { - collect_simple_grouped_numeric_projection_aggregates( - else_expr, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - } - Some(()) + } + + for (chunk_index, chunk) in manifest.chunks.iter().enumerate() { + if checked_chunks.contains(&chunk_index) { + continue; } - Expr::Aggregate { .. } => { - let binding = analyze_simple_grouped_numeric_aggregate_binding( - expr, - usize::MAX, - table_name, - binding_name, - table_binding, - table_schema, - )?; - if !matches!(binding.kind, SimpleGroupedNumericAggregateKind::CountRows) { - *saw_supported_aggregate = true; - } - if !aggregate_bindings.iter().any(|existing| { - existing.kind == binding.kind - && existing.source_column_name == binding.source_column_name - && existing.source_expr == binding.source_expr - }) { - aggregate_bindings.push(binding); - } - Some(()) + if let Some(row) = read_deferred_row_by_id_from_paged_chunk(store, chunk, row_id)? { + return Ok(Some(row)); } - Expr::Column { .. } - | Expr::RowNumber { .. } - | Expr::WindowFunction { .. } - | Expr::InSubquery { .. } - | Expr::CompareSubquery { .. } - | Expr::ScalarSubquery(_) - | Expr::Exists(_) => None, } + Ok(None) } -#[allow(clippy::too_many_arguments)] -fn collect_simple_grouped_numeric_having_aggregates( - expr: &Expr, - table_name: &str, - binding_name: &str, - table_binding: TableBindingRef<'_>, +fn read_deferred_stored_row_by_id( + store: &S, + state: PersistedTableState, table_schema: &TableSchema, - aggregate_bindings: &mut Vec, - saw_supported_aggregate: &mut bool, -) -> Option<()> { - match expr { - Expr::Literal(_) | Expr::Parameter(_) | Expr::Column { .. } => Some(()), - Expr::Unary { expr, .. } - | Expr::Cast { expr, .. } - | Expr::IsNull { expr, .. } - | Expr::Collate { expr, .. } => collect_simple_grouped_numeric_having_aggregates( - expr, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - ), - Expr::Binary { left, right, .. } => { - collect_simple_grouped_numeric_having_aggregates( - left, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - collect_simple_grouped_numeric_having_aggregates( - right, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - ) + row_id: i64, + use_persistent_pk_index: bool, + paged_locator_cache: Option<&DeferredPagedRowLocatorCache>, +) -> Result> { + let locator = if use_persistent_pk_index { + if let Some(pk_index_root) = table_schema.pk_index_root { + btree_find_exact( + store, + Some(pk_index_root), + encode_row_id_locator_key(row_id), + )? + .map(|payload| decode_row_locator(&payload)) + .transpose()? + } else { + None } - Expr::Between { - expr, low, high, .. - } => { - collect_simple_grouped_numeric_having_aggregates( - expr, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - collect_simple_grouped_numeric_having_aggregates( - low, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - collect_simple_grouped_numeric_having_aggregates( - high, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - ) + } else { + None + }; + + if state.pointer.is_table_paged_manifest() { + if let Some(cache) = paged_locator_cache.filter(|cache| cache.matches_state(state)) { + return cache + .locators + .get(row_id) + .map(|cached| { + read_deferred_row_by_cached_paged_locator( + store, + row_id, + cached, + cache.verified_payload(cached.pointer, cached.checksum), + ) + }) + .unwrap_or(Ok(None)); } - Expr::InList { expr, items, .. } => { - collect_simple_grouped_numeric_having_aggregates( - expr, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - for item in items { - collect_simple_grouped_numeric_having_aggregates( - item, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; + return match locator { + Some(DecodedRowLocator::V2(locator)) => { + read_deferred_row_by_locator_from_paged_table_payload(store, state, row_id, locator) } - Some(()) - } - Expr::Like { - expr, - pattern, - escape, - .. - } => { - collect_simple_grouped_numeric_having_aggregates( - expr, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - collect_simple_grouped_numeric_having_aggregates( - pattern, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - if let Some(escape) = escape { - collect_simple_grouped_numeric_having_aggregates( - escape, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; + _ => read_deferred_row_by_id_from_paged_table_manifest(store, state, row_id), + }; + } + + if let Some(locator) = locator { + return match locator { + DecodedRowLocator::V1(locator) => { + read_deferred_row_by_locator_from_table_payload(store, state, row_id, locator) } - Some(()) - } - Expr::Function { args, .. } | Expr::Row(args) => { - for arg in args { - collect_simple_grouped_numeric_having_aggregates( - arg, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; + DecodedRowLocator::V2(locator) => read_deferred_row_by_locator_from_table_payload( + store, + state, + row_id, + RowLocatorV1 { + byte_offset: locator.byte_offset, + byte_len: locator.byte_len, + }, + ), + }; + } + + read_deferred_row_by_id_from_table_payload(store, state, row_id) +} + +fn read_deferred_projected_values_by_id( + store: &S, + state: PersistedTableState, + table_schema: &TableSchema, + row_id: i64, + use_persistent_pk_index: bool, + paged_locator_cache: Option<&DeferredPagedRowLocatorCache>, + projection_indexes: &[usize], +) -> Result>> { + if projection_indexes.is_empty() { + if state.pointer.is_compressed() { + let entry = read_deferred_compressed_table_lookup_entry(store, state)?; + if entry.row_locators.contains_key(&row_id) { + return Ok(Some(Vec::new())); } - Some(()) } - Expr::Case { - operand, - branches, - else_expr, - } => { - if let Some(operand) = operand { - collect_simple_grouped_numeric_having_aggregates( - operand, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - } - for (condition, value) in branches { - collect_simple_grouped_numeric_having_aggregates( - condition, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - collect_simple_grouped_numeric_having_aggregates( - value, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; - } - if let Some(else_expr) = else_expr { - collect_simple_grouped_numeric_having_aggregates( - else_expr, - table_name, - binding_name, - table_binding, - table_schema, - aggregate_bindings, - saw_supported_aggregate, - )?; + if use_persistent_pk_index { + if let Some(pk_index_root) = table_schema.pk_index_root { + if btree_find_exact( + store, + Some(pk_index_root), + encode_row_id_locator_key(row_id), + )? + .is_some() + { + return Ok(Some(Vec::new())); + } } - Some(()) } - Expr::Aggregate { .. } => { - let binding = analyze_simple_grouped_numeric_aggregate_binding( - expr, - usize::MAX, - table_name, - binding_name, - table_binding, - table_schema, - )?; - if !matches!(binding.kind, SimpleGroupedNumericAggregateKind::CountRows) { - *saw_supported_aggregate = true; - } - if !aggregate_bindings.iter().any(|existing| { - existing.kind == binding.kind - && existing.source_column_name == binding.source_column_name - && existing.source_expr == binding.source_expr - }) { - aggregate_bindings.push(binding); - } - Some(()) + if state.pointer.is_table_paged_manifest() + && paged_locator_cache + .filter(|cache| cache.matches_state(state)) + .and_then(|cache| cache.locators.get(row_id)) + .is_some() + { + return Ok(Some(Vec::new())); + } + + return read_deferred_stored_row_by_id( + store, + state, + table_schema, + row_id, + use_persistent_pk_index, + paged_locator_cache, + ) + .map(|row| row.map(|_| Vec::new())); + } + + if state.pointer.is_table_paged_manifest() { + if let Some(cache) = paged_locator_cache.filter(|cache| cache.matches_state(state)) { + return cache + .locators + .get(row_id) + .map(|cached| { + read_deferred_projected_values_by_cached_paged_locator( + store, + cached, + cache.verified_payload(cached.pointer, cached.checksum), + projection_indexes, + ) + }) + .transpose(); } - Expr::RowNumber { .. } - | Expr::WindowFunction { .. } - | Expr::InSubquery { .. } - | Expr::CompareSubquery { .. } - | Expr::ScalarSubquery(_) - | Expr::Exists(_) => None, } + + read_deferred_stored_row_by_id( + store, + state, + table_schema, + row_id, + use_persistent_pk_index, + paged_locator_cache, + ) + .map(|row| row.map(|row| project_simple_projection_value_vec(&row.values, projection_indexes))) } -fn from_item_is_all_inner_table_joins(item: &FromItem) -> bool { - match item { - FromItem::Table { .. } => true, - FromItem::Join { - left, - right, - kind: JoinKind::Inner, - constraint: JoinConstraint::On(_), - } => from_item_is_all_inner_table_joins(left) && from_item_is_all_inner_table_joins(right), - _ => false, - } +fn deferred_rowid_lookup_available( + state: PersistedTableState, + table_schema: &TableSchema, + use_persistent_pk_index: bool, + paged_locator_cache: Option<&DeferredPagedRowLocatorCache>, +) -> bool { + state.pointer.is_compressed() + || (use_persistent_pk_index && table_schema.pk_index_root.is_some()) + || paged_locator_cache.is_some_and(|cache| cache.matches_state(state)) } -fn simple_select_item_column_index( - dataset: &Dataset, - table: Option<&str>, - column: &str, -) -> Option { - let matches = dataset - .columns - .iter() - .enumerate() - .filter(|(_, binding)| { - if !identifiers_equal(&binding.name, column) { - return false; - } - match table { - Some(table_name) => binding - .table - .as_deref() - .is_some_and(|binding_table| identifiers_equal(binding_table, table_name)), - None => !binding.hidden, - } - }) - .map(|(index, _)| index) - .collect::>(); - match matches.as_slice() { - [index] => Some(*index), - _ => None, +fn read_table_payload_row_count( + store: &S, + pointer: OverflowPointer, +) -> Result { + if pointer.head_page_id == 0 || pointer.logical_len == 0 { + return Ok(0); + } + if pointer.is_compressed() { + let payload = read_overflow(store, pointer)?; + let mut cursor = Cursor::new(&payload); + let magic = cursor.read_slice(TABLE_PAYLOAD_MAGIC.len())?; + if magic != TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + return Ok(cursor.read_u32()? as usize); + } + + let mut cursor = OverflowPayloadCursor::new(store, pointer); + let mut magic = [0_u8; TABLE_PAYLOAD_MAGIC.len()]; + cursor.read_exact(&mut magic)?; + if magic != *TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); } + Ok(cursor.read_u32()? as usize) } -#[allow(clippy::too_many_arguments)] -fn simple_join_projection_plan( - items: &[SelectItem], - eval_dataset: &Dataset, - left_table_name: &str, - left_alias: &Option, - left_schema: &TableSchema, - right_table_name: &str, - right_alias: &Option, - right_schema: &TableSchema, - using_join_columns: &[String], -) -> Option<(Vec, Vec)> { - let left_binding = left_alias.as_deref().unwrap_or(left_table_name); - let right_binding = right_alias.as_deref().unwrap_or(right_table_name); - let mut projection_plan = Vec::new(); - let mut column_names = Vec::new(); +pub(crate) fn read_persisted_table_row_count( + store: &S, + state: PersistedTableState, +) -> Result { + if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { + return Ok(0); + } + if !state.pointer.is_table_paged_manifest() { + let payload = read_overflow(store, state.pointer)?; + return read_table_payload_live_row_count_from_bytes(&payload); + } - for (index, item) in items.iter().enumerate() { - match item { - SelectItem::Wildcard => { - if !using_join_columns.is_empty() { - for using_column in using_join_columns { - projection_plan.push(SimpleJoinProjectionSource::Expr( - merged_single_column_using_expr( - left_binding, - right_binding, - using_column, - ), - )); - column_names.push(using_column.to_string()); - } - projection_plan.extend( - left_schema - .columns - .iter() - .enumerate() - .filter(|(_, column)| { - !using_join_columns.iter().any(|using_column| { - identifiers_equal(&column.name, using_column) - }) - }) - .map(|(column_index, _)| { - SimpleJoinProjectionSource::Left(column_index) - }), - ); - column_names.extend( - left_schema - .columns - .iter() - .filter(|column| { - !using_join_columns.iter().any(|using_column| { - identifiers_equal(&column.name, using_column) - }) - }) - .map(|column| column.name.clone()), - ); - projection_plan.extend( - right_schema - .columns - .iter() - .enumerate() - .filter(|(_, column)| { - !using_join_columns.iter().any(|using_column| { - identifiers_equal(&column.name, using_column) - }) - }) - .map(|(column_index, _)| { - SimpleJoinProjectionSource::Right(column_index) - }), - ); - column_names.extend( - right_schema - .columns - .iter() - .filter(|column| { - !using_join_columns.iter().any(|using_column| { - identifiers_equal(&column.name, using_column) - }) - }) - .map(|column| column.name.clone()), - ); - } else { - projection_plan.extend( - left_schema - .columns - .iter() - .enumerate() - .map(|(column_index, _)| { - SimpleJoinProjectionSource::Left(column_index) - }), - ); - column_names - .extend(left_schema.columns.iter().map(|column| column.name.clone())); - projection_plan.extend( - right_schema - .columns - .iter() - .enumerate() - .map(|(column_index, _)| { - SimpleJoinProjectionSource::Right(column_index) - }), - ); - column_names.extend( - right_schema - .columns - .iter() - .map(|column| column.name.clone()), - ); - } - } - SelectItem::QualifiedWildcard(table_name) => { - if identifiers_equal(table_name, left_table_name) - || identifiers_equal(table_name, left_binding) - { - projection_plan.extend( - left_schema - .columns - .iter() - .enumerate() - .map(|(column_index, _)| { - SimpleJoinProjectionSource::Left(column_index) - }), - ); - column_names - .extend(left_schema.columns.iter().map(|column| column.name.clone())); - } else if identifiers_equal(table_name, right_table_name) - || identifiers_equal(table_name, right_binding) - { - projection_plan.extend( - right_schema - .columns - .iter() - .enumerate() - .map(|(column_index, _)| { - SimpleJoinProjectionSource::Right(column_index) - }), - ); - column_names.extend( - right_schema - .columns - .iter() - .map(|column| column.name.clone()), - ); - } else { - return None; - } - } - SelectItem::Expr { expr, alias } => { - let source = if let Expr::Column { table, column } = expr { - let left_index = left_schema - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, column)); - let right_index = right_schema - .columns - .iter() - .position(|candidate| identifiers_equal(&candidate.name, column)); - match table.as_deref() { - Some(table_name) - if identifiers_equal(table_name, left_table_name) - || identifiers_equal(table_name, left_binding) => - { - Some(SimpleJoinProjectionSource::Left(left_index?)) - } - Some(table_name) - if identifiers_equal(table_name, right_table_name) - || identifiers_equal(table_name, right_binding) => - { - Some(SimpleJoinProjectionSource::Right(right_index?)) - } - Some(_) => None, - None => match (left_index, right_index) { - (Some(left_index), None) => { - Some(SimpleJoinProjectionSource::Left(left_index)) - } - (None, Some(right_index)) => { - Some(SimpleJoinProjectionSource::Right(right_index)) - } - (Some(_), Some(_)) - if using_join_columns.iter().any(|using_column| { - identifiers_equal(column, using_column) - }) => - { - Some(SimpleJoinProjectionSource::Expr( - merged_single_column_using_expr( - left_binding, - right_binding, - column, - ), - )) - } - _ => None, - }, - } - } else if expr_resolves_against_dataset(expr, eval_dataset) { - Some(SimpleJoinProjectionSource::Expr(expr.clone())) - } else { - None - }?; - projection_plan.push(source); - column_names.push( - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - ); - } + let manifest_payload = read_overflow(store, state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); + } + let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + let mut row_count = 0usize; + for chunk in manifest.chunks { + if chunk.tombstoned_row_ids.is_empty() && chunk.overlay_pointer.is_none() { + row_count = row_count.saturating_add(chunk.row_count); + continue; } + let base_count = read_table_payload_row_count(store, chunk.pointer)?; + let overlay_count = match chunk.overlay_pointer { + Some(pointer) => read_table_payload_row_count(store, pointer)?, + None => 0, + }; + row_count = row_count.saturating_add( + base_count + .saturating_sub(chunk.tombstoned_row_ids.len()) + .saturating_add(overlay_count), + ); } - - Some((projection_plan, column_names)) + Ok(row_count) } -fn merged_single_column_using_expr(left_binding: &str, right_binding: &str, column: &str) -> Expr { - Expr::Function { - name: "coalesce".to_string(), - args: vec![ - Expr::Column { - table: Some(left_binding.to_string()), - column: column.to_string(), - }, - Expr::Column { - table: Some(right_binding.to_string()), - column: column.to_string(), - }, - ], +fn read_deferred_row_by_id_from_table_payload( + store: &S, + state: PersistedTableState, + row_id: i64, +) -> Result> { + let pointer = state.pointer; + if pointer.head_page_id == 0 { + return Ok(None); + } + if pointer.is_compressed() { + let entry = read_deferred_compressed_table_lookup_entry(store, state)?; + let Some(locator) = entry.row_locators.get(&row_id).copied() else { + return Ok(None); + }; + return decode_row_by_locator_from_payload(entry.payload.as_slice(), row_id, locator) + .map(Some); + } + let mut cursor = OverflowPayloadCursor::new(store, pointer); + let mut magic = [0_u8; TABLE_PAYLOAD_MAGIC.len()]; + cursor.read_exact(&mut magic)?; + if magic != *TABLE_PAYLOAD_MAGIC { + return Err(DbError::corruption("table payload magic is invalid")); + } + + let row_count = cursor.read_u32()? as usize; + for _ in 0..row_count { + let candidate_row_id = cursor.read_i64()?; + let (is_tombstone, row_bytes_len) = split_table_payload_row_len(cursor.read_u32()?); + if candidate_row_id == row_id && !is_tombstone { + let row_bytes = cursor.read_vec(row_bytes_len)?; + let row = Row::decode(&row_bytes)?; + return Ok(Some(StoredRow { + row_id: candidate_row_id, + values: row.into_values(), + })); + } + cursor.skip(row_bytes_len)?; } + Ok(None) } -#[allow(clippy::too_many_arguments)] -fn simple_join_projection_order_by_plan( - query: &Query, - projection_items: &[SelectItem], - projection_plan: &[SimpleJoinProjectionSource], - column_names: &[String], - left_table_name: &str, - left_alias: &Option, - left_schema: &TableSchema, - right_table_name: &str, - right_alias: &Option, - right_schema: &TableSchema, -) -> Result>> { - if query.order_by.is_empty() { - return Ok(None); +fn matches_table_binding(table: TableBindingRef<'_>, qualifier: Option<&str>) -> bool { + qualifier.is_some_and(|qualifier| identifiers_equal(qualifier, table.binding_name())) +} + +fn matches_filter_binding( + table_name: &str, + alias: &Option, + qualifier: Option<&str>, +) -> bool { + match qualifier { + Some(qualifier) => identifiers_equal(qualifier, alias.as_deref().unwrap_or(table_name)), + None => true, } - let left_binding = TableBindingRef { - name: left_table_name, - alias: left_alias, - }; - let right_binding = TableBindingRef { - name: right_table_name, - alias: right_alias, - }; - let direct_order = query - .order_by - .iter() - .map(|entry| { - let Expr::Column { - table: order_table, - column: order_column, - } = &entry.expr - else { - return None; - }; +} - let mut projection_index = None; - for (index, source) in projection_plan.iter().enumerate() { - let source_matches = match source { - SimpleJoinProjectionSource::Left(column_index) => { - identifiers_equal(&left_schema.columns[*column_index].name, order_column) - && order_table.as_deref().is_none_or(|qualifier| { - matches_table_binding(left_binding, Some(qualifier)) - }) - } - SimpleJoinProjectionSource::Right(column_index) => { - identifiers_equal(&right_schema.columns[*column_index].name, order_column) - && order_table.as_deref().is_none_or(|qualifier| { - matches_table_binding(right_binding, Some(qualifier)) - }) - } - SimpleJoinProjectionSource::Expr(_) => false, - }; - let alias_matches = order_table.is_none() - && column_names - .get(index) - .is_some_and(|candidate| candidate.eq_ignore_ascii_case(order_column)); - if !source_matches && !alias_matches { - continue; - } - if projection_index.replace(index).is_some() { - return None; - } +fn dataset_column_index(dataset: &Dataset, qualifier: Option<&str>, column: &str) -> Option { + let matches = dataset + .columns + .iter() + .enumerate() + .filter(|(_, binding)| { + if !identifiers_equal(&binding.name, column) { + return false; + } + if let Some(qualifier) = qualifier { + binding + .table + .as_deref() + .is_some_and(|table| identifiers_equal(table, qualifier)) + } else { + !binding.hidden } - - projection_index.map(|projection_index| SimpleOrderByPlan { - projection_index, - descending: entry.descending, - collation: entry.collation.clone(), - }) }) - .collect::>>(); - if direct_order.is_some() { - return Ok(direct_order); + .map(|(index, _)| index) + .collect::>(); + match matches.as_slice() { + [index] => Some(*index), + _ => None, } - - Ok(projection_order_by_plan(&query.order_by, projection_items)) } -fn simple_join_eval_row( - left_values: Option<&[Value]>, - left_width: usize, - right_values: Option<&[Value]>, - right_width: usize, -) -> Vec { - let mut values = Vec::with_capacity(left_width + right_width); - match left_values { - Some(left_values) => values.extend(left_values.iter().cloned()), - None => values.extend((0..left_width).map(|_| Value::Null)), +fn projected_dataset_order_column_index(dataset: &Dataset, expr: &Expr) -> Option { + let Expr::Column { table, column } = expr else { + return None; + }; + if let Some(index) = dataset_column_index(dataset, table.as_deref(), column) { + return Some(index); } - match right_values { - Some(right_values) => values.extend(right_values.iter().cloned()), - None => values.extend((0..right_width).map(|_| Value::Null)), + if table.is_none() { + return None; } - values -} - -type SimpleJoinHashRows = BTreeMap, Vec<(i64, Vec)>>; - -fn simple_join_key_from_indexes(row: &[Value], indexes: &[usize]) -> Result>> { - let join_values = indexes - .iter() - .map(|index| { - row.get(*index) - .ok_or_else(|| DbError::internal("join row is shorter than table schema")) - }) - .collect::>>()?; - if join_values + let matches = dataset + .columns .iter() - .any(|join_value| matches!(join_value, Value::Null)) - { - return Ok(None); + .enumerate() + .filter(|(_, binding)| !binding.hidden && identifiers_equal(&binding.name, column)) + .map(|(index, _)| index) + .collect::>(); + match matches.as_slice() { + [index] => Some(*index), + _ => None, } - Row::new(join_values.iter().cloned().cloned().collect()) - .encode() - .map(Some) } -#[allow(clippy::too_many_arguments)] -fn simple_join_filter_matches( - runtime: &EngineRuntime, - filter: Option<&Expr>, - eval_dataset: &Dataset, - left_values: Option<&[Value]>, - left_width: usize, - right_values: Option<&[Value]>, - right_width: usize, - params: &[Value], -) -> Result { - let Some(filter) = filter else { - return Ok(true); - }; - let joined_values = simple_join_eval_row(left_values, left_width, right_values, right_width); - Ok(matches!( - runtime.eval_expr( - filter, - eval_dataset, - &joined_values, - params, - &BTreeMap::new(), - None, - )?, - Value::Bool(true) - )) +#[derive(Debug)] +enum MembershipValue { + Scalar(Value), + Row(Vec), } -#[allow(clippy::too_many_arguments)] -fn project_simple_join_row( - runtime: &EngineRuntime, - projection_plan: &[SimpleJoinProjectionSource], - eval_dataset: &Dataset, - left_values: Option<&[Value]>, - left_width: usize, - right_values: Option<&[Value]>, - right_width: usize, - params: &[Value], -) -> Result> { - let mut projected = Vec::with_capacity(projection_plan.len()); - let mut joined_values = None::>; - for slot in projection_plan { - match slot { - SimpleJoinProjectionSource::Left(index) => { - let value = left_values - .and_then(|row| row.get(*index)) - .cloned() - .unwrap_or(Value::Null); - projected.push(value); - } - SimpleJoinProjectionSource::Right(index) => { - let value = right_values - .and_then(|row| row.get(*index)) - .cloned() - .unwrap_or(Value::Null); - projected.push(value); - } - SimpleJoinProjectionSource::Expr(expr) => { - let joined_values = joined_values.get_or_insert_with(|| { - simple_join_eval_row(left_values, left_width, right_values, right_width) - }); - projected.push(runtime.eval_expr( - expr, - eval_dataset, - joined_values, - params, - &BTreeMap::new(), - None, - )?); - } - } +fn membership_value_has_nulls(value: &MembershipValue) -> bool { + match value { + MembershipValue::Scalar(value) => matches!(value, Value::Null), + MembershipValue::Row(values) => values.iter().any(|value| matches!(value, Value::Null)), } - Ok(projected) } -fn try_project_simple_select_items( - dataset: &Dataset, - items: &[SelectItem], -) -> Result> { - let mut output_columns = Vec::new(); - let mut projection_plan = Vec::::new(); - for (index, item) in items.iter().enumerate() { - match item { - SelectItem::Expr { expr, alias } => { - let Expr::Column { table, column } = expr else { - return Ok(None); - }; - let Some(source_index) = - simple_select_item_column_index(dataset, table.as_deref(), column) - else { - return Ok(None); - }; - projection_plan.push(source_index); - output_columns.push(ColumnBinding::visible( - None, - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - )); +fn compare_membership_values( + left: &MembershipValue, + right: &MembershipValue, +) -> Result> { + match (left, right) { + (MembershipValue::Scalar(left), MembershipValue::Scalar(right)) => { + if matches!(left, Value::Null) || matches!(right, Value::Null) { + Ok(None) + } else { + Ok(Some( + compare_values(left, right)? == std::cmp::Ordering::Equal, + )) } - SelectItem::Wildcard => { - for (source_index, binding) in dataset.columns.iter().enumerate() { - if binding.hidden { - continue; - } - projection_plan.push(source_index); - output_columns.push(binding.as_output()); - } + } + (MembershipValue::Row(left), MembershipValue::Row(right)) => { + if left.len() != right.len() { + return Err(DbError::sql(format!( + "row-value comparison expected {} columns but got {}", + left.len(), + right.len() + ))); } - SelectItem::QualifiedWildcard(table) => { - let mut matched = false; - for (source_index, binding) in dataset.columns.iter().enumerate() { - if binding.hidden || binding.table.as_deref() != Some(table.as_str()) { - continue; - } - projection_plan.push(source_index); - output_columns.push(binding.as_output()); - matched = true; + let mut saw_null = false; + for (left_value, right_value) in left.iter().zip(right) { + if matches!(left_value, Value::Null) || matches!(right_value, Value::Null) { + saw_null = true; + continue; } - if !matched { - return Ok(None); + if compare_values(left_value, right_value)? != std::cmp::Ordering::Equal { + return Ok(Some(false)); } } - } - } - - let mut output_rows = Vec::with_capacity(dataset.rows.len()); - for row in dataset.rows.iter() { - let mut output_row = Vec::with_capacity(projection_plan.len()); - for source_index in &projection_plan { - let value = row - .get(*source_index) - .ok_or_else(|| DbError::internal("projection source index exceeds row width"))?; - output_row.push(value.clone()); - } - output_rows.push(output_row); - } - Ok(Some(Dataset::with_rows(output_columns, output_rows))) -} - -#[derive(Clone, Copy, Debug)] -struct SimpleTrigramLookup<'a> { - table_qualifier: Option<&'a str>, - column_name: &'a str, - pattern_expr: &'a Expr, - has_additional_filter: bool, -} - -fn simple_trigram_lookup(filter: &Expr) -> Option> { - match filter { - Expr::Like { - expr, - pattern, - escape, - negated, - .. - } if !negated && escape.is_none() => match (&**expr, &**pattern) { - (Expr::Column { table, column }, pattern @ (Expr::Literal(_) | Expr::Parameter(_))) => { - Some(SimpleTrigramLookup { - table_qualifier: table.as_deref(), - column_name: column.as_str(), - pattern_expr: pattern, - has_additional_filter: false, - }) - } - _ => None, - }, - Expr::Binary { - left, - op: BinaryOp::And, - right, - } => { - if let Some(mut lookup) = simple_trigram_lookup(left) { - lookup.has_additional_filter = true; - Some(lookup) + if saw_null { + Ok(None) } else { - simple_trigram_lookup(right).map(|mut lookup| { - lookup.has_additional_filter = true; - lookup - }) + Ok(Some(true)) } } - _ => None, + (MembershipValue::Scalar(_), MembershipValue::Row(right)) + | (MembershipValue::Row(right), MembershipValue::Scalar(_)) => Err(DbError::sql(format!( + "row-value comparison expected {} columns but got 1", + right.len() + ))), } } -#[derive(Clone, Copy, Debug)] -struct SimpleFullTextLookup<'a> { - index_name_expr: &'a Expr, - query_expr: &'a Expr, +fn schema_column_index(schema: &TableSchema, column: &str) -> Option { + schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, column)) } -fn exact_fulltext_lookup(filter: Option<&Expr>) -> Option> { - let Some(Expr::Function { name, args }) = filter else { - return None; - }; - if !name.eq_ignore_ascii_case("fulltext_match") || args.len() != 2 { - return None; +fn append_showdown_review_ranking_group( + group: &mut Vec, + rows: &mut Vec, +) { + let mut buckets: [Vec; 11] = std::array::from_fn(|_| Vec::new()); + for item in group.drain(..) { + buckets[item.score as usize].push(item); } - Some(SimpleFullTextLookup { - index_name_expr: &args[0], - query_expr: &args[1], - }) -} - -fn simple_fulltext_lookup(filter: &Expr) -> Option> { - match filter { - Expr::Function { name, args } - if name.eq_ignore_ascii_case("fulltext_match") && args.len() == 2 => - { - Some(SimpleFullTextLookup { - index_name_expr: &args[0], - query_expr: &args[1], - }) + let mut current_rank = 1_i64; + let mut current_dense_rank = 1_i64; + let mut ordinal = 0_usize; + let mut seen_score = false; + for score in (1..buckets.len()).rev() { + let bucket = &mut buckets[score]; + if bucket.is_empty() { + continue; + } + if seen_score { + current_rank = (ordinal + 1) as i64; + current_dense_rank += 1; + } else { + seen_score = true; + } + for item in bucket.drain(..) { + rows.push(QueryRow::new(vec![ + Value::Int64(item.movie_id), + Value::Int64(item.score), + item.author, + Value::Int64(current_rank), + Value::Int64(current_dense_rank), + ])); + ordinal += 1; } - Expr::Binary { - left, - op: BinaryOp::And, - right, - } => simple_fulltext_lookup(left).or_else(|| simple_fulltext_lookup(right)), - _ => None, } } -#[derive(Clone, Copy, Debug)] -struct SimpleSpatialLookup<'a> { - table_qualifier: Option<&'a str>, - column_name: &'a str, - value_expr: &'a Expr, - radius_expr: Option<&'a Expr>, -} - -#[derive(Clone, Copy, Debug)] -struct SimpleSpatialJoinPredicate<'a> { - left: QualifiedColumnRef<'a>, - right: QualifiedColumnRef<'a>, - radius_expr: Option<&'a Expr>, -} - -fn simple_spatial_join_predicate<'a>( - expr: &'a Expr, - left_binding: TableBindingRef<'a>, - right_binding: TableBindingRef<'a>, -) -> Option> { - let Expr::Function { name, args } = expr else { - return None; - }; - let lower = name.to_ascii_lowercase(); - let (left, right, radius_expr) = if lower == "st_dwithin" { - let [left, right, radius] = args.as_slice() else { - return None; - }; - if expr_has_column_ref(radius) { - return None; +fn table_has_single_column_foreign_key( + child_schema: &TableSchema, + child_column: &str, + parent_schema: &TableSchema, + parent_column: &str, +) -> bool { + child_schema.foreign_keys.iter().any(|foreign_key| { + if foreign_key.columns.len() != 1 + || !identifiers_equal(&foreign_key.columns[0], child_column) + || !identifiers_equal(&foreign_key.referenced_table, &parent_schema.name) + { + return false; } - (left, right, Some(radius)) - } else if matches!( - lower.as_str(), - "st_intersects" | "st_contains" | "st_within" | "st_equals" - ) { - let [left, right] = args.as_slice() else { - return None; + let referenced_columns = if foreign_key.referenced_columns.is_empty() { + parent_schema.primary_key_columns.as_slice() + } else { + foreign_key.referenced_columns.as_slice() }; - (left, right, None) - } else { - return None; - }; - let left_ref = qualified_column_ref_expr(left)?; - let right_ref = qualified_column_ref_expr(right)?; - let left_is_left = matches_table_binding(left_binding, left_ref.table); - let left_is_right = matches_table_binding(right_binding, left_ref.table); - let right_is_left = matches_table_binding(left_binding, right_ref.table); - let right_is_right = matches_table_binding(right_binding, right_ref.table); - if (left_is_left && right_is_right) || (left_is_right && right_is_left) { - Some(SimpleSpatialJoinPredicate { - left: left_ref, - right: right_ref, - radius_expr, - }) - } else { - None - } -} - -fn qualified_column_ref_expr(expr: &Expr) -> Option> { - let Expr::Column { table, column } = expr else { - return None; - }; - Some(QualifiedColumnRef { - table: table.as_deref(), - column, + referenced_columns.len() == 1 && identifiers_equal(&referenced_columns[0], parent_column) }) } -fn simple_spatial_lookup(filter: &Expr) -> Option> { - match filter { - Expr::Function { name, args } if name.eq_ignore_ascii_case("st_dwithin") => { - let [left, right, radius] = args.as_slice() else { - return None; - }; - simple_spatial_column_value_pair(left, right).map( - |(table_qualifier, column_name, value_expr)| SimpleSpatialLookup { - table_qualifier, - column_name, - value_expr, - radius_expr: Some(radius), - }, - ) - } - Expr::Function { name, args } - if matches!( - name.to_ascii_lowercase().as_str(), - "st_intersects" | "st_contains" | "st_within" | "st_equals" - ) => - { - let [left, right] = args.as_slice() else { - return None; - }; - simple_spatial_column_value_pair(left, right).map( - |(table_qualifier, column_name, value_expr)| SimpleSpatialLookup { - table_qualifier, - column_name, - value_expr, - radius_expr: None, - }, - ) - } - Expr::Binary { - left, - op: BinaryOp::And, - right, - } => simple_spatial_lookup(left).or_else(|| simple_spatial_lookup(right)), +fn value_as_int64(value: &Value) -> Option { + match value { + Value::Int64(value) => Some(*value), _ => None, } } -fn simple_spatial_column_value_pair<'a>( - left: &'a Expr, - right: &'a Expr, -) -> Option<(Option<&'a str>, &'a str, &'a Expr)> { - match (left, right) { - (Expr::Column { table, column }, value) if !expr_has_column_ref(value) => { - Some((table.as_deref(), column.as_str(), value)) - } - (value, Expr::Column { table, column }) if !expr_has_column_ref(value) => { - Some((table.as_deref(), column.as_str(), value)) - } +fn value_as_f64(value: &Value) -> Option { + match value { + Value::Int64(value) => Some(*value as f64), + Value::Float64(value) => Some(*value), _ => None, } } -fn expr_has_column_ref(expr: &Expr) -> bool { - match expr { - Expr::Column { .. } => true, - Expr::Unary { expr, .. } - | Expr::Cast { expr, .. } - | Expr::IsNull { expr, .. } - | Expr::Collate { expr, .. } => expr_has_column_ref(expr), - Expr::Binary { left, right, .. } => expr_has_column_ref(left) || expr_has_column_ref(right), - Expr::Between { - expr, low, high, .. - } => expr_has_column_ref(expr) || expr_has_column_ref(low) || expr_has_column_ref(high), - Expr::InList { expr, items, .. } => { - expr_has_column_ref(expr) || items.iter().any(expr_has_column_ref) - } - Expr::Like { - expr, - pattern, - escape, - .. - } => { - expr_has_column_ref(expr) - || expr_has_column_ref(pattern) - || escape.as_deref().is_some_and(expr_has_column_ref) - } - Expr::Function { args, .. } => args.iter().any(expr_has_column_ref), - Expr::Aggregate { args, order_by, .. } => { - args.iter().any(expr_has_column_ref) - || order_by - .iter() - .any(|order| expr_has_column_ref(&order.expr)) - } - Expr::Case { - operand, - branches, - else_expr, - } => { - operand.as_deref().is_some_and(expr_has_column_ref) - || branches.iter().any(|(condition, value)| { - expr_has_column_ref(condition) || expr_has_column_ref(value) - }) - || else_expr.as_deref().is_some_and(expr_has_column_ref) - } - Expr::Row(items) => items.iter().any(expr_has_column_ref), - Expr::InSubquery { expr, .. } | Expr::CompareSubquery { expr, .. } => { - expr_has_column_ref(expr) - } - Expr::Literal(_) - | Expr::Parameter(_) - | Expr::RowNumber { .. } - | Expr::WindowFunction { .. } - | Expr::ScalarSubquery(_) - | Expr::Exists(_) => false, +fn value_as_text(value: &Value) -> Option<&str> { + match value { + Value::Text(value) => Some(value.as_str()), + _ => None, } } -fn expr_contains_aggregate(expr: &Expr) -> bool { - match expr { - Expr::Aggregate { .. } => true, - Expr::Unary { expr, .. } | Expr::Collate { expr, .. } => expr_contains_aggregate(expr), - Expr::Binary { left, right, .. } => { - expr_contains_aggregate(left) || expr_contains_aggregate(right) - } - Expr::Between { - expr, low, high, .. - } => { - expr_contains_aggregate(expr) - || expr_contains_aggregate(low) - || expr_contains_aggregate(high) - } - Expr::InList { expr, items, .. } => { - expr_contains_aggregate(expr) || items.iter().any(expr_contains_aggregate) - } - Expr::InSubquery { expr, .. } => expr_contains_aggregate(expr), - Expr::CompareSubquery { expr, .. } => expr_contains_aggregate(expr), - Expr::ScalarSubquery(_) | Expr::Exists(_) => false, - Expr::Like { - expr, - pattern, - escape, - .. - } => { - expr_contains_aggregate(expr) - || expr_contains_aggregate(pattern) - || escape.as_deref().is_some_and(expr_contains_aggregate) - } - Expr::IsNull { expr, .. } => expr_contains_aggregate(expr), - Expr::Function { args, .. } => args.iter().any(expr_contains_aggregate), - Expr::RowNumber { .. } | Expr::WindowFunction { .. } => false, - Expr::Case { - operand, - branches, - else_expr, - } => { - operand.as_deref().is_some_and(expr_contains_aggregate) - || branches.iter().any(|(left, right)| { - expr_contains_aggregate(left) || expr_contains_aggregate(right) - }) - || else_expr.as_deref().is_some_and(expr_contains_aggregate) - } - Expr::Row(items) => items.iter().any(expr_contains_aggregate), - Expr::Cast { expr, .. } => expr_contains_aggregate(expr), - Expr::Literal(_) | Expr::Column { .. } | Expr::Parameter(_) => false, - } +fn expr_matches_binding_column(expr: &Expr, binding: TableBindingRef<'_>, column: &str) -> bool { + let Expr::Column { + table, + column: expr_column, + } = expr + else { + return false; + }; + matches_table_binding(binding, table.as_deref()) && identifiers_equal(expr_column, column) } -fn expr_contains_runtime_extension_aggregate(runtime: &EngineRuntime, expr: &Expr) -> Result { - Ok(match expr { - Expr::Aggregate { .. } => true, - Expr::Function { name, args } => { - crate::extensions::runtime_has_aggregate_function(runtime, name)? - || args.iter().try_fold(false, |found, arg| { - if found { - Ok(true) - } else { - expr_contains_runtime_extension_aggregate(runtime, arg) - } - })? - } - Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => { - expr_contains_runtime_extension_aggregate(runtime, expr)? - } - Expr::Binary { left, right, .. } => { - expr_contains_runtime_extension_aggregate(runtime, left)? - || expr_contains_runtime_extension_aggregate(runtime, right)? - } - Expr::Between { - expr, low, high, .. - } => { - expr_contains_runtime_extension_aggregate(runtime, expr)? - || expr_contains_runtime_extension_aggregate(runtime, low)? - || expr_contains_runtime_extension_aggregate(runtime, high)? - } - Expr::InList { expr, items, .. } => { - expr_contains_runtime_extension_aggregate(runtime, expr)? - || items.iter().try_fold(false, |found, item| { - if found { - Ok(true) - } else { - expr_contains_runtime_extension_aggregate(runtime, item) - } - })? +#[allow(clippy::too_many_arguments)] +fn accumulate_genre_popularity_movie( + movie_source: &VisibleTableRowSource<'_>, + movie_index_keys: Option<&RuntimeBtreeKeys>, + movie_id_is_rowid_alias: bool, + movie_id_value: Option<&Value>, + movie_rating_index: usize, + movie_count: &mut i64, + rating_sum: &mut f64, + rating_count: &mut i64, +) -> Result<()> { + let Some(movie_id_value) = movie_id_value else { + return Ok(()); + }; + if matches!(movie_id_value, Value::Null) { + return Ok(()); + } + + if movie_id_is_rowid_alias { + if let Some(row_id) = value_as_int64(movie_id_value) { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + accumulate_genre_popularity_rating( + movie_row.values(), + movie_rating_index, + movie_count, + rating_sum, + rating_count, + ); + return Ok(()); + } } - Expr::InSubquery { expr, .. } | Expr::CompareSubquery { expr, .. } => { - expr_contains_runtime_extension_aggregate(runtime, expr)? + } + + let Some(keys) = movie_index_keys else { + return Ok(()); + }; + match keys.row_ids_for_value_set(movie_id_value)? { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(row_id) => { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + accumulate_genre_popularity_rating( + movie_row.values(), + movie_rating_index, + movie_count, + rating_sum, + rating_count, + ); + } } - Expr::Like { - expr, - pattern, - escape, - .. - } => { - expr_contains_runtime_extension_aggregate(runtime, expr)? - || expr_contains_runtime_extension_aggregate(runtime, pattern)? - || escape - .as_deref() - .map(|expr| expr_contains_runtime_extension_aggregate(runtime, expr)) - .transpose()? - .unwrap_or(false) + RuntimeRowIdSet::Contiguous { start, len } => { + for row_id in contiguous_row_ids(start, len) { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + accumulate_genre_popularity_rating( + movie_row.values(), + movie_rating_index, + movie_count, + rating_sum, + rating_count, + ); + } + } } - Expr::IsNull { expr, .. } => expr_contains_runtime_extension_aggregate(runtime, expr)?, - Expr::Case { - operand, - branches, - else_expr, - } => { - operand - .as_deref() - .map(|expr| expr_contains_runtime_extension_aggregate(runtime, expr)) - .transpose()? - .unwrap_or(false) - || branches.iter().try_fold(false, |found, (left, right)| { - if found { - Ok(true) - } else { - Ok(expr_contains_runtime_extension_aggregate(runtime, left)? - || expr_contains_runtime_extension_aggregate(runtime, right)?) - } - })? - || else_expr - .as_deref() - .map(|expr| expr_contains_runtime_extension_aggregate(runtime, expr)) - .transpose()? - .unwrap_or(false) + RuntimeRowIdSet::Many(row_ids) => { + for row_id in row_ids { + if let Some(movie_row) = movie_source.row_by_id(*row_id)? { + accumulate_genre_popularity_rating( + movie_row.values(), + movie_rating_index, + movie_count, + rating_sum, + rating_count, + ); + } + } } - Expr::Row(items) => items.iter().try_fold(false, |found, item| { - if found { - Ok(true) - } else { - expr_contains_runtime_extension_aggregate(runtime, item) + RuntimeRowIdSet::Owned(row_ids) => { + for row_id in row_ids { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + accumulate_genre_popularity_rating( + movie_row.values(), + movie_rating_index, + movie_count, + rating_sum, + rating_count, + ); + } } - })?, - Expr::ScalarSubquery(_) | Expr::Exists(_) => false, - Expr::RowNumber { .. } - | Expr::WindowFunction { .. } - | Expr::Literal(_) - | Expr::Column { .. } - | Expr::Parameter(_) => false, - }) + } + } + Ok(()) } -fn expr_contains_window(expr: &Expr) -> bool { - match expr { - Expr::RowNumber { .. } | Expr::WindowFunction { .. } => true, - Expr::Unary { expr, .. } - | Expr::Cast { expr, .. } - | Expr::IsNull { expr, .. } - | Expr::Collate { expr, .. } => expr_contains_window(expr), - Expr::Binary { left, right, .. } => { - expr_contains_window(left) || expr_contains_window(right) - } - Expr::Between { - expr, low, high, .. - } => expr_contains_window(expr) || expr_contains_window(low) || expr_contains_window(high), - Expr::InList { expr, items, .. } => { - expr_contains_window(expr) || items.iter().any(expr_contains_window) - } - Expr::Like { - expr, - pattern, - escape, - .. - } => { - expr_contains_window(expr) - || expr_contains_window(pattern) - || escape.as_ref().is_some_and(|e| expr_contains_window(e)) - } - Expr::Function { args, .. } => args.iter().any(expr_contains_window), - Expr::Case { - operand, - branches, - else_expr, - } => { - operand.as_ref().is_some_and(|e| expr_contains_window(e)) - || branches - .iter() - .any(|(c, v)| expr_contains_window(c) || expr_contains_window(v)) - || else_expr.as_ref().is_some_and(|e| expr_contains_window(e)) +fn accumulate_genre_popularity_rating( + movie_values: &[Value], + movie_rating_index: usize, + movie_count: &mut i64, + rating_sum: &mut f64, + rating_count: &mut i64, +) { + *movie_count = movie_count.saturating_add(1); + if let Some(value) = movie_values.get(movie_rating_index) { + if let Some(rating) = indexed_join_aggregate_as_f64(value) { + *rating_sum += rating; + *rating_count = rating_count.saturating_add(1); } - Expr::Row(items) => items.iter().any(expr_contains_window), - Expr::Aggregate { args, .. } => args.iter().any(expr_contains_window), - Expr::InSubquery { .. } - | Expr::CompareSubquery { .. } - | Expr::ScalarSubquery(_) - | Expr::Exists(_) => false, - Expr::Literal(_) | Expr::Column { .. } | Expr::Parameter(_) => false, } } -#[derive(Clone, Debug)] -struct JoinUsingColumn { - name: String, - left_index: usize, - right_index: usize, -} +#[allow(clippy::too_many_arguments)] +fn push_movie_tag_search_movie_rows( + runtime: &EngineRuntime, + movie_source: &VisibleTableRowSource<'_>, + movie_index_keys: Option<&RuntimeBtreeKeys>, + movie_id_is_rowid_alias: bool, + movie_id_value: Option<&Value>, + projection_indexes: &[usize], + bounded_order: Option<(&[SimpleOrderByPlan], usize)>, + rows: &mut Vec, +) -> Result<()> { + let Some(movie_id_value) = movie_id_value else { + return Ok(()); + }; + if matches!(movie_id_value, Value::Null) { + return Ok(()); + } -struct JoinEvalContext<'a> { - dataset: &'a Dataset, - runtime: &'a EngineRuntime, - params: &'a [Value], - ctes: &'a BTreeMap, -} + if movie_id_is_rowid_alias { + if let Some(row_id) = value_as_int64(movie_id_value) { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + push_movie_tag_search_projected_row( + runtime, + movie_row.values(), + projection_indexes, + bounded_order, + rows, + )?; + return Ok(()); + } + } + } -fn visible_column_names(dataset: &Dataset) -> Vec { - let mut names = Vec::::new(); - for binding in dataset.columns.iter().filter(|binding| !binding.hidden) { - if !names - .iter() - .any(|name| identifiers_equal(name, &binding.name)) - { - names.push(binding.name.clone()); + let Some(keys) = movie_index_keys else { + return Ok(()); + }; + match keys.row_ids_for_value_set(movie_id_value)? { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(row_id) => { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + push_movie_tag_search_projected_row( + runtime, + movie_row.values(), + projection_indexes, + bounded_order, + rows, + )?; + } + } + RuntimeRowIdSet::Contiguous { start, len } => { + for row_id in contiguous_row_ids(start, len) { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + push_movie_tag_search_projected_row( + runtime, + movie_row.values(), + projection_indexes, + bounded_order, + rows, + )?; + } + } + } + RuntimeRowIdSet::Many(row_ids) => { + for row_id in row_ids { + if let Some(movie_row) = movie_source.row_by_id(*row_id)? { + push_movie_tag_search_projected_row( + runtime, + movie_row.values(), + projection_indexes, + bounded_order, + rows, + )?; + } + } + } + RuntimeRowIdSet::Owned(row_ids) => { + for row_id in row_ids { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + push_movie_tag_search_projected_row( + runtime, + movie_row.values(), + projection_indexes, + bounded_order, + rows, + )?; + } + } } } - names + Ok(()) } -fn visible_column_exists(dataset: &Dataset, column: &str) -> bool { - dataset - .columns - .iter() - .any(|binding| !binding.hidden && identifiers_equal(&binding.name, column)) +fn push_movie_tag_search_projected_row( + runtime: &EngineRuntime, + movie_values: &[Value], + projection_indexes: &[usize], + bounded_order: Option<(&[SimpleOrderByPlan], usize)>, + rows: &mut Vec, +) -> Result<()> { + let row = project_simple_projection_values(movie_values, projection_indexes); + if let Some((order_by, limit)) = bounded_order { + push_bounded_projection_ordered_query_row(Some(runtime), rows, row, order_by, limit) + } else { + rows.push(row); + Ok(()) + } } -fn resolve_visible_join_column( - dataset: &Dataset, - column: &str, - join_form: &str, - side: &str, -) -> Result { - let matches = dataset - .columns - .iter() - .enumerate() - .filter(|(_, binding)| !binding.hidden && identifiers_equal(&binding.name, column)) - .collect::>(); - match matches.as_slice() { - [single] => Ok(single.0), - [] => Err(DbError::sql(format!( - "{join_form} column {column} does not exist in {side} input" - ))), - _ => Err(DbError::sql(format!( - "{join_form} column {column} is ambiguous in {side} input" - ))), +#[allow(clippy::too_many_arguments)] +fn insert_movie_watchlist_group_rows( + movie_source: &VisibleTableRowSource<'_>, + movie_index_keys: Option<&RuntimeBtreeKeys>, + movie_id_is_rowid_alias: bool, + movie_id_value: &Value, + priority: &Value, + review_source: &VisibleTableRowSource<'_>, + review_movie_keys: &RuntimeBtreeKeys, + movie_id_index: usize, + movie_title_index: usize, + review_score_index: usize, + groups: &mut BTreeMap, QueryRow>, +) -> Result<()> { + if movie_id_is_rowid_alias { + if let Some(row_id) = value_as_int64(movie_id_value) { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + insert_movie_watchlist_group_row( + movie_row.values(), + priority, + review_source, + review_movie_keys, + movie_id_index, + movie_title_index, + review_score_index, + groups, + )?; + return Ok(()); + } + } } -} -fn resolve_join_using_columns( - left: &Dataset, - right: &Dataset, - constraint: &JoinConstraint, -) -> Result> { - match constraint { - JoinConstraint::On(_) => Ok(Vec::new()), - JoinConstraint::Using(columns) => { - let mut pairs = Vec::with_capacity(columns.len()); - let mut seen = Vec::::new(); - for column in columns { - if seen - .iter() - .any(|existing| identifiers_equal(existing, column)) - { - return Err(DbError::sql(format!( - "JOIN USING column {column} specified more than once" - ))); - } - let left_index = resolve_visible_join_column(left, column, "JOIN USING", "left")?; - let right_index = - resolve_visible_join_column(right, column, "JOIN USING", "right")?; - pairs.push(JoinUsingColumn { - name: left.columns[left_index].name.clone(), - left_index, - right_index, - }); - seen.push(column.clone()); + let Some(keys) = movie_index_keys else { + return Ok(()); + }; + match keys.row_ids_for_value_set(movie_id_value)? { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(row_id) => { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + insert_movie_watchlist_group_row( + movie_row.values(), + priority, + review_source, + review_movie_keys, + movie_id_index, + movie_title_index, + review_score_index, + groups, + )?; } - Ok(pairs) } - JoinConstraint::Natural => { - let mut pairs = Vec::new(); - for column in visible_column_names(left) { - if !visible_column_exists(right, &column) { - continue; + RuntimeRowIdSet::Contiguous { start, len } => { + for row_id in contiguous_row_ids(start, len) { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + insert_movie_watchlist_group_row( + movie_row.values(), + priority, + review_source, + review_movie_keys, + movie_id_index, + movie_title_index, + review_score_index, + groups, + )?; } - let left_index = - resolve_visible_join_column(left, &column, "NATURAL JOIN", "left")?; - let right_index = - resolve_visible_join_column(right, &column, "NATURAL JOIN", "right")?; - pairs.push(JoinUsingColumn { - name: left.columns[left_index].name.clone(), - left_index, - right_index, - }); } - Ok(pairs) } - } -} - -fn resolve_join_using_columns_for_schemas( - left_columns: &[ColumnBinding], - right_columns: &[ColumnBinding], - constraint: &JoinConstraint, - _left_table: &TableSchema, - _right_table: &TableSchema, -) -> Result> { - match constraint { - JoinConstraint::Using(names) => { - let mut result = Vec::new(); - for name in names { - let left_idx = left_columns - .iter() - .position(|c| identifiers_equal(&c.name, name)) - .ok_or_else(|| { - DbError::sql(format!("column \"{name}\" not found in left table")) - })?; - let right_idx = right_columns - .iter() - .position(|c| identifiers_equal(&c.name, name)) - .ok_or_else(|| { - DbError::sql(format!("column \"{name}\" not found in right table")) - })?; - result.push(JoinUsingColumn { - name: left_columns[left_idx].name.clone(), - left_index: left_idx, - right_index: right_idx, - }); + RuntimeRowIdSet::Many(row_ids) => { + for row_id in row_ids { + if let Some(movie_row) = movie_source.row_by_id(*row_id)? { + insert_movie_watchlist_group_row( + movie_row.values(), + priority, + review_source, + review_movie_keys, + movie_id_index, + movie_title_index, + review_score_index, + groups, + )?; + } } - Ok(result) } - JoinConstraint::Natural => { - let mut result = Vec::new(); - for lc in left_columns.iter() { - if let Some(right_idx) = right_columns - .iter() - .position(|rc| identifiers_equal(&rc.name, &lc.name)) - { - let left_idx = left_columns - .iter() - .position(|c| identifiers_equal(&c.name, &lc.name)) - .ok_or_else(|| { - DbError::internal( - "internal: NATURAL join column is missing on left table", - ) - })?; - result.push(JoinUsingColumn { - name: left_columns[left_idx].name.clone(), - left_index: left_idx, - right_index: right_idx, - }); + RuntimeRowIdSet::Owned(row_ids) => { + for row_id in row_ids { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + insert_movie_watchlist_group_row( + movie_row.values(), + priority, + review_source, + review_movie_keys, + movie_id_index, + movie_title_index, + review_score_index, + groups, + )?; } } - Ok(result) } - JoinConstraint::On(_) => Ok(Vec::new()), } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn insert_movie_watchlist_group_row( + movie_values: &[Value], + priority: &Value, + review_source: &VisibleTableRowSource<'_>, + review_movie_keys: &RuntimeBtreeKeys, + movie_id_index: usize, + movie_title_index: usize, + review_score_index: usize, + groups: &mut BTreeMap, QueryRow>, +) -> Result<()> { + let Some(movie_id) = movie_values.get(movie_id_index) else { + return Err(DbError::internal( + "movie watchlist id column missing from movie row", + )); + }; + let group_key = row_identity(std::slice::from_ref(movie_id))?; + if groups.contains_key(&group_key) { + return Ok(()); + } + let Some(title) = movie_values.get(movie_title_index) else { + return Err(DbError::internal( + "movie watchlist title column missing from movie row", + )); + }; + let avg = movie_watchlist_review_avg( + review_source, + review_movie_keys, + movie_id, + review_score_index, + )?; + groups.insert( + group_key, + QueryRow::new(vec![movie_id.clone(), title.clone(), priority.clone(), avg]), + ); + Ok(()) } -fn join_output_columns( - left: &Dataset, - right: &Dataset, - using_columns: &[JoinUsingColumn], -) -> Vec { - if using_columns.is_empty() { - let mut columns = left.columns.clone(); - columns.extend(right.columns.clone()); - return columns; +fn push_bounded_movie_busiest_people_count( + counts: &mut Vec, + candidate: MovieBusiestPeopleCount, + bounded_count: usize, +) { + if bounded_count == 0 { + return; } - - let left_hidden = using_columns - .iter() - .map(|column| column.left_index) - .collect::>(); - let right_hidden = using_columns - .iter() - .map(|column| column.right_index) - .collect::>(); - - let mut columns = - Vec::with_capacity(using_columns.len() + left.columns.len() + right.columns.len()); - for column in using_columns { - columns.push(ColumnBinding::visible(None, column.name.clone())); + if counts.len() < bounded_count { + counts.push(candidate); + return; } - for (index, binding) in left.columns.iter().enumerate() { - let mut binding = binding.clone(); - if left_hidden.contains(&index) { - binding.hidden = true; + let mut worst_index = 0; + for index in 1..counts.len() { + if compare_movie_busiest_people_counts(&counts[index], &counts[worst_index]) + == std::cmp::Ordering::Greater + { + worst_index = index; } - columns.push(binding); } - for (index, binding) in right.columns.iter().enumerate() { - let mut binding = binding.clone(); - if right_hidden.contains(&index) { - binding.hidden = true; - } - columns.push(binding); + if compare_movie_busiest_people_counts(&candidate, &counts[worst_index]) + == std::cmp::Ordering::Less + { + counts[worst_index] = candidate; } - columns } -fn merged_join_value(left: &Value, right: &Value) -> Value { - if matches!(left, Value::Null) { - right.clone() - } else { - left.clone() - } +fn sort_movie_busiest_people_counts(counts: &mut [MovieBusiestPeopleCount]) { + counts.sort_by(compare_movie_busiest_people_counts); } -fn join_output_row( - left_row: &[Value], - right_row: &[Value], - using_columns: &[JoinUsingColumn], -) -> Result> { - if using_columns.is_empty() { - let mut row = left_row.to_vec(); - row.extend_from_slice(right_row); - return Ok(row); - } +fn compare_movie_busiest_people_counts( + left: &MovieBusiestPeopleCount, + right: &MovieBusiestPeopleCount, +) -> std::cmp::Ordering { + right + .role_count + .cmp(&left.role_count) + .then_with(|| compare_runtime_btree_keys(&left.person_key, &right.person_key)) +} - let mut row = Vec::with_capacity(using_columns.len() + left_row.len() + right_row.len()); - for column in using_columns { - let left_value = left_row - .get(column.left_index) - .ok_or_else(|| DbError::internal("left join row is shorter than its bindings"))?; - let right_value = right_row - .get(column.right_index) - .ok_or_else(|| DbError::internal("right join row is shorter than its bindings"))?; - row.push(merged_join_value(left_value, right_value)); +fn compare_runtime_btree_keys( + left: &RuntimeBtreeKey, + right: &RuntimeBtreeKey, +) -> std::cmp::Ordering { + match (left, right) { + (RuntimeBtreeKey::Encoded(left), RuntimeBtreeKey::Encoded(right)) => left.cmp(right), + (RuntimeBtreeKey::Int64(left), RuntimeBtreeKey::Int64(right)) => left.cmp(right), + (RuntimeBtreeKey::Uuid(left), RuntimeBtreeKey::Uuid(right)) => left.cmp(right), + (RuntimeBtreeKey::Encoded(_), RuntimeBtreeKey::Int64(_)) => std::cmp::Ordering::Less, + (RuntimeBtreeKey::Encoded(_), RuntimeBtreeKey::Uuid(_)) => std::cmp::Ordering::Less, + (RuntimeBtreeKey::Int64(_), RuntimeBtreeKey::Encoded(_)) => std::cmp::Ordering::Greater, + (RuntimeBtreeKey::Int64(_), RuntimeBtreeKey::Uuid(_)) => std::cmp::Ordering::Less, + (RuntimeBtreeKey::Uuid(_), RuntimeBtreeKey::Encoded(_)) => std::cmp::Ordering::Greater, + (RuntimeBtreeKey::Uuid(_), RuntimeBtreeKey::Int64(_)) => std::cmp::Ordering::Greater, } - row.extend_from_slice(left_row); - row.extend_from_slice(right_row); - Ok(row) } -fn join_rows_match( - constraint: &JoinConstraint, - using_columns: &[JoinUsingColumn], - eval_row: &[Value], - left_row: &[Value], - right_row: &[Value], - context: &JoinEvalContext<'_>, -) -> Result { - match constraint { - JoinConstraint::On(on) => Ok(matches!( - context.runtime.eval_expr( - on, - context.dataset, - eval_row, - context.params, - context.ctes, - None - )?, - Value::Bool(true) - )), - JoinConstraint::Using(_) | JoinConstraint::Natural => { - for column in using_columns { - let left_value = left_row.get(column.left_index).ok_or_else(|| { - DbError::internal("left join row is shorter than its bindings") - })?; - let right_value = right_row.get(column.right_index).ok_or_else(|| { - DbError::internal("right join row is shorter than its bindings") - })?; - if matches!(left_value, Value::Null) || matches!(right_value, Value::Null) { - return Ok(false); - } - if compare_values(left_value, right_value)? != std::cmp::Ordering::Equal { - return Ok(false); - } +fn push_movie_busiest_people_row( + people_source: &VisibleTableRowSource<'_>, + people_index_keys: Option<&RuntimeBtreeKeys>, + people_id_is_rowid_alias: bool, + candidate: &MovieBusiestPeopleCount, + projection_indexes: &[usize], + rows: &mut Vec, +) -> Result<()> { + if people_id_is_rowid_alias { + if let RuntimeBtreeKey::Int64(row_id) = &candidate.person_key { + if let Some(people_row) = people_source.row_by_id(*row_id)? { + push_movie_busiest_people_projected_row( + people_row.values(), + candidate.role_count, + projection_indexes, + rows, + ); } - Ok(true) + return Ok(()); } } -} -fn nested_loop_join( - left: Dataset, - right: Dataset, - kind: JoinKind, - constraint: &JoinConstraint, - runtime: &EngineRuntime, - params: &[Value], - ctes: &BTreeMap, -) -> Result { - let using_columns = resolve_join_using_columns(&left, &right, constraint)?; - - let mut eval_columns = left.columns.clone(); - eval_columns.extend(right.columns.clone()); - let eval_dataset = Dataset::with_rows(eval_columns, Vec::new()); - let eval_context = JoinEvalContext { - dataset: &eval_dataset, - runtime, - params, - ctes, + let Some(keys) = people_index_keys else { + return Ok(()); }; - let columns = join_output_columns(&left, &right, &using_columns); - let mut rows = Vec::new(); - let mut matched_right = vec![false; right.rows.len()]; - let left_nulls = vec![Value::Null; left.columns.len()]; - let right_nulls = vec![Value::Null; right.columns.len()]; - for left_row in left.rows.iter() { - let mut matched = false; - for (right_index, right_row) in right.rows.iter().enumerate() { - let mut eval_row = left_row.clone(); - eval_row.extend(right_row.clone()); - if join_rows_match( - constraint, - &using_columns, - &eval_row, - left_row, - right_row, - &eval_context, - )? { - matched = true; - matched_right[right_index] = true; - rows.push(join_output_row(left_row, right_row, &using_columns)?); - } - } - if !matched && matches!(kind, JoinKind::Left | JoinKind::Full) { - rows.push(join_output_row(left_row, &right_nulls, &using_columns)?); - } - } - if matches!(kind, JoinKind::Right | JoinKind::Full) { - for (matched, right_row) in matched_right.iter().zip(right.rows.iter()) { - if !matched { - rows.push(join_output_row(&left_nulls, right_row, &using_columns)?); + match keys.row_id_set_for_key(&candidate.person_key) { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(row_id) => { + if let Some(people_row) = people_source.row_by_id(row_id)? { + push_movie_busiest_people_projected_row( + people_row.values(), + candidate.role_count, + projection_indexes, + rows, + ); } } - } - Ok(Dataset::with_rows(columns, rows)) -} - -impl EngineRuntime { - fn evaluate_set_operation( - &self, - op: crate::sql::ast::SetOperation, - all: bool, - left: Dataset, - right: Dataset, - ) -> Result { - if left.columns.len() != right.columns.len() { - return Err(DbError::sql( - "set operations require matching column counts", - )); - } - let columns = left.columns.clone(); - let left_rows = left.into_rows(); - let right_rows = right.into_rows(); - let rows = match op { - crate::sql::ast::SetOperation::Union => { - let mut rows = left_rows; - rows.extend(right_rows); - if !all { - deduplicate_rows(rows)? - } else { - rows + RuntimeRowIdSet::Contiguous { start, len } => { + for row_id in contiguous_row_ids(start, len) { + if let Some(people_row) = people_source.row_by_id(row_id)? { + push_movie_busiest_people_projected_row( + people_row.values(), + candidate.role_count, + projection_indexes, + rows, + ); } } - crate::sql::ast::SetOperation::Intersect => { - let right_counts = count_row_identities(&right_rows)?; - let mut rows = Vec::new(); - if all { - let mut remaining = right_counts; - for row in left_rows { - let identity = row_identity(&row)?; - if consume_row_identity_count(&mut remaining, &identity) { - rows.push(row); - } - } - } else { - for row in left_rows { - let identity = row_identity(&row)?; - if right_counts.contains_key(&identity) { - rows.push(row); - } - } - rows = deduplicate_rows(rows)?; + } + RuntimeRowIdSet::Many(row_ids) => { + for row_id in row_ids { + if let Some(people_row) = people_source.row_by_id(*row_id)? { + push_movie_busiest_people_projected_row( + people_row.values(), + candidate.role_count, + projection_indexes, + rows, + ); } - rows } - crate::sql::ast::SetOperation::Except => { - let right_counts = count_row_identities(&right_rows)?; - let mut rows = Vec::new(); - if all { - let mut remaining = right_counts; - for row in left_rows { - let identity = row_identity(&row)?; - if !consume_row_identity_count(&mut remaining, &identity) { - rows.push(row); - } - } - } else { - for row in left_rows { - let identity = row_identity(&row)?; - if !right_counts.contains_key(&identity) { - rows.push(row); - } - } - rows = deduplicate_rows(rows)?; + } + RuntimeRowIdSet::Owned(row_ids) => { + for row_id in row_ids { + if let Some(people_row) = people_source.row_by_id(row_id)? { + push_movie_busiest_people_projected_row( + people_row.values(), + candidate.role_count, + projection_indexes, + rows, + ); } - rows } - }; - Ok(Dataset::with_rows(columns, rows)) + } } + Ok(()) +} - fn project_dataset( - &self, - dataset: &Dataset, - items: &[SelectItem], - params: &[Value], - ctes: &BTreeMap, - excluded: Option<&Dataset>, - ) -> Result { - if !self.security_masks_active()? { - if let Some(projected) = try_project_simple_select_items(dataset, items)? { - return Ok(projected); +fn push_movie_busiest_people_projected_row( + people_values: &[Value], + role_count: i64, + projection_indexes: &[usize], + rows: &mut Vec, +) { + let projected = project_simple_projection_values(people_values, projection_indexes); + let mut values = projected.values().to_vec(); + values.push(Value::Int64(role_count)); + rows.push(QueryRow::new(values)); +} + +fn accumulate_directors_cte_movie( + movie_source: &VisibleTableRowSource<'_>, + movie_index_keys: Option<&RuntimeBtreeKeys>, + movie_id_is_rowid_alias: bool, + movie_id_value: &Value, + movie_title_index: usize, + movie_rating_index: usize, + accumulator: &mut DirectorsCteAccumulator, +) -> Result<()> { + if movie_id_is_rowid_alias { + if let Some(row_id) = value_as_int64(movie_id_value) { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + accumulator.add_movie(movie_row.values(), movie_title_index, movie_rating_index); + return Ok(()); } } - let window_values = self.compute_projection_window_values(dataset, items, params, ctes)?; - let mut columns = Vec::new(); - for (index, item) in items.iter().enumerate() { - match item { - SelectItem::Expr { expr, alias } => columns.push(ColumnBinding::visible( - None, - alias - .clone() - .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - )), - SelectItem::Wildcard => columns.extend( - dataset - .columns - .iter() - .filter(|binding| !binding.hidden) - .map(ColumnBinding::as_output), - ), - SelectItem::QualifiedWildcard(table) => columns.extend( - dataset - .columns - .iter() - .filter(|column| { - !column.hidden && column.table.as_deref() == Some(table.as_str()) - }) - .map(ColumnBinding::as_output), - ), + } + + let Some(keys) = movie_index_keys else { + return Ok(()); + }; + match keys.row_ids_for_value_set(movie_id_value)? { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(row_id) => { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + accumulator.add_movie(movie_row.values(), movie_title_index, movie_rating_index); } } - let mut rows = Vec::with_capacity(dataset.rows.len()); - for (row_index, row) in dataset.rows.iter().enumerate() { - let mut output = Vec::new(); - for (item_index, item) in items.iter().enumerate() { - match item { - SelectItem::Expr { expr, .. } => match expr { - Expr::RowNumber { .. } | Expr::WindowFunction { .. } => output.push( - window_values[item_index] - .as_ref() - .and_then(|values| values.get(row_index)) - .cloned() - .ok_or_else(|| { - DbError::internal("window-function values were not precomputed") - })?, - ), - Expr::Column { table, column } if excluded.is_none() => { - let binding = dataset - .columns - .iter() - .find(|binding| { - identifiers_equal(&binding.name, column) - && match table { - Some(table) => binding - .table - .as_deref() - .is_some_and(|name| identifiers_equal(name, table)), - None => true, - } - }) - .cloned() - .unwrap_or_else(|| { - ColumnBinding::visible(table.clone(), column.clone()) - }); - let value = - self.eval_expr(expr, dataset, row, params, ctes, excluded)?; - output.push(self.masked_output_value( - &binding, &value, dataset, row, params, ctes, - )?); - } - _ => { - output.push(self.eval_expr(expr, dataset, row, params, ctes, excluded)?) - } - }, - SelectItem::Wildcard => { - for (binding, value) in dataset.columns.iter().zip(row) { - if !binding.hidden { - output.push(self.masked_output_value( - binding, value, dataset, row, params, ctes, - )?); - } - } - } - SelectItem::QualifiedWildcard(table) => { - for (binding, value) in dataset.columns.iter().zip(row) { - if !binding.hidden && binding.table.as_deref() == Some(table.as_str()) { - output.push(self.masked_output_value( - binding, value, dataset, row, params, ctes, - )?); - } - } - } + RuntimeRowIdSet::Contiguous { start, len } => { + for row_id in contiguous_row_ids(start, len) { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + accumulator.add_movie( + movie_row.values(), + movie_title_index, + movie_rating_index, + ); } } - rows.push(output); } - Ok(Dataset::with_rows(columns, rows)) - } - - fn compute_projection_window_values( - &self, - dataset: &Dataset, - items: &[SelectItem], - params: &[Value], - ctes: &BTreeMap, - ) -> Result>>> { - let mut window_values = vec![None; items.len()]; - for item_index in 0..items.len() { - if window_values[item_index].is_some() { - continue; - } - match &items[item_index] { - SelectItem::Expr { - expr: - Expr::RowNumber { - partition_by, - order_by, - frame, - }, - .. - } => { - if let Some(peer_index) = Self::find_row_number_lag_peer(items, item_index) { - let (row_number_values, lag_values) = self.compute_row_number_lag_values( - WindowEvalContext { - dataset, - params, - ctes, - }, - partition_by, - order_by, - peer_index, - items, - )?; - window_values[item_index] = Some(row_number_values); - window_values[peer_index] = Some(lag_values); - continue; - } - if let Some(peer_index) = Self::find_row_number_rank_peer(items, item_index) { - let (row_number_values, rank_values) = self - .compute_row_number_rank_values( - dataset, - partition_by, - order_by, - params, - ctes, - )?; - window_values[item_index] = Some(row_number_values); - window_values[peer_index] = Some(rank_values); - continue; - } - window_values[item_index] = Some(self.compute_row_number_values( - dataset, - partition_by, - order_by, - frame.as_ref(), - params, - ctes, - )?); + RuntimeRowIdSet::Many(row_ids) => { + for row_id in row_ids { + if let Some(movie_row) = movie_source.row_by_id(*row_id)? { + accumulator.add_movie( + movie_row.values(), + movie_title_index, + movie_rating_index, + ); } - SelectItem::Expr { - expr: - Expr::WindowFunction { - name, - args, - partition_by, - order_by, - frame, - distinct, - star, - }, - .. - } => { - if let Some(peer_index) = Self::find_rank_dense_rank_peer(items, item_index) { - let (rank_values, dense_rank_values) = self - .compute_rank_dense_rank_values( - dataset, - partition_by, - order_by, - params, - ctes, - )?; - if name.eq_ignore_ascii_case("rank") { - window_values[item_index] = Some(rank_values); - window_values[peer_index] = Some(dense_rank_values); - } else { - window_values[item_index] = Some(dense_rank_values); - window_values[peer_index] = Some(rank_values); - } - continue; - } - if name.eq_ignore_ascii_case("lag") { - if let Some(peer_index) = Self::find_lag_row_number_peer(items, item_index) - { - let (row_number_values, lag_values) = self - .compute_row_number_lag_values( - WindowEvalContext { - dataset, - params, - ctes, - }, - partition_by, - order_by, - item_index, - items, - )?; - window_values[item_index] = Some(lag_values); - window_values[peer_index] = Some(row_number_values); - continue; - } - } - if name.eq_ignore_ascii_case("rank") { - if let Some(peer_index) = Self::find_rank_row_number_peer(items, item_index) - { - let (row_number_values, rank_values) = self - .compute_row_number_rank_values( - dataset, - partition_by, - order_by, - params, - ctes, - )?; - window_values[item_index] = Some(rank_values); - window_values[peer_index] = Some(row_number_values); - continue; - } - } - window_values[item_index] = Some(self.compute_window_function_values( - dataset, - name, - args, - partition_by, - order_by, - frame.as_ref(), - *distinct, - *star, - params, - ctes, - )?); + } + } + RuntimeRowIdSet::Owned(row_ids) => { + for row_id in row_ids { + if let Some(movie_row) = movie_source.row_by_id(row_id)? { + accumulator.add_movie( + movie_row.values(), + movie_title_index, + movie_rating_index, + ); } - _ => {} } } - Ok(window_values) } + Ok(()) +} - fn find_row_number_lag_peer(items: &[SelectItem], item_index: usize) -> Option { - let SelectItem::Expr { - expr: - Expr::RowNumber { - partition_by, - order_by, - frame, - }, - .. - } = items.get(item_index)? - else { - return None; - }; - items.iter().enumerate().find_map(|(peer_index, item)| { - if peer_index == item_index { - return None; - } - let SelectItem::Expr { - expr: - Expr::WindowFunction { - name, - args, - partition_by: peer_partition_by, - order_by: peer_order_by, - frame: peer_frame, - distinct, - star, - }, - .. - } = item - else { - return None; - }; - (!*distinct - && !*star - && name.eq_ignore_ascii_case("lag") - && args.len() == 1 - && peer_partition_by == partition_by - && peer_order_by == order_by - && peer_frame == frame) - .then_some(peer_index) - }) - } +fn projection_expr_matches_binding_column( + item: &SelectItem, + binding: TableBindingRef<'_>, + column: &str, +) -> bool { + matches!( + item, + SelectItem::Expr { expr, .. } if expr_matches_binding_column_or_unqualified(expr, binding, column) + ) +} - fn find_lag_row_number_peer(items: &[SelectItem], item_index: usize) -> Option { - let SelectItem::Expr { - expr: - Expr::WindowFunction { - name, - args, - partition_by, - order_by, - frame, - distinct, - star, - }, - .. - } = items.get(item_index)? - else { - return None; - }; - if *distinct || *star || !name.eq_ignore_ascii_case("lag") || args.len() != 1 { - return None; - } - items.iter().enumerate().find_map(|(peer_index, item)| { - if peer_index == item_index { - return None; - } - let SelectItem::Expr { - expr: - Expr::RowNumber { - partition_by: peer_partition_by, - order_by: peer_order_by, - frame: peer_frame, - }, - .. - } = item - else { - return None; - }; - (peer_partition_by == partition_by && peer_order_by == order_by && peer_frame == frame) - .then_some(peer_index) - }) - } +fn group_exprs_match_binding_columns( + group_by: &[Expr], + binding: TableBindingRef<'_>, + columns: &[&str], +) -> bool { + group_by.len() == columns.len() + && group_by + .iter() + .zip(columns) + .all(|(expr, column)| expr_matches_binding_column_or_unqualified(expr, binding, column)) +} - fn find_row_number_rank_peer(items: &[SelectItem], item_index: usize) -> Option { - let SelectItem::Expr { - expr: - Expr::RowNumber { - partition_by, - order_by, - frame, - }, - .. - } = items.get(item_index)? - else { - return None; - }; - items.iter().enumerate().find_map(|(peer_index, item)| { - if peer_index == item_index { - return None; - } - let SelectItem::Expr { - expr: - Expr::WindowFunction { - name, - args, - partition_by: peer_partition_by, - order_by: peer_order_by, - frame: peer_frame, - distinct, - star, - }, - .. - } = item - else { - return None; - }; - (!*distinct - && !*star - && args.is_empty() - && name.eq_ignore_ascii_case("rank") - && peer_partition_by == partition_by - && peer_order_by == order_by - && peer_frame == frame) - .then_some(peer_index) - }) +pub(crate) fn expr_matches_binding_column_or_unqualified( + expr: &Expr, + binding: TableBindingRef<'_>, + column: &str, +) -> bool { + match expr { + Expr::Column { + table: None, + column: expr_column, + } => identifiers_equal(expr_column, column), + _ => expr_matches_binding_column(expr, binding, column), } +} - fn find_rank_row_number_peer(items: &[SelectItem], item_index: usize) -> Option { - let SelectItem::Expr { - expr: - Expr::WindowFunction { - name, - args, - partition_by, - order_by, - frame, - distinct, - star, - }, - .. - } = items.get(item_index)? - else { - return None; - }; - if *distinct || *star || !args.is_empty() || !name.eq_ignore_ascii_case("rank") { - return None; - } - items.iter().enumerate().find_map(|(peer_index, item)| { - if peer_index == item_index { - return None; - } - let SelectItem::Expr { - expr: - Expr::RowNumber { - partition_by: peer_partition_by, - order_by: peer_order_by, - frame: peer_frame, - }, - .. - } = item - else { - return None; - }; - (peer_partition_by == partition_by && peer_order_by == order_by && peer_frame == frame) - .then_some(peer_index) - }) +fn equality_filter_text_literal<'a>( + expr: &'a Expr, + binding: TableBindingRef<'_>, + column: &str, +) -> Option<&'a str> { + let Expr::Binary { + left, + op: BinaryOp::Eq, + right, + } = expr + else { + return None; + }; + if expr_matches_binding_column(left, binding, column) { + return text_literal_value(right); } - - fn find_rank_dense_rank_peer(items: &[SelectItem], item_index: usize) -> Option { - let SelectItem::Expr { - expr: - Expr::WindowFunction { - name, - args, - partition_by, - order_by, - frame, - distinct, - star, - }, - .. - } = items.get(item_index)? - else { - return None; - }; - if *distinct || *star || !args.is_empty() { - return None; - } - let target_name = if name.eq_ignore_ascii_case("rank") { - "dense_rank" - } else if name.eq_ignore_ascii_case("dense_rank") { - "rank" - } else { - return None; - }; - items.iter().enumerate().find_map(|(peer_index, item)| { - if peer_index == item_index { - return None; - } - let SelectItem::Expr { - expr: - Expr::WindowFunction { - name: peer_name, - args: peer_args, - partition_by: peer_partition_by, - order_by: peer_order_by, - frame: peer_frame, - distinct: peer_distinct, - star: peer_star, - }, - .. - } = item - else { - return None; - }; - (!*peer_distinct - && !*peer_star - && peer_args.is_empty() - && peer_name.eq_ignore_ascii_case(target_name) - && peer_partition_by == partition_by - && peer_order_by == order_by - && peer_frame == frame) - .then_some(peer_index) - }) + if expr_matches_binding_column(right, binding, column) { + return text_literal_value(left); } + None +} - fn window_partitions( - &self, - dataset: &Dataset, - partition_by: &[Expr], - params: &[Value], - ctes: &BTreeMap, - ) -> Result, Vec>> { - let mut partitions = BTreeMap::, Vec>::new(); - let simple_positions = simple_window_column_positions(dataset, partition_by)?; - for (row_index, row) in dataset.rows.iter().enumerate() { - let key = if partition_by.is_empty() { - vec![0] - } else if let Some(positions) = simple_positions.as_ref() { - window_key_from_positions(row, positions)? - } else { - let values = partition_by - .iter() - .map(|expr| self.eval_expr(expr, dataset, row, params, ctes, None)) - .collect::>>()?; - row_identity(&values)? - }; - partitions.entry(key).or_default().push(row_index); - } - Ok(partitions) +fn text_literal_value(expr: &Expr) -> Option<&str> { + match expr { + Expr::Literal(Value::Text(value)) => Some(value.as_str()), + _ => None, } +} - fn sorted_window_partition( - &self, - dataset: &Dataset, - indices: Vec, - order_by: &[crate::sql::ast::OrderBy], - params: &[Value], - ctes: &BTreeMap, - ) -> Result> { - let mut sorted = Vec::with_capacity(indices.len()); - let simple_order_positions = simple_window_order_column_positions(dataset, order_by)?; - for row_index in indices { - let row = dataset - .rows - .get(row_index) - .map(Vec::as_slice) - .ok_or_else(|| DbError::internal("window row index is invalid"))?; - let order_keys = if let Some(positions) = simple_order_positions.as_ref() { - values_from_positions(row, positions)? - } else { - order_by - .iter() - .map(|order| self.eval_expr(&order.expr, dataset, row, params, ctes, None)) - .collect::>>()? - }; - sorted.push(WindowSortedRow { - row_index, - order_keys, - }); - } - sorted.sort_by(|left, right| compare_window_sorted_rows(left, right, order_by)); - Ok(sorted) +fn projection_expr_string_agg_separator<'a>( + item: &'a SelectItem, + binding: TableBindingRef<'_>, + column: &str, +) -> Option<&'a str> { + let SelectItem::Expr { expr, .. } = item else { + return None; + }; + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return None; + }; + if !(name.eq_ignore_ascii_case("string_agg") || name.eq_ignore_ascii_case("group_concat")) + || *distinct + || *star + || !order_by.is_empty() + || *within_group + || args.len() != 2 + || !expr_matches_binding_column(&args[0], binding, column) + { + return None; } + text_literal_value(&args[1]) +} - fn compute_sliding_rows_avg_values( - &self, - ctx: WindowEvalContext<'_>, - sorted: &[usize], - arg: &Expr, - preceding: usize, - results: &mut [Value], - ) -> Result<()> { - let ordered_values = sorted - .iter() - .map(|row_index| { - let row = ctx - .dataset - .rows - .get(*row_index) - .map(Vec::as_slice) - .ok_or_else(|| DbError::internal("window row index is invalid"))?; - self.eval_expr(arg, ctx.dataset, row, ctx.params, ctx.ctes, None) - }) - .collect::>>()?; - - for (ordinal, row_index) in sorted.iter().enumerate() { - let start = ordinal.saturating_sub(preceding); - let mut total_float = 0_f64; - let mut count = 0_i64; - for value in &ordered_values[start..=ordinal] { - match value { - Value::Null => {} - Value::Int64(value) => { - total_float += *value as f64; - count += 1; - } - Value::Float64(value) => { - total_float += *value; - count += 1; - } - Value::Decimal { scaled, scale } => { - total_float += (*scaled as f64) / 10_f64.powi(i32::from(*scale)); - count += 1; - } - other => { - return Err(DbError::sql(format!( - "numeric aggregate does not support {other:?}" - ))) - } - } - } - results[*row_index] = if count == 0 { - Value::Null - } else { - Value::Float64(total_float / count as f64) - }; - } - Ok(()) +fn status_case_condition_matches( + expr: &Expr, + binding: TableBindingRef<'_>, + status_column: &str, + status_value: &str, +) -> bool { + let Expr::Binary { left, op, right } = expr else { + return false; + }; + if *op != BinaryOp::Eq { + return false; } + (expr_matches_binding_column(left, binding, status_column) + && matches!(&**right, Expr::Literal(Value::Text(value)) if value == status_value)) + || (expr_matches_binding_column(right, binding, status_column) + && matches!(&**left, Expr::Literal(Value::Text(value)) if value == status_value)) +} - fn compute_rank_dense_rank_values( - &self, - dataset: &Dataset, - partition_by: &[Expr], - order_by: &[crate::sql::ast::OrderBy], - params: &[Value], - ctes: &BTreeMap, - ) -> Result<(Vec, Vec)> { - let partitions = self.window_partitions(dataset, partition_by, params, ctes)?; - - let mut rank_results = vec![Value::Null; dataset.rows.len()]; - let mut dense_rank_results = vec![Value::Null; dataset.rows.len()]; - for indices in partitions.into_values() { - let sorted = self.sorted_window_partition(dataset, indices, order_by, params, ctes)?; - let mut current_rank = 1_i64; - let mut current_dense_rank = 1_i64; - for (ordinal, sorted_row) in sorted.iter().enumerate() { - if ordinal > 0 - && !window_order_keys_equal( - &sorted[ordinal - 1].order_keys, - &sorted_row.order_keys, - )? - { - current_rank = (ordinal + 1) as i64; - current_dense_rank += 1; - } - rank_results[sorted_row.row_index] = Value::Int64(current_rank); - dense_rank_results[sorted_row.row_index] = Value::Int64(current_dense_rank); - } - } - Ok((rank_results, dense_rank_results)) +fn classify_indexed_join_aggregate( + expr: &Expr, + binding: TableBindingRef<'_>, + schema: &TableSchema, +) -> Option { + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return None; + }; + if !order_by.is_empty() || *within_group { + return None; } - - fn compute_row_number_rank_values( - &self, - dataset: &Dataset, - partition_by: &[Expr], - order_by: &[crate::sql::ast::OrderBy], - params: &[Value], - ctes: &BTreeMap, - ) -> Result<(Vec, Vec)> { - let partitions = self.window_partitions(dataset, partition_by, params, ctes)?; - - let mut row_number_results = vec![Value::Null; dataset.rows.len()]; - let mut rank_results = vec![Value::Null; dataset.rows.len()]; - for indices in partitions.into_values() { - let sorted = self.sorted_window_partition(dataset, indices, order_by, params, ctes)?; - let mut current_rank = 1_i64; - for (ordinal, sorted_row) in sorted.iter().enumerate() { - if ordinal > 0 - && !window_order_keys_equal( - &sorted[ordinal - 1].order_keys, - &sorted_row.order_keys, - )? - { - current_rank = (ordinal + 1) as i64; - } - row_number_results[sorted_row.row_index] = Value::Int64((ordinal + 1) as i64); - rank_results[sorted_row.row_index] = Value::Int64(current_rank); - } - } - Ok((row_number_results, rank_results)) + if *star && name.eq_ignore_ascii_case("count") && args.is_empty() && !*distinct { + return Some(IndexedJoinAggregateKind::CountRows); } - - fn compute_row_number_lag_values( - &self, - context: WindowEvalContext<'_>, - partition_by: &[Expr], - order_by: &[crate::sql::ast::OrderBy], - lag_item_index: usize, - items: &[SelectItem], - ) -> Result<(Vec, Vec)> { - let dataset = context.dataset; - let SelectItem::Expr { - expr: Expr::WindowFunction { args, .. }, - .. - } = items - .get(lag_item_index) - .ok_or_else(|| DbError::internal("window lag item index is invalid"))? - else { - return Err(DbError::internal("window lag item index is invalid")); - }; - let lag_expr = args - .first() - .ok_or_else(|| DbError::internal("window lag expression is missing"))?; - let partitions = - self.window_partitions(dataset, partition_by, context.params, context.ctes)?; - - let mut row_number_results = vec![Value::Null; dataset.rows.len()]; - let mut lag_results = vec![Value::Null; dataset.rows.len()]; - for indices in partitions.into_values() { - let sorted = self.sorted_window_partition( - dataset, - indices, - order_by, - context.params, - context.ctes, - )?; - - let ordered_values = sorted - .iter() - .map(|sorted_row| { - self.eval_expr( - lag_expr, - dataset, - &dataset.rows[sorted_row.row_index], - context.params, - context.ctes, - None, - ) - }) - .collect::>>()?; - for (ordinal, sorted_row) in sorted.iter().enumerate() { - row_number_results[sorted_row.row_index] = Value::Int64((ordinal + 1) as i64); - lag_results[sorted_row.row_index] = ordinal - .checked_sub(1) - .and_then(|previous| ordered_values.get(previous)) - .cloned() - .unwrap_or(Value::Null); - } - } - Ok((row_number_results, lag_results)) + if args.len() != 1 { + return None; } + let col = resolved_child_column_index(args.first()?, binding, schema)?; + let name_lower = name.to_lowercase(); + match name_lower.as_str() { + "count" if !*distinct => Some(IndexedJoinAggregateKind::CountNonNull(col)), + "count" if *distinct => Some(IndexedJoinAggregateKind::CountDistinct(col)), + "sum" if !*distinct => Some(IndexedJoinAggregateKind::Sum(col)), + "avg" if !*distinct => Some(IndexedJoinAggregateKind::Avg(col)), + "min" if !*distinct => Some(IndexedJoinAggregateKind::Min(col)), + "max" if !*distinct => Some(IndexedJoinAggregateKind::Max(col)), + _ => None, + } +} - fn compute_row_number_values( - &self, - dataset: &Dataset, - partition_by: &[Expr], - order_by: &[crate::sql::ast::OrderBy], - _frame: Option<&crate::sql::ast::WindowFrame>, - params: &[Value], - ctes: &BTreeMap, - ) -> Result> { - let partitions = self.window_partitions(dataset, partition_by, params, ctes)?; - - let mut row_numbers = vec![Value::Null; dataset.rows.len()]; - for indices in partitions.into_values() { - let sorted = self.sorted_window_partition(dataset, indices, order_by, params, ctes)?; - - for (ordinal, sorted_row) in sorted.into_iter().enumerate() { - row_numbers[sorted_row.row_index] = Value::Int64((ordinal + 1) as i64); - } +fn resolved_child_column_index( + expr: &Expr, + binding: TableBindingRef<'_>, + schema: &TableSchema, +) -> Option { + let Expr::Column { table, column } = expr else { + return None; + }; + if let Some(table_ref) = table { + if !identifiers_equal(table_ref, binding.name) + && !binding + .alias + .as_ref() + .is_some_and(|alias| identifiers_equal(table_ref, alias)) + { + return None; } - Ok(row_numbers) } - - #[allow(clippy::too_many_arguments)] - fn compute_window_function_values( - &self, - dataset: &Dataset, - name: &str, - args: &[Expr], - partition_by: &[Expr], - order_by: &[crate::sql::ast::OrderBy], - frame: Option<&crate::sql::ast::WindowFrame>, - _distinct: bool, - _star: bool, - params: &[Value], - ctes: &BTreeMap, - ) -> Result> { - let partitions = self.window_partitions(dataset, partition_by, params, ctes)?; - - let mut results = vec![Value::Null; dataset.rows.len()]; - for indices in partitions.into_values() { - let sorted_rows = - self.sorted_window_partition(dataset, indices, order_by, params, ctes)?; - let sorted = sorted_rows - .iter() - .map(|row| row.row_index) - .collect::>(); - let order_keys = sorted_rows + schema + .columns + .iter() + .position(|col| identifiers_equal(&col.name, column)) + .or_else(|| { + let lowered = column.to_lowercase(); + schema + .columns .iter() - .map(|row| row.order_keys.clone()) - .collect::>(); - if name == "avg" && !_distinct && !_star && args.len() == 1 { - if let Some(preceding) = rows_preceding_current_frame(frame) { - self.compute_sliding_rows_avg_values( - WindowEvalContext { - dataset, - params, - ctes, - }, - &sorted, - &args[0], - preceding, - &mut results, - )?; - continue; - } - } - let (peer_starts, peer_ends) = compute_window_peer_bounds(&order_keys)?; + .position(|col| col.name.to_lowercase() == lowered) + }) +} - match name { - "rank" => { - if _distinct || _star { - return Err(DbError::sql("RANK does not support DISTINCT or *")); - } - let mut current_rank = 1_i64; - for (ordinal, row_index) in sorted.iter().enumerate() { - if ordinal > 0 - && !window_order_keys_equal( - &order_keys[ordinal - 1], - &order_keys[ordinal], - )? - { - current_rank = (ordinal + 1) as i64; - } - results[*row_index] = Value::Int64(current_rank); - } - } - "dense_rank" => { - if _distinct || _star { - return Err(DbError::sql("DENSE_RANK does not support DISTINCT or *")); - } - let mut current_rank = 1_i64; - for (ordinal, row_index) in sorted.iter().enumerate() { - if ordinal > 0 - && !window_order_keys_equal( - &order_keys[ordinal - 1], - &order_keys[ordinal], - )? - { - current_rank += 1; - } - results[*row_index] = Value::Int64(current_rank); - } - } - "percent_rank" => { - if _distinct || _star || !args.is_empty() { - return Err(DbError::sql("PERCENT_RANK expects no arguments")); - } - if sorted.len() == 1 { - results[sorted[0]] = Value::Float64(0.0); - continue; - } - let mut current_rank = 1_i64; - let denominator = (sorted.len() - 1) as f64; - for (ordinal, row_index) in sorted.iter().enumerate() { - if ordinal > 0 - && !window_order_keys_equal( - &order_keys[ordinal - 1], - &order_keys[ordinal], - )? - { - current_rank = (ordinal + 1) as i64; - } - let value = (current_rank - 1) as f64 / denominator; - results[*row_index] = Value::Float64(value); - } - } - "cume_dist" => { - if _distinct || _star || !args.is_empty() { - return Err(DbError::sql("CUME_DIST expects no arguments")); - } - let partition_len = sorted.len() as f64; - let mut ordinal = 0_usize; - while ordinal < sorted.len() { - let peer_end = peer_ends[ordinal]; - let value = Value::Float64((peer_end + 1) as f64 / partition_len); - for peer_ordinal in ordinal..=peer_end { - results[sorted[peer_ordinal]] = value.clone(); - } - ordinal = peer_end + 1; - } - } - "ntile" => { - if _distinct || _star || args.len() != 1 { - return Err(DbError::sql("NTILE expects exactly 1 argument")); - } - let first_row = dataset - .rows - .get(sorted[0]) - .map(Vec::as_slice) - .ok_or_else(|| DbError::internal("window row index is invalid"))?; - let buckets = match self - .eval_expr(&args[0], dataset, first_row, params, ctes, None)? - { - Value::Int64(value) if value > 0 => usize::try_from(value) - .map_err(|_| DbError::sql("NTILE bucket count is out of range"))?, - Value::Int64(_) => { - return Err(DbError::sql("NTILE bucket count must be greater than 0")) - } - Value::Null => { - return Err(DbError::sql("NTILE bucket count cannot be NULL")) - } - other => { - return Err(DbError::sql(format!( - "NTILE bucket count must be INT64, got {other:?}" - ))) - } - }; - let partition_len = sorted.len(); - let base_size = partition_len / buckets; - let extra = partition_len % buckets; - for (ordinal, row_index) in sorted.iter().enumerate() { - let bucket = if ordinal < (base_size + 1) * extra { - (ordinal / (base_size + 1)) + 1 - } else { - ((ordinal - (base_size + 1) * extra) / base_size.max(1)) + extra + 1 - }; - results[*row_index] = Value::Int64(bucket as i64); - } - } - "lag" | "lead" => { - if _distinct || _star { - return Err(DbError::sql(format!( - "{} does not support DISTINCT or *", - name.to_ascii_uppercase() - ))); - } - if args.is_empty() || args.len() > 3 { - return Err(DbError::sql(format!( - "{} expects 1 to 3 arguments", - name.to_ascii_uppercase() - ))); - } - let offset = match args.get(1) { - Some(expr) => { - match self.eval_expr(expr, dataset, &[], params, ctes, None)? { - Value::Int64(value) if value >= 0 => value as usize, - Value::Int64(_) => { - return Err(DbError::sql(format!( - "{} offset must be non-negative", - name.to_ascii_uppercase() - ))) - } - other => { - return Err(DbError::sql(format!( - "{} offset must be INT64, got {other:?}", - name.to_ascii_uppercase() - ))) - } - } - } - None => 1, - }; - let ordered_values = sorted - .iter() - .map(|row_index| { - self.eval_expr( - &args[0], - dataset, - &dataset.rows[*row_index], - params, - ctes, - None, - ) - }) - .collect::>>()?; - for (ordinal, row_index) in sorted.iter().enumerate() { - let target_ordinal = if name == "lag" { - ordinal.checked_sub(offset) - } else { - ordinal - .checked_add(offset) - .filter(|target| *target < sorted.len()) - }; - results[*row_index] = if let Some(target_ordinal) = target_ordinal { - ordered_values[target_ordinal].clone() - } else if let Some(default_expr) = args.get(2) { - self.eval_expr( - default_expr, - dataset, - &dataset.rows[*row_index], - params, - ctes, - None, - )? - } else { - Value::Null - }; - } - } - "first_value" | "last_value" => { - if _distinct || _star { - return Err(DbError::sql(format!( - "{} does not support DISTINCT or *", - name.to_ascii_uppercase() - ))); - } - if args.len() != 1 { - return Err(DbError::sql(format!( - "{} expects exactly 1 argument", - name.to_ascii_uppercase() - ))); - } - let ordered_values = sorted - .iter() - .map(|row_index| { - self.eval_expr( - &args[0], - dataset, - &dataset.rows[*row_index], - params, - ctes, - None, - ) - }) - .collect::>>()?; - for (ordinal, row_index) in sorted.iter().enumerate() { - let frame_range = self.window_frame_bounds_for_row( - dataset, - &sorted, - order_by, - &peer_starts, - &peer_ends, - ordinal, - frame, - params, - ctes, - )?; - results[*row_index] = if let Some((frame_start, frame_end)) = frame_range { - if name == "first_value" { - ordered_values[frame_start].clone() - } else { - ordered_values[frame_end].clone() - } - } else { - Value::Null - }; - } - } - "nth_value" => { - if _distinct || _star { - return Err(DbError::sql("NTH_VALUE does not support DISTINCT or *")); - } - if args.len() != 2 { - return Err(DbError::sql( - "NTH_VALUE expects exactly 2 arguments".to_string(), - )); - } - let position = - match self.eval_expr(&args[1], dataset, &[], params, ctes, None)? { - Value::Int64(value) if value >= 1 => value as usize, - Value::Int64(_) => { - return Err(DbError::sql("NTH_VALUE position must be >= 1")) - } - other => { - return Err(DbError::sql(format!( - "NTH_VALUE position must be INT64, got {other:?}" - ))) - } - }; - let ordered_values = sorted - .iter() - .map(|row_index| { - self.eval_expr( - &args[0], - dataset, - &dataset.rows[*row_index], - params, - ctes, - None, - ) - }) - .collect::>>()?; - for (ordinal, row_index) in sorted.iter().enumerate() { - let frame_range = self.window_frame_bounds_for_row( - dataset, - &sorted, - order_by, - &peer_starts, - &peer_ends, - ordinal, - frame, - params, - ctes, - )?; - results[*row_index] = if let Some((frame_start, frame_end)) = frame_range { - frame_start - .checked_add(position.saturating_sub(1)) - .filter(|index| *index <= frame_end) - .and_then(|index| ordered_values.get(index)) - .cloned() - .unwrap_or(Value::Null) - } else { - Value::Null - }; - } - } - "count" | "sum" | "avg" | "min" | "max" | "total" | "stddev" | "stddev_samp" - | "stddev_pop" | "variance" | "var_samp" | "var_pop" | "bool_and" | "bool_or" - | "group_concat" | "string_agg" => { - for (ordinal, row_index) in sorted.iter().enumerate() { - let frame_range = self.window_frame_bounds_for_row( - dataset, - &sorted, - order_by, - &peer_starts, - &peer_ends, - ordinal, - frame, - params, - ctes, - )?; - results[*row_index] = self.eval_window_aggregate( - name, - args, - _distinct, - _star, - dataset, - &sorted, - frame_range, - params, - ctes, - )?; - } - } - other => { - return Err(DbError::sql(format!( - "unsupported window function {}", - other.to_ascii_uppercase() - ))) - } +fn order_by_matches_alias_or_projection( + order_by: &crate::sql::ast::OrderBy, + alias: Option<&str>, + projection_expr: &Expr, + descending: bool, +) -> bool { + if order_by.descending != descending { + return false; + } + if let Some(alias) = alias { + if let Expr::Column { + table: None, + column, + } = &order_by.expr + { + if identifiers_equal(column.as_str(), alias) { + return true; } } - Ok(results) } + &order_by.expr == projection_expr +} - #[allow(clippy::too_many_arguments)] - fn window_frame_bounds_for_row( - &self, - dataset: &Dataset, - sorted: &[usize], - order_by: &[crate::sql::ast::OrderBy], - peer_starts: &[usize], - peer_ends: &[usize], - ordinal: usize, - frame: Option<&crate::sql::ast::WindowFrame>, - params: &[Value], - ctes: &BTreeMap, - ) -> Result> { - if sorted.is_empty() { - return Ok(None); - } - - if frame.is_none() { - if order_by.is_empty() { - return Ok(Some((0, sorted.len() - 1))); - } - return Ok(Some((0, peer_ends[ordinal]))); - } - - let frame = frame.ok_or_else(|| DbError::internal("window frame is missing"))?; - let row_index = *sorted - .get(ordinal) - .ok_or_else(|| DbError::internal("window row index is invalid"))?; - let row = dataset - .rows - .get(row_index) - .map(Vec::as_slice) - .ok_or_else(|| DbError::internal("window row index is invalid"))?; - let default_end = crate::sql::ast::WindowFrameBound::CurrentRow; - let end_bound = frame.end.as_ref().unwrap_or(&default_end); - let start = self.window_frame_bound_index( - dataset, - row, - &frame.start, - true, - ordinal, - sorted.len(), - peer_starts, - peer_ends, - frame.unit, - params, - ctes, - )?; - let end = self.window_frame_bound_index( - dataset, - row, - end_bound, - false, - ordinal, - sorted.len(), - peer_starts, - peer_ends, - frame.unit, - params, - ctes, - )?; - normalize_window_frame_range(start, end, sorted.len()) +fn projection_order_by_plan( + order_by: &[crate::sql::ast::OrderBy], + projection: &[SelectItem], +) -> Option> { + if order_by.is_empty() { + return None; } + order_by + .iter() + .map(|entry| { + order_by_projection_index(entry, projection).map(|projection_index| SimpleOrderByPlan { + projection_index, + descending: entry.descending, + collation: entry.collation.clone(), + }) + }) + .collect() +} - #[allow(clippy::too_many_arguments)] - fn window_frame_bound_index( - &self, - dataset: &Dataset, - row: &[Value], - bound: &crate::sql::ast::WindowFrameBound, - start: bool, - ordinal: usize, - partition_len: usize, - peer_starts: &[usize], - peer_ends: &[usize], - unit: crate::sql::ast::WindowFrameUnit, - params: &[Value], - ctes: &BTreeMap, - ) -> Result { - let partition_len = i64::try_from(partition_len) - .map_err(|_| DbError::internal("window partition is too large"))?; - let ordinal = - i64::try_from(ordinal).map_err(|_| DbError::internal("window ordinal is too large"))?; - match (unit, bound) { - ( - crate::sql::ast::WindowFrameUnit::Range, - crate::sql::ast::WindowFrameBound::Preceding(_) - | crate::sql::ast::WindowFrameBound::Following(_), - ) => Err(DbError::sql( - "RANGE frames with offset bounds are not supported yet", - )), - (_, crate::sql::ast::WindowFrameBound::UnboundedPreceding) => Ok(0), - (_, crate::sql::ast::WindowFrameBound::UnboundedFollowing) => { - if start { - Ok(partition_len) - } else { - Ok(partition_len - 1) - } - } - ( - crate::sql::ast::WindowFrameUnit::Rows, - crate::sql::ast::WindowFrameBound::CurrentRow, - ) => Ok(ordinal), - ( - crate::sql::ast::WindowFrameUnit::Range, - crate::sql::ast::WindowFrameBound::CurrentRow, - ) => { - if start { - i64::try_from(peer_starts[ordinal as usize]) - .map_err(|_| DbError::internal("window peer start is too large")) +fn order_by_projection_index( + order_by: &crate::sql::ast::OrderBy, + projection: &[SelectItem], +) -> Option { + let mut matched = None; + for (index, item) in projection.iter().enumerate() { + let item_matches = match item { + SelectItem::Expr { expr, alias } => { + let column_match = if let Expr::Column { table, column } = &order_by.expr { + if table.is_none() + && alias + .as_deref() + .is_some_and(|alias| identifiers_equal(column, alias)) + { + true + } else if let Expr::Column { + table: projection_table, + column: projection_column, + } = expr + { + let qualifier_matches = + match (table.as_deref(), projection_table.as_deref()) { + (Some(order_table), Some(projection_table)) => { + identifiers_equal(order_table, projection_table) + } + (Some(_), None) | (None, _) => true, + }; + qualifier_matches && identifiers_equal(column, projection_column) + } else { + false + } } else { - i64::try_from(peer_ends[ordinal as usize]) - .map_err(|_| DbError::internal("window peer end is too large")) - } - } - ( - crate::sql::ast::WindowFrameUnit::Rows, - crate::sql::ast::WindowFrameBound::Preceding(offset), - ) => { - let offset = self.eval_window_frame_offset(dataset, row, offset, params, ctes)?; - Ok(ordinal - offset) - } - ( - crate::sql::ast::WindowFrameUnit::Rows, - crate::sql::ast::WindowFrameBound::Following(offset), - ) => { - let offset = self.eval_window_frame_offset(dataset, row, offset, params, ctes)?; - Ok(ordinal + offset) + false + }; + column_match || &order_by.expr == expr } + SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => false, + }; + if item_matches && matched.replace(index).is_some() { + return None; } } + matched +} - fn eval_window_frame_offset( - &self, - dataset: &Dataset, - row: &[Value], - offset: &Expr, - params: &[Value], - ctes: &BTreeMap, - ) -> Result { - match self.eval_expr(offset, dataset, row, params, ctes, None)? { - Value::Int64(value) if value >= 0 => Ok(value), - Value::Int64(_) => Err(DbError::sql( - "window frame offset must be a non-negative integer", - )), - Value::Null => Err(DbError::sql("window frame offset cannot be NULL")), - other => Err(DbError::sql(format!( - "window frame offset must be INT64, got {other:?}" - ))), - } +fn matching_simple_grouped_aggregate_binding<'a>( + expr: &Expr, + table_name: &str, + binding_name: &str, + aggregate_bindings: &'a [SimpleGroupedNumericAggregateBinding], +) -> Option<&'a SimpleGroupedNumericAggregateBinding> { + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return None; + }; + if !order_by.is_empty() || *within_group { + return None; } - - #[allow(clippy::too_many_arguments)] - fn eval_window_aggregate( - &self, - name: &str, - args: &[Expr], - distinct: bool, - star: bool, - dataset: &Dataset, - sorted_partition: &[usize], - frame_range: Option<(usize, usize)>, - params: &[Value], - ctes: &BTreeMap, - ) -> Result { - let aggregate_ctx = AggregateEvalContext { - runtime: self, - dataset, - params, - ctes, - }; - let empty_indexes: [usize; 0] = []; - let row_indexes = if let Some((start, end)) = frame_range { - sorted_partition - .get(start..=end) - .ok_or_else(|| DbError::internal("window frame range is invalid"))? - } else { - &empty_indexes - }; - - match name { - "count" => { - if star { - if distinct { - return Err(DbError::sql("COUNT(DISTINCT *) is not supported")); - } - return Ok(Value::Int64(row_indexes.len() as i64)); - } - if args.len() != 1 { - return Err(DbError::sql("COUNT expects exactly 1 argument")); + aggregate_bindings + .iter() + .find(|binding| match binding.kind { + SimpleGroupedNumericAggregateKind::CountRows => { + name.eq_ignore_ascii_case("count") && !*distinct && args.is_empty() && *star + } + SimpleGroupedNumericAggregateKind::CountNonNull => { + if !name.eq_ignore_ascii_case("count") || *distinct || *star || args.len() != 1 { + return false; } - if distinct { - let mut vals = Vec::new(); - for row_index in row_indexes { - let row = dataset - .rows - .get(*row_index) - .map(Vec::as_slice) - .ok_or_else(|| DbError::internal("window row index is invalid"))?; - let val = self.eval_expr(&args[0], dataset, row, params, ctes, None)?; - if !matches!(val, Value::Null) { - vals.push(val); - } - } - vals.sort_by(|a, b| compare_values(a, b).unwrap_or(std::cmp::Ordering::Equal)); - vals.dedup_by(|a, b| { - compare_values(a, b).unwrap_or(std::cmp::Ordering::Equal) - == std::cmp::Ordering::Equal - }); - Ok(Value::Int64(vals.len() as i64)) + if let Some(expected) = binding.source_expr.as_ref() { + expected == &args[0] + && expr_references_binding_names(&args[0], table_name, binding_name) } else { - let mut count = 0_i64; - for row_index in row_indexes { - let row = dataset - .rows - .get(*row_index) - .map(Vec::as_slice) - .ok_or_else(|| DbError::internal("window row index is invalid"))?; - if !matches!( - self.eval_expr(&args[0], dataset, row, params, ctes, None)?, - Value::Null - ) { - count += 1; + let Expr::Column { table, column } = &args[0] else { + return false; + }; + if let Some(table) = table.as_deref() { + if !identifiers_equal(table, table_name) + && !identifiers_equal(table, binding_name) + { + return false; } } - Ok(Value::Int64(count)) - } - } - "sum" => { - if star || args.len() != 1 { - return Err(DbError::sql("SUM expects exactly 1 argument")); - } - aggregate_numeric( - &aggregate_ctx, - row_indexes, - &args[0], - NumericAgg::Sum, - distinct, - ) - } - "avg" => { - if star || args.len() != 1 { - return Err(DbError::sql("AVG expects exactly 1 argument")); - } - aggregate_numeric( - &aggregate_ctx, - row_indexes, - &args[0], - NumericAgg::Avg, - distinct, - ) - } - "total" => { - if star || args.len() != 1 { - return Err(DbError::sql("TOTAL expects exactly 1 argument")); - } - aggregate_numeric( - &aggregate_ctx, - row_indexes, - &args[0], - NumericAgg::Total, - distinct, - ) - } - "stddev" | "stddev_samp" => { - if star || args.len() != 1 { - return Err(DbError::sql("STDDEV expects exactly 1 argument")); - } - aggregate_variance( - &aggregate_ctx, - row_indexes, - &args[0], - VarianceAgg::StddevSamp, - distinct, - ) - } - "stddev_pop" => { - if star || args.len() != 1 { - return Err(DbError::sql("STDDEV_POP expects exactly 1 argument")); - } - aggregate_variance( - &aggregate_ctx, - row_indexes, - &args[0], - VarianceAgg::StddevPop, - distinct, - ) - } - "variance" | "var_samp" => { - if star || args.len() != 1 { - return Err(DbError::sql("VAR_SAMP expects exactly 1 argument")); - } - aggregate_variance( - &aggregate_ctx, - row_indexes, - &args[0], - VarianceAgg::VarSamp, - distinct, - ) - } - "var_pop" => { - if star || args.len() != 1 { - return Err(DbError::sql("VAR_POP expects exactly 1 argument")); + binding + .source_column_name + .as_deref() + .is_some_and(|expected| identifiers_equal(column, expected)) } - aggregate_variance( - &aggregate_ctx, - row_indexes, - &args[0], - VarianceAgg::VarPop, - distinct, - ) } - "bool_and" => { - if star || args.len() != 1 { - return Err(DbError::sql("BOOL_AND expects exactly 1 argument")); + SimpleGroupedNumericAggregateKind::CountDistinct => { + if !name.eq_ignore_ascii_case("count") || !*distinct || *star || args.len() != 1 { + return false; } - aggregate_bool( - &aggregate_ctx, - row_indexes, - &args[0], - BoolAgg::And, - distinct, - ) - } - "bool_or" => { - if star || args.len() != 1 { - return Err(DbError::sql("BOOL_OR expects exactly 1 argument")); + if let Some(expected) = binding.source_expr.as_ref() { + expected == &args[0] + && expr_references_binding_names(&args[0], table_name, binding_name) + } else { + let Expr::Column { table, column } = &args[0] else { + return false; + }; + if let Some(table) = table.as_deref() { + if !identifiers_equal(table, table_name) + && !identifiers_equal(table, binding_name) + { + return false; + } + } + binding + .source_column_name + .as_deref() + .is_some_and(|expected| identifiers_equal(column, expected)) } - aggregate_bool(&aggregate_ctx, row_indexes, &args[0], BoolAgg::Or, distinct) } - "min" => { - if star || args.len() != 1 { - return Err(DbError::sql("MIN expects exactly 1 argument")); + SimpleGroupedNumericAggregateKind::Sum + | SimpleGroupedNumericAggregateKind::SumDistinct + | SimpleGroupedNumericAggregateKind::Avg + | SimpleGroupedNumericAggregateKind::AvgDistinct + | SimpleGroupedNumericAggregateKind::Total + | SimpleGroupedNumericAggregateKind::TotalDistinct + | SimpleGroupedNumericAggregateKind::StddevSamp + | SimpleGroupedNumericAggregateKind::StddevSampDistinct + | SimpleGroupedNumericAggregateKind::StddevPop + | SimpleGroupedNumericAggregateKind::StddevPopDistinct + | SimpleGroupedNumericAggregateKind::VarSamp + | SimpleGroupedNumericAggregateKind::VarSampDistinct + | SimpleGroupedNumericAggregateKind::VarPop + | SimpleGroupedNumericAggregateKind::VarPopDistinct + | SimpleGroupedNumericAggregateKind::BoolAnd + | SimpleGroupedNumericAggregateKind::BoolAndDistinct + | SimpleGroupedNumericAggregateKind::BoolOr + | SimpleGroupedNumericAggregateKind::BoolOrDistinct => { + let expected_distinct = binding.kind.uses_distinct(); + if !binding.kind.matches_aggregate_name(name) + || *distinct != expected_distinct + || *star + || args.len() != 1 + { + return false; } - aggregate_extreme(self, dataset, row_indexes, &args[0], params, ctes, true) - } - "max" => { - if star || args.len() != 1 { - return Err(DbError::sql("MAX expects exactly 1 argument")); + if let Some(expected) = binding.source_expr.as_ref() { + expected == &args[0] + && expr_references_binding_names(&args[0], table_name, binding_name) + } else { + let Expr::Column { table, column } = &args[0] else { + return false; + }; + if let Some(table) = table.as_deref() { + if !identifiers_equal(table, table_name) + && !identifiers_equal(table, binding_name) + { + return false; + } + } + binding + .source_column_name + .as_deref() + .is_some_and(|expected| identifiers_equal(column, expected)) } - aggregate_extreme(self, dataset, row_indexes, &args[0], params, ctes, false) } - name @ ("group_concat" | "string_agg") => { - if star { - return Err(DbError::sql(format!( - "{} does not support *", - name.to_ascii_uppercase() - ))); - } - if distinct { - return Err(DbError::sql(format!( - "{} DISTINCT is not supported in window context", - name.to_ascii_uppercase() - ))); + SimpleGroupedNumericAggregateKind::Min | SimpleGroupedNumericAggregateKind::Max => { + if !binding.kind.matches_aggregate_name(name) || *star || args.len() != 1 { + return false; } - aggregate_group_concat(&aggregate_ctx, row_indexes, args, false, &[], name) + binding.source_expr.as_ref().is_some_and(|expected| { + expected == &args[0] + && expr_references_binding_names(&args[0], table_name, binding_name) + }) } - other => Err(DbError::sql(format!( - "unsupported aggregate window function {other}" - ))), - } + }) +} + +fn from_item_is_all_inner_table_joins(item: &FromItem) -> bool { + match item { + FromItem::Table { .. } => true, + FromItem::Join { + left, + right, + kind: JoinKind::Inner, + constraint: JoinConstraint::On(_), + } => from_item_is_all_inner_table_joins(left) && from_item_is_all_inner_table_joins(right), + _ => false, + } +} + +fn merged_single_column_using_expr(left_binding: &str, right_binding: &str, column: &str) -> Expr { + Expr::Function { + name: "coalesce".to_string(), + args: vec![ + Expr::Column { + table: Some(left_binding.to_string()), + column: column.to_string(), + }, + Expr::Column { + table: Some(right_binding.to_string()), + column: column.to_string(), + }, + ], } +} - fn evaluate_grouped_select( - &self, - select: &Select, - dataset: Dataset, - params: &[Value], - ctes: &BTreeMap, - ) -> Result { - if select.group_by.iter().any(expr_contains_collation) { - return Err(DbError::sql( - "COLLATE in GROUP BY keys is not supported in this compatibility slice", - )); - } - let mut groups = BTreeMap::, Vec>::new(); - if dataset.rows.is_empty() && select.group_by.is_empty() { - groups.insert(Vec::new(), Vec::new()); - } else { - for (row_index, row) in dataset.rows.iter().enumerate() { - let key_values = select - .group_by - .iter() - .map(|expr| self.eval_expr(expr, &dataset, row, params, ctes, None)) - .collect::>>()?; - groups - .entry(row_identity(&key_values)?) - .or_default() - .push(row_index); - } - } - let columns = select - .projection - .iter() - .enumerate() - .map(|(index, item)| match item { - SelectItem::Expr { expr, alias } => ColumnBinding::visible( +type SimpleJoinHashRows = BTreeMap, Vec<(i64, Vec)>>; + +fn try_project_simple_select_items( + dataset: &Dataset, + items: &[SelectItem], +) -> Result> { + let mut output_columns = Vec::new(); + let mut projection_plan = Vec::::new(); + for (index, item) in items.iter().enumerate() { + match item { + SelectItem::Expr { expr, alias } => { + let Expr::Column { table, column } = expr else { + return Ok(None); + }; + let Some(source_index) = + simple_select_item_column_index(dataset, table.as_deref(), column) + else { + return Ok(None); + }; + projection_plan.push(source_index); + output_columns.push(ColumnBinding::visible( None, alias .clone() .unwrap_or_else(|| infer_expr_name(expr, index + 1)), - ), - SelectItem::Wildcard => ColumnBinding::visible(None, format!("col{}", index + 1)), - SelectItem::QualifiedWildcard(_) => { - ColumnBinding::visible(None, format!("col{}", index + 1)) - } - }) - .collect::>(); - let mut rows = Vec::new(); - for group_row_indexes in groups.into_values() { - if let Some(having) = &select.having { - if !matches!( - self.eval_group_expr(having, &dataset, &group_row_indexes, params, ctes)?, - Value::Bool(true) - ) { - continue; + )); + } + SelectItem::Wildcard => { + for (source_index, binding) in dataset.columns.iter().enumerate() { + if binding.hidden { + continue; + } + projection_plan.push(source_index); + output_columns.push(binding.as_output()); } } - let mut output = Vec::new(); - for item in &select.projection { - match item { - SelectItem::Expr { expr, .. } => output.push(self.eval_group_expr( - expr, - &dataset, - &group_row_indexes, - params, - ctes, - )?), - SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => { - return Err(DbError::sql( - "wildcards are not supported in grouped SELECT output", - )) + SelectItem::QualifiedWildcard(table) => { + let mut matched = false; + for (source_index, binding) in dataset.columns.iter().enumerate() { + if binding.hidden || binding.table.as_deref() != Some(table.as_str()) { + continue; } + projection_plan.push(source_index); + output_columns.push(binding.as_output()); + matched = true; + } + if !matched { + return Ok(None); } } - rows.push(output); } - Ok(Dataset::with_rows(columns, rows)) } - fn sort_dataset( - &self, - dataset: &mut Dataset, - order_by: &[crate::sql::ast::OrderBy], - params: &[Value], - ctes: &BTreeMap, - ) -> Result<()> { - if order_by.is_empty() || dataset.rows.len() <= 1 { - return Ok(()); + let mut output_rows = Vec::with_capacity(dataset.rows.len()); + for row in dataset.rows.iter() { + let mut output_row = Vec::with_capacity(projection_plan.len()); + for source_index in &projection_plan { + let value = row + .get(*source_index) + .ok_or_else(|| DbError::internal("projection source index exceeds row width"))?; + output_row.push(value.clone()); } - let eval_dataset = Dataset::with_rows(dataset.columns.clone(), Vec::new()); - let projected_order_indexes = order_by - .iter() - .map(|order| projected_dataset_order_column_index(dataset, &order.expr)) - .collect::>(); - let sort_keys = dataset - .rows - .iter() - .map(|row| { - order_by - .iter() - .zip(&projected_order_indexes) - .map(|(order, projected_index)| { - if let Some(index) = projected_index { - row.get(*index).cloned().unwrap_or(Value::Null) - } else { - self.eval_expr(&order.expr, &eval_dataset, row, params, ctes, None) - .unwrap_or(Value::Null) - } - }) - .collect::>() - }) - .collect::>(); + output_rows.push(output_row); + } + Ok(Some(Dataset::with_rows(output_columns, output_rows))) +} - let mut sort_error = None; - let mut order = (0..dataset.rows.len()).collect::>(); - order.sort_by(|left_index, right_index| { - let left_key = &sort_keys[*left_index]; - let right_key = &sort_keys[*right_index]; - for (order_clause, (left_value, right_value)) in - order_by.iter().zip(left_key.iter().zip(right_key.iter())) - { - let ordering = match compare_values_with_runtime_collation( - Some(self), - left_value, - right_value, - order_clause.collation.clone(), - ) { - Ok(ordering) => ordering, - Err(error) => { - if sort_error.is_none() { - sort_error = Some(error); - } - std::cmp::Ordering::Equal - } - }; - if ordering != std::cmp::Ordering::Equal { - return if order_clause.descending { - ordering.reverse() - } else { - ordering - }; - } - } - left_index.cmp(right_index) - }); - if let Some(error) = sort_error { - return Err(error); - } +#[derive(Clone, Copy, Debug)] +pub(crate) struct SimpleTrigramLookup<'a> { + table_qualifier: Option<&'a str>, + column_name: &'a str, + pattern_expr: &'a Expr, + has_additional_filter: bool, +} - let mut rows = dataset - .take_rows() - .into_iter() - .map(Some) - .collect::>(); - dataset.set_rows( - order - .into_iter() - .map(|row_index| { - rows.get_mut(row_index) - .and_then(Option::take) - .ok_or_else(|| DbError::internal("sorted row index is invalid")) - }) - .collect::>>()?, - ); - Ok(()) - } +#[derive(Clone, Copy, Debug)] +pub(crate) struct SimpleFullTextLookup<'a> { + index_name_expr: &'a Expr, + query_expr: &'a Expr, +} - fn eval_constant_i64( - &self, - expr: &Expr, - params: &[Value], - ctes: &BTreeMap, - ) -> Result { - match self.eval_expr(expr, &Dataset::empty(), &[], params, ctes, None)? { - Value::Int64(value) => Ok(value), - other => Err(DbError::sql(format!( - "expected integer constant, got {other:?}" - ))), - } +pub(crate) fn exact_fulltext_lookup(filter: Option<&Expr>) -> Option> { + let Some(Expr::Function { name, args }) = filter else { + return None; + }; + if !name.eq_ignore_ascii_case("fulltext_match") || args.len() != 2 { + return None; } + Some(SimpleFullTextLookup { + index_name_expr: &args[0], + query_expr: &args[1], + }) +} - fn eval_group_membership_value( - &self, - expr: &Expr, - dataset: &Dataset, - group_row_indexes: &[usize], - params: &[Value], - ctes: &BTreeMap, - ) -> Result { - match expr { - Expr::Row(items) => Ok(MembershipValue::Row( - items +#[derive(Clone, Copy, Debug)] +pub(crate) struct SimpleSpatialLookup<'a> { + table_qualifier: Option<&'a str>, + column_name: &'a str, + value_expr: &'a Expr, + radius_expr: Option<&'a Expr>, +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct SimpleSpatialJoinPredicate<'a> { + left: QualifiedColumnRef<'a>, + right: QualifiedColumnRef<'a>, + radius_expr: Option<&'a Expr>, +} + +fn qualified_column_ref_expr(expr: &Expr) -> Option> { + let Expr::Column { table, column } = expr else { + return None; + }; + Some(QualifiedColumnRef { + table: table.as_deref(), + column, + }) +} + +fn expr_has_column_ref(expr: &Expr) -> bool { + match expr { + Expr::Column { .. } => true, + Expr::Unary { expr, .. } + | Expr::Cast { expr, .. } + | Expr::IsNull { expr, .. } + | Expr::Collate { expr, .. } => expr_has_column_ref(expr), + Expr::Binary { left, right, .. } => expr_has_column_ref(left) || expr_has_column_ref(right), + Expr::Between { + expr, low, high, .. + } => expr_has_column_ref(expr) || expr_has_column_ref(low) || expr_has_column_ref(high), + Expr::InList { expr, items, .. } => { + expr_has_column_ref(expr) || items.iter().any(expr_has_column_ref) + } + Expr::Like { + expr, + pattern, + escape, + .. + } => { + expr_has_column_ref(expr) + || expr_has_column_ref(pattern) + || escape.as_deref().is_some_and(expr_has_column_ref) + } + Expr::Function { args, .. } => args.iter().any(expr_has_column_ref), + Expr::Aggregate { args, order_by, .. } => { + args.iter().any(expr_has_column_ref) + || order_by .iter() - .map(|item| { - self.eval_group_expr(item, dataset, group_row_indexes, params, ctes) - }) - .collect::>>()?, - )), - _ => Ok(MembershipValue::Scalar(self.eval_group_expr( - expr, - dataset, - group_row_indexes, - params, - ctes, - )?)), + .any(|order| expr_has_column_ref(&order.expr)) + } + Expr::Case { + operand, + branches, + else_expr, + } => { + operand.as_deref().is_some_and(expr_has_column_ref) + || branches.iter().any(|(condition, value)| { + expr_has_column_ref(condition) || expr_has_column_ref(value) + }) + || else_expr.as_deref().is_some_and(expr_has_column_ref) + } + Expr::Row(items) => items.iter().any(expr_has_column_ref), + Expr::InSubquery { expr, .. } | Expr::CompareSubquery { expr, .. } => { + expr_has_column_ref(expr) } + Expr::Literal(_) + | Expr::Parameter(_) + | Expr::RowNumber { .. } + | Expr::WindowFunction { .. } + | Expr::ScalarSubquery(_) + | Expr::Exists(_) => false, } +} - fn eval_membership_value( - &self, - expr: &Expr, - dataset: &Dataset, - row: &[Value], - params: &[Value], - ctes: &BTreeMap, - excluded: Option<&Dataset>, - ) -> Result { - match expr { - Expr::Row(items) => Ok(MembershipValue::Row( - items - .iter() - .map(|item| self.eval_expr(item, dataset, row, params, ctes, excluded)) - .collect::>>()?, - )), - _ => Ok(MembershipValue::Scalar( - self.eval_expr(expr, dataset, row, params, ctes, excluded)?, - )), +fn expr_contains_aggregate(expr: &Expr) -> bool { + match expr { + Expr::Aggregate { .. } => true, + Expr::Unary { expr, .. } | Expr::Collate { expr, .. } => expr_contains_aggregate(expr), + Expr::Binary { left, right, .. } => { + expr_contains_aggregate(left) || expr_contains_aggregate(right) + } + Expr::Between { + expr, low, high, .. + } => { + expr_contains_aggregate(expr) + || expr_contains_aggregate(low) + || expr_contains_aggregate(high) + } + Expr::InList { expr, items, .. } => { + expr_contains_aggregate(expr) || items.iter().any(expr_contains_aggregate) + } + Expr::InSubquery { expr, .. } => expr_contains_aggregate(expr), + Expr::CompareSubquery { expr, .. } => expr_contains_aggregate(expr), + Expr::ScalarSubquery(_) | Expr::Exists(_) => false, + Expr::Like { + expr, + pattern, + escape, + .. + } => { + expr_contains_aggregate(expr) + || expr_contains_aggregate(pattern) + || escape.as_deref().is_some_and(expr_contains_aggregate) + } + Expr::IsNull { expr, .. } => expr_contains_aggregate(expr), + Expr::Function { args, .. } => args.iter().any(expr_contains_aggregate), + Expr::RowNumber { .. } | Expr::WindowFunction { .. } => false, + Expr::Case { + operand, + branches, + else_expr, + } => { + operand.as_deref().is_some_and(expr_contains_aggregate) + || branches.iter().any(|(left, right)| { + expr_contains_aggregate(left) || expr_contains_aggregate(right) + }) + || else_expr.as_deref().is_some_and(expr_contains_aggregate) } + Expr::Row(items) => items.iter().any(expr_contains_aggregate), + Expr::Cast { expr, .. } => expr_contains_aggregate(expr), + Expr::Literal(_) | Expr::Column { .. } | Expr::Parameter(_) => false, } +} - fn eval_group_expr( - &self, - expr: &Expr, - dataset: &Dataset, - group_row_indexes: &[usize], - params: &[Value], - ctes: &BTreeMap, - ) -> Result { - let aggregate_ctx = AggregateEvalContext { - runtime: self, - dataset, - params, - ctes, - }; - match expr { - Expr::Aggregate { - name, - args, - star, - distinct, - order_by, - within_group, - } => match name.as_str() { - "array_agg" | "median" | "percentile_cont" | "percentile_disc" => { - match name.as_str() { - "array_agg" => { - if *within_group { - return Err(DbError::sql( - "ARRAY_AGG does not support WITHIN GROUP", - )); - } - if *star || args.len() != 1 { - return Err(DbError::sql("ARRAY_AGG expects exactly 1 argument")); - } - aggregate_array_agg( - &aggregate_ctx, - group_row_indexes, - &args[0], - *distinct, - order_by, - ) - } - "median" => { - if *within_group { - return Err(DbError::sql( - "MEDIAN does not support WITHIN GROUP; use MEDIAN(expr)", - )); - } - if !order_by.is_empty() { - return Err(DbError::sql( - "MEDIAN does not support aggregate ORDER BY", - )); - } - if *star || args.len() != 1 { - return Err(DbError::sql("MEDIAN expects exactly 1 argument")); - } - aggregate_median(&aggregate_ctx, group_row_indexes, &args[0], *distinct) - } - "percentile_cont" => { - if !*within_group { - return Err(DbError::sql( - "PERCENTILE_CONT requires WITHIN GROUP (ORDER BY ...)", - )); - } - if *distinct { - return Err(DbError::sql( - "PERCENTILE_CONT does not support DISTINCT", - )); - } - if *star || args.len() != 1 { - return Err(DbError::sql( - "PERCENTILE_CONT expects exactly 1 argument", - )); - } - aggregate_percentile_cont( - self, - dataset, - group_row_indexes, - &args[0], - order_by, - params, - ctes, - ) - } - "percentile_disc" => { - if !*within_group { - return Err(DbError::sql( - "PERCENTILE_DISC requires WITHIN GROUP (ORDER BY ...)", - )); - } - if *distinct { - return Err(DbError::sql( - "PERCENTILE_DISC does not support DISTINCT", - )); - } - if *star || args.len() != 1 { - return Err(DbError::sql( - "PERCENTILE_DISC expects exactly 1 argument", - )); - } - aggregate_percentile_disc( - self, - dataset, - group_row_indexes, - &args[0], - order_by, - params, - ctes, - ) - } - _ => Err(DbError::sql(format!( - "unsupported aggregate function {}", - name.to_ascii_uppercase() - ))), - } - } - name if *within_group => Err(DbError::sql(format!( - "{} does not support WITHIN GROUP", - name.to_ascii_uppercase() - ))), - name if !order_by.is_empty() && !matches!(name, "group_concat" | "string_agg") => { - Err(DbError::sql(format!( - "{} does not support aggregate ORDER BY", - name.to_ascii_uppercase() - ))) - } - "count" => { - if *star { - Ok(Value::Int64(group_row_indexes.len() as i64)) - } else if *distinct { - let mut vals = Vec::new(); - for row_index in group_row_indexes { - let row = - dataset.rows.get(*row_index).map(Vec::as_slice).ok_or_else( - || DbError::internal("group row index is invalid"), - )?; - let val = self.eval_expr(&args[0], dataset, row, params, ctes, None)?; - if !matches!(val, Value::Null) { - vals.push(val); - } - } - vals.sort_by(|a, b| { - compare_values(a, b).unwrap_or(std::cmp::Ordering::Equal) - }); - vals.dedup_by(|a, b| { - compare_values(a, b).unwrap_or(std::cmp::Ordering::Equal) - == std::cmp::Ordering::Equal - }); - Ok(Value::Int64(vals.len() as i64)) +fn expr_contains_runtime_extension_aggregate(runtime: &EngineRuntime, expr: &Expr) -> Result { + Ok(match expr { + Expr::Aggregate { .. } => true, + Expr::Function { name, args } => { + crate::extensions::runtime_has_aggregate_function(runtime, name)? + || args.iter().try_fold(false, |found, arg| { + if found { + Ok(true) } else { - let mut count = 0_i64; - for row_index in group_row_indexes { - let row = - dataset.rows.get(*row_index).map(Vec::as_slice).ok_or_else( - || DbError::internal("group row index is invalid"), - )?; - if !matches!( - self.eval_expr(&args[0], dataset, row, params, ctes, None)?, - Value::Null - ) { - count += 1; - } - } - Ok(Value::Int64(count)) - } - } - "sum" => aggregate_numeric( - &aggregate_ctx, - group_row_indexes, - &args[0], - NumericAgg::Sum, - *distinct, - ), - "avg" => aggregate_numeric( - &aggregate_ctx, - group_row_indexes, - &args[0], - NumericAgg::Avg, - *distinct, - ), - "total" => aggregate_numeric( - &aggregate_ctx, - group_row_indexes, - &args[0], - NumericAgg::Total, - *distinct, - ), - "stddev" | "stddev_samp" => aggregate_variance( - &aggregate_ctx, - group_row_indexes, - &args[0], - VarianceAgg::StddevSamp, - *distinct, - ), - "stddev_pop" => aggregate_variance( - &aggregate_ctx, - group_row_indexes, - &args[0], - VarianceAgg::StddevPop, - *distinct, - ), - "variance" | "var_samp" => aggregate_variance( - &aggregate_ctx, - group_row_indexes, - &args[0], - VarianceAgg::VarSamp, - *distinct, - ), - "var_pop" => aggregate_variance( - &aggregate_ctx, - group_row_indexes, - &args[0], - VarianceAgg::VarPop, - *distinct, - ), - "bool_and" => aggregate_bool( - &aggregate_ctx, - group_row_indexes, - &args[0], - BoolAgg::And, - *distinct, - ), - "bool_or" => aggregate_bool( - &aggregate_ctx, - group_row_indexes, - &args[0], - BoolAgg::Or, - *distinct, - ), - "min" => aggregate_extreme( - self, - dataset, - group_row_indexes, - &args[0], - params, - ctes, - true, - ), - "max" => aggregate_extreme( - self, - dataset, - group_row_indexes, - &args[0], - params, - ctes, - false, - ), - name @ ("group_concat" | "string_agg") => aggregate_group_concat( - &aggregate_ctx, - group_row_indexes, - args, - *distinct, - order_by, - name, - ), - other => { - let mut arg_rows = Vec::with_capacity(group_row_indexes.len()); - for row_index in group_row_indexes { - let row = dataset - .rows - .get(*row_index) - .map(Vec::as_slice) - .ok_or_else(|| DbError::internal("group row index is invalid"))?; - let values = args - .iter() - .map(|arg| self.eval_expr(arg, dataset, row, params, ctes, None)) - .collect::>>()?; - arg_rows.push(values); - } - if let Some(value) = - crate::extensions::invoke_aggregate_from_runtime(self, other, arg_rows)? - { - return Ok(value); - } - Err(DbError::sql(format!( - "unsupported aggregate function {other}" - ))) - } - }, - Expr::Unary { op, expr } => { - let value = self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes)?; - match op { - UnaryOp::Not => Ok(match truthy(&value) { - Some(value) => Value::Bool(!value), - None => Value::Null, - }), - UnaryOp::Negate => match value { - Value::Int64(value) => Ok(Value::Int64(-value)), - Value::Float64(value) => Ok(Value::Float64(-value)), - Value::Null => Ok(Value::Null), - other => Err(DbError::sql(format!("cannot negate {other:?}"))), - }, - } - } - Expr::Binary { left, op, right } => { - let collation = expr_collation(left).or_else(|| expr_collation(right)); - eval_binary_with_collation( - Some(self), - op, - self.eval_group_expr(left, dataset, group_row_indexes, params, ctes)?, - self.eval_group_expr(right, dataset, group_row_indexes, params, ctes)?, - collation, - ) - } - Expr::Between { - expr, - low, - high, - negated, - } => { - let value = self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes)?; - let low = self.eval_group_expr(low, dataset, group_row_indexes, params, ctes)?; - let high = self.eval_group_expr(high, dataset, group_row_indexes, params, ctes)?; - if matches!(value, Value::Null) - || matches!(low, Value::Null) - || matches!(high, Value::Null) - { - return Ok(Value::Null); - } - let collation = expr_collation(expr); - let in_range = compare_values_with_runtime_collation( - Some(self), - &value, - &low, - collation.clone(), - )? != std::cmp::Ordering::Less - && compare_values_with_runtime_collation(Some(self), &value, &high, collation)? - != std::cmp::Ordering::Greater; - Ok(Value::Bool(if *negated { !in_range } else { in_range })) - } - Expr::InList { - expr, - items, - negated, - } => { - let value = self.eval_group_membership_value( - expr, - dataset, - group_row_indexes, - params, - ctes, - )?; - if membership_value_has_nulls(&value) { - return Ok(Value::Null); - } - let mut saw_null = false; - for item in items { - let candidate = self.eval_group_membership_value( - item, - dataset, - group_row_indexes, - params, - ctes, - )?; - match compare_membership_values(&value, &candidate)? { - Some(true) => return Ok(Value::Bool(!*negated)), - Some(false) => {} - None => saw_null = true, - } - } - if saw_null { - Ok(Value::Null) - } else { - Ok(Value::Bool(*negated)) - } - } - Expr::Like { - expr, - pattern, - escape, - case_insensitive, - negated, - .. - } => { - let left = self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes)?; - let right = - self.eval_group_expr(pattern, dataset, group_row_indexes, params, ctes)?; - let escape = escape - .as_ref() - .map(|expr| { - self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes) - }) - .transpose()?; - eval_like(left, right, escape, *case_insensitive, *negated) - } - Expr::IsNull { expr, negated } => { - let is_null = matches!( - self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes)?, - Value::Null - ); - Ok(Value::Bool(if *negated { !is_null } else { is_null })) - } - Expr::Function { name, args } => { - let args_contain_aggregate = args.iter().any(expr_contains_aggregate) - || args.iter().try_fold(false, |found, arg| { - if found { - Ok(true) - } else { - expr_contains_runtime_extension_aggregate(self, arg) - } - })?; - if !args_contain_aggregate { - let mut arg_rows = Vec::with_capacity(group_row_indexes.len()); - for row_index in group_row_indexes { - let row = dataset - .rows - .get(*row_index) - .map(Vec::as_slice) - .ok_or_else(|| DbError::internal("group row index is invalid"))?; - let values = args - .iter() - .map(|arg| self.eval_expr(arg, dataset, row, params, ctes, None)) - .collect::>>()?; - arg_rows.push(values); - } - if let Some(value) = - crate::extensions::invoke_aggregate_from_runtime(self, name, arg_rows)? - { - return Ok(value); - } - } - let row = if let Some(row_index) = group_row_indexes.first().copied() { - dataset - .rows - .get(row_index) - .map(Vec::as_slice) - .ok_or_else(|| DbError::internal("group row index is invalid"))? - } else { - &[] - }; - let values = args - .iter() - .map(|arg| self.eval_group_expr(arg, dataset, group_row_indexes, params, ctes)) - .collect::>>()?; - match name.as_str() { - "coalesce" => Ok(values - .into_iter() - .find(|value| !matches!(value, Value::Null)) - .unwrap_or(Value::Null)), - "nullif" => { - if values.len() != 2 { - return Err(DbError::sql("NULLIF expects exactly two arguments")); - } - if compare_values(&values[0], &values[1])? == std::cmp::Ordering::Equal { - Ok(Value::Null) - } else { - Ok(values[0].clone()) - } + expr_contains_runtime_extension_aggregate(runtime, arg) } - "length" => unary_text_fn(values, |value| value.len().to_string()) - .and_then(|value| cast_value(value, crate::catalog::ColumnType::Int64)), - "lower" => unary_text_fn(values, |value| value.to_ascii_lowercase()), - "upper" => unary_text_fn(values, |value| value.to_ascii_uppercase()), - "trim" => unary_text_fn(values, |value| value.trim().to_string()), - other => self.eval_expr( - &Expr::Function { - name: other.to_string(), - args: args.to_vec(), - }, - dataset, - row, - params, - ctes, - None, - ), - } - } - Expr::Case { - operand, - branches, - else_expr, - } => { - let operand_value = operand - .as_deref() - .map(|expr| { - self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes) - }) - .transpose()?; - for (condition, result) in branches { - let matches = if let Some(operand_value) = &operand_value { - compare_values( - operand_value, - &self.eval_group_expr( - condition, - dataset, - group_row_indexes, - params, - ctes, - )?, - )? == std::cmp::Ordering::Equal + })? + } + Expr::Unary { expr, .. } | Expr::Collate { expr, .. } | Expr::Cast { expr, .. } => { + expr_contains_runtime_extension_aggregate(runtime, expr)? + } + Expr::Binary { left, right, .. } => { + expr_contains_runtime_extension_aggregate(runtime, left)? + || expr_contains_runtime_extension_aggregate(runtime, right)? + } + Expr::Between { + expr, low, high, .. + } => { + expr_contains_runtime_extension_aggregate(runtime, expr)? + || expr_contains_runtime_extension_aggregate(runtime, low)? + || expr_contains_runtime_extension_aggregate(runtime, high)? + } + Expr::InList { expr, items, .. } => { + expr_contains_runtime_extension_aggregate(runtime, expr)? + || items.iter().try_fold(false, |found, item| { + if found { + Ok(true) } else { - matches!( - self.eval_group_expr( - condition, - dataset, - group_row_indexes, - params, - ctes, - )?, - Value::Bool(true) - ) - }; - if matches { - return self.eval_group_expr( - result, - dataset, - group_row_indexes, - params, - ctes, - ); + expr_contains_runtime_extension_aggregate(runtime, item) } - } - else_expr + })? + } + Expr::InSubquery { expr, .. } | Expr::CompareSubquery { expr, .. } => { + expr_contains_runtime_extension_aggregate(runtime, expr)? + } + Expr::Like { + expr, + pattern, + escape, + .. + } => { + expr_contains_runtime_extension_aggregate(runtime, expr)? + || expr_contains_runtime_extension_aggregate(runtime, pattern)? + || escape .as_deref() - .map(|expr| { - self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes) - }) + .map(|expr| expr_contains_runtime_extension_aggregate(runtime, expr)) .transpose()? - .map_or(Ok(Value::Null), Ok) - } - Expr::Cast { expr, target_type } => cast_value( - self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes)?, - *target_type, - ), - Expr::Collate { expr, .. } => { - self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes) - } - Expr::Row(_) => Err(DbError::sql( - "row values are only supported in IN comparisons", - )), - Expr::RowNumber { .. } | Expr::WindowFunction { .. } => Err(DbError::sql( - "window functions cannot be nested inside grouped expressions", - )), - _ => { - let row = if let Some(row_index) = group_row_indexes.first().copied() { - dataset - .rows - .get(row_index) - .map(Vec::as_slice) - .ok_or_else(|| DbError::internal("group row index is invalid"))? - } else { - &[] - }; - self.eval_expr(expr, dataset, row, params, ctes, None) - } + .unwrap_or(false) } - } - - fn eval_expr( - &self, - expr: &Expr, - dataset: &Dataset, - row: &[Value], - params: &[Value], - ctes: &BTreeMap, - excluded: Option<&Dataset>, - ) -> Result { - match expr { - Expr::Literal(value) => Ok(value.clone()), - Expr::Column { table, column } => { - self.resolve_column(dataset, row, table.as_deref(), column, excluded) - } - Expr::Parameter(number) => params - .get(number.saturating_sub(1)) - .cloned() - .ok_or_else(|| DbError::sql(format!("missing value for parameter ${number}"))), - Expr::Unary { op, expr } => { - let value = self.eval_expr(expr, dataset, row, params, ctes, excluded)?; - match op { - UnaryOp::Not => Ok(match truthy(&value) { - Some(value) => Value::Bool(!value), - None => Value::Null, - }), - UnaryOp::Negate => match value { - Value::Int64(value) => Ok(Value::Int64(-value)), - Value::Float64(value) => Ok(Value::Float64(-value)), - Value::Null => Ok(Value::Null), - other => Err(DbError::sql(format!("cannot negate {other:?}"))), - }, - } - } - Expr::Binary { left, op, right } => { - let collation = expr_collation(left).or_else(|| expr_collation(right)); - let left = self.eval_expr(left, dataset, row, params, ctes, excluded)?; - let right = self.eval_expr(right, dataset, row, params, ctes, excluded)?; - eval_binary_with_collation(Some(self), op, left, right, collation) - } - Expr::Between { - expr, - low, - high, - negated, - } => { - let value = self.eval_expr(expr, dataset, row, params, ctes, excluded)?; - let low = self.eval_expr(low, dataset, row, params, ctes, excluded)?; - let high = self.eval_expr(high, dataset, row, params, ctes, excluded)?; - if matches!(value, Value::Null) - || matches!(low, Value::Null) - || matches!(high, Value::Null) - { - return Ok(Value::Null); - } - let collation = expr_collation(expr); - let in_range = compare_values_with_runtime_collation( - Some(self), - &value, - &low, - collation.clone(), - )? != std::cmp::Ordering::Less - && compare_values_with_runtime_collation(Some(self), &value, &high, collation)? - != std::cmp::Ordering::Greater; - Ok(Value::Bool(if *negated { !in_range } else { in_range })) - } - Expr::InList { - expr, - items, - negated, - } => { - let value = - self.eval_membership_value(expr, dataset, row, params, ctes, excluded)?; - if membership_value_has_nulls(&value) { - return Ok(Value::Null); - } - let mut saw_null = false; - for item in items { - let candidate = - self.eval_membership_value(item, dataset, row, params, ctes, excluded)?; - match compare_membership_values(&value, &candidate)? { - Some(true) => return Ok(Value::Bool(!*negated)), - Some(false) => {} - None => saw_null = true, - } - } - if saw_null { - Ok(Value::Null) - } else { - Ok(Value::Bool(*negated)) - } - } - Expr::InSubquery { - expr, - query, - negated, - } => { - let value = - self.eval_membership_value(expr, dataset, row, params, ctes, excluded)?; - if membership_value_has_nulls(&value) { - return Ok(Value::Null); - } - let subquery = self.evaluate_query_with_outer(query, params, ctes, dataset, row)?; - let expected_width = match &value { - MembershipValue::Scalar(_) => 1, - MembershipValue::Row(values) => values.len(), - }; - if subquery.columns.len() != expected_width { - return Err(DbError::sql(format!( - "IN subquery must return exactly {} column{}", - expected_width, - if expected_width == 1 { "" } else { "s" } - ))); - } - let mut saw_null = false; - for subquery_row in subquery.rows.iter() { - let candidate = if expected_width == 1 { - MembershipValue::Scalar( - subquery_row.first().cloned().unwrap_or(Value::Null), - ) - } else { - MembershipValue::Row(subquery_row.clone()) - }; - match compare_membership_values(&value, &candidate)? { - Some(true) => return Ok(Value::Bool(!*negated)), - Some(false) => {} - None => saw_null = true, - } - } - if saw_null { - Ok(Value::Null) - } else { - Ok(Value::Bool(*negated)) - } - } - Expr::CompareSubquery { - expr, - op, - quantifier, - query, - } => { - let left_value = self.eval_expr(expr, dataset, row, params, ctes, excluded)?; - let subquery = self.evaluate_query_with_outer(query, params, ctes, dataset, row)?; - if subquery.columns.len() != 1 { - return Err(DbError::sql( - "subquery comparison must return exactly one column", - )); - } - let mut saw_null = false; - let mut saw_row = false; - for subquery_row in subquery.rows.iter() { - saw_row = true; - let candidate = subquery_row.first().cloned().unwrap_or(Value::Null); - match eval_binary_with_collation( - Some(self), - op, - left_value.clone(), - candidate, - expr_collation(expr), - )? { - Value::Bool(result) => match quantifier { - SubqueryQuantifier::Any if result => return Ok(Value::Bool(true)), - SubqueryQuantifier::All if !result => return Ok(Value::Bool(false)), - _ => {} - }, - Value::Null => saw_null = true, - other => { - return Err(DbError::internal(format!( - "subquery comparison did not evaluate to boolean: {other:?}" - ))) - } - } - } - if !saw_row { - return Ok(Value::Bool(matches!(quantifier, SubqueryQuantifier::All))); - } - if saw_null { - Ok(Value::Null) - } else { - Ok(Value::Bool(matches!(quantifier, SubqueryQuantifier::All))) - } - } - Expr::ScalarSubquery(query) => { - let subquery = self.evaluate_query_with_outer(query, params, ctes, dataset, row)?; - if subquery.columns.len() != 1 { - return Err(DbError::sql( - "scalar subquery must return exactly one column", - )); - } - Ok(subquery - .rows - .first() - .and_then(|subquery_row| subquery_row.first()) - .cloned() - .unwrap_or(Value::Null)) - } - Expr::Exists(query) => Ok(Value::Bool( - !self - .evaluate_query_with_outer(query, params, ctes, dataset, row)? - .rows - .is_empty(), - )), - Expr::Like { - expr, - pattern, - escape, - case_insensitive, - negated, - .. - } => { - let left = self.eval_expr(expr, dataset, row, params, ctes, excluded)?; - let right = self.eval_expr(pattern, dataset, row, params, ctes, excluded)?; - let escape = escape - .as_ref() - .map(|expr| self.eval_expr(expr, dataset, row, params, ctes, excluded)) - .transpose()?; - eval_like(left, right, escape, *case_insensitive, *negated) - } - Expr::IsNull { expr, negated } => { - let is_null = matches!( - self.eval_expr(expr, dataset, row, params, ctes, excluded)?, - Value::Null - ); - Ok(Value::Bool(if *negated { !is_null } else { is_null })) - } - Expr::Function { name, args } => { - eval_function(self, name, args, dataset, row, params, ctes, excluded) - } - Expr::Aggregate { .. } => Err(DbError::sql( - "aggregate expressions require grouped evaluation", - )), - Expr::RowNumber { .. } | Expr::WindowFunction { .. } => Err(DbError::sql( - "window-function execution is not yet implemented", - )), - Expr::Case { - operand, - branches, - else_expr, - } => { - let operand_value = operand - .as_deref() - .map(|expr| self.eval_expr(expr, dataset, row, params, ctes, excluded)) - .transpose()?; - for (condition, result) in branches { - let matches = if let Some(operand_value) = &operand_value { - compare_values( - operand_value, - &self.eval_expr(condition, dataset, row, params, ctes, excluded)?, - )? == std::cmp::Ordering::Equal + Expr::IsNull { expr, .. } => expr_contains_runtime_extension_aggregate(runtime, expr)?, + Expr::Case { + operand, + branches, + else_expr, + } => { + operand + .as_deref() + .map(|expr| expr_contains_runtime_extension_aggregate(runtime, expr)) + .transpose()? + .unwrap_or(false) + || branches.iter().try_fold(false, |found, (left, right)| { + if found { + Ok(true) } else { - matches!( - self.eval_expr(condition, dataset, row, params, ctes, excluded)?, - Value::Bool(true) - ) - }; - if matches { - return self.eval_expr(result, dataset, row, params, ctes, excluded); + Ok(expr_contains_runtime_extension_aggregate(runtime, left)? + || expr_contains_runtime_extension_aggregate(runtime, right)?) } - } - else_expr + })? + || else_expr .as_deref() - .map(|expr| self.eval_expr(expr, dataset, row, params, ctes, excluded)) + .map(|expr| expr_contains_runtime_extension_aggregate(runtime, expr)) .transpose()? - .map_or(Ok(Value::Null), Ok) - } - Expr::Cast { expr, target_type } => cast_value( - self.eval_expr(expr, dataset, row, params, ctes, excluded)?, - *target_type, - ), - Expr::Collate { expr, .. } => { - self.eval_expr(expr, dataset, row, params, ctes, excluded) + .unwrap_or(false) + } + Expr::Row(items) => items.iter().try_fold(false, |found, item| { + if found { + Ok(true) + } else { + expr_contains_runtime_extension_aggregate(runtime, item) } - Expr::Row(_) => Err(DbError::sql( - "row values are only supported in IN comparisons", - )), + })?, + Expr::ScalarSubquery(_) | Expr::Exists(_) => false, + Expr::RowNumber { .. } + | Expr::WindowFunction { .. } + | Expr::Literal(_) + | Expr::Column { .. } + | Expr::Parameter(_) => false, + }) +} + +fn expr_contains_window(expr: &Expr) -> bool { + match expr { + Expr::RowNumber { .. } | Expr::WindowFunction { .. } => true, + Expr::Unary { expr, .. } + | Expr::Cast { expr, .. } + | Expr::IsNull { expr, .. } + | Expr::Collate { expr, .. } => expr_contains_window(expr), + Expr::Binary { left, right, .. } => { + expr_contains_window(left) || expr_contains_window(right) + } + Expr::Between { + expr, low, high, .. + } => expr_contains_window(expr) || expr_contains_window(low) || expr_contains_window(high), + Expr::InList { expr, items, .. } => { + expr_contains_window(expr) || items.iter().any(expr_contains_window) + } + Expr::Like { + expr, + pattern, + escape, + .. + } => { + expr_contains_window(expr) + || expr_contains_window(pattern) + || escape.as_ref().is_some_and(|e| expr_contains_window(e)) + } + Expr::Function { args, .. } => args.iter().any(expr_contains_window), + Expr::Case { + operand, + branches, + else_expr, + } => { + operand.as_ref().is_some_and(|e| expr_contains_window(e)) + || branches + .iter() + .any(|(c, v)| expr_contains_window(c) || expr_contains_window(v)) + || else_expr.as_ref().is_some_and(|e| expr_contains_window(e)) } + Expr::Row(items) => items.iter().any(expr_contains_window), + Expr::Aggregate { args, .. } => args.iter().any(expr_contains_window), + Expr::InSubquery { .. } + | Expr::CompareSubquery { .. } + | Expr::ScalarSubquery(_) + | Expr::Exists(_) => false, + Expr::Literal(_) | Expr::Column { .. } | Expr::Parameter(_) => false, } +} - fn resolve_column( - &self, - dataset: &Dataset, - row: &[Value], - table: Option<&str>, - column: &str, - excluded: Option<&Dataset>, - ) -> Result { - if let Some(table_name) = table { - if identifiers_equal(table_name, "excluded") { - let excluded = excluded.ok_or_else(|| { - DbError::sql("EXCLUDED is only valid in ON CONFLICT DO UPDATE") - })?; - return self.resolve_column( - excluded, - excluded.rows.first().map(Vec::as_slice).unwrap_or(&[]), - None, - column, - None, - ); - } +#[derive(Clone, Debug)] +pub(crate) struct JoinUsingColumn { + name: String, + left_index: usize, + right_index: usize, +} + +pub(crate) struct JoinEvalContext<'a> { + dataset: &'a Dataset, + runtime: &'a EngineRuntime, + params: &'a [Value], + ctes: &'a BTreeMap, +} + +fn visible_column_names(dataset: &Dataset) -> Vec { + let mut names = Vec::::new(); + for binding in dataset.columns.iter().filter(|binding| !binding.hidden) { + if !names + .iter() + .any(|name| identifiers_equal(name, &binding.name)) + { + names.push(binding.name.clone()); } - let mut matched_index = None; - for (index, binding) in dataset.columns.iter().enumerate() { - let visible_match = table.is_some() || !binding.hidden; - if !visible_match || !identifiers_equal(&binding.name, column) { - continue; - } - if table.is_some_and(|table| { - !binding - .table - .as_deref() - .is_some_and(|binding_table| identifiers_equal(binding_table, table)) - }) { - continue; - } - if matched_index.replace(index).is_some() { - return Err(DbError::sql(format!("ambiguous column reference {column}"))); + } + names +} + +fn visible_column_exists(dataset: &Dataset, column: &str) -> bool { + dataset + .columns + .iter() + .any(|binding| !binding.hidden && identifiers_equal(&binding.name, column)) +} + +fn resolve_visible_join_column( + dataset: &Dataset, + column: &str, + join_form: &str, + side: &str, +) -> Result { + let matches = dataset + .columns + .iter() + .enumerate() + .filter(|(_, binding)| !binding.hidden && identifiers_equal(&binding.name, column)) + .collect::>(); + match matches.as_slice() { + [single] => Ok(single.0), + [] => Err(DbError::sql(format!( + "{join_form} column {column} does not exist in {side} input" + ))), + _ => Err(DbError::sql(format!( + "{join_form} column {column} is ambiguous in {side} input" + ))), + } +} + +fn resolve_join_using_columns( + left: &Dataset, + right: &Dataset, + constraint: &JoinConstraint, +) -> Result> { + match constraint { + JoinConstraint::On(_) => Ok(Vec::new()), + JoinConstraint::Using(columns) => { + let mut pairs = Vec::with_capacity(columns.len()); + let mut seen = Vec::::new(); + for column in columns { + if seen + .iter() + .any(|existing| identifiers_equal(existing, column)) + { + return Err(DbError::sql(format!( + "JOIN USING column {column} specified more than once" + ))); + } + let left_index = resolve_visible_join_column(left, column, "JOIN USING", "left")?; + let right_index = + resolve_visible_join_column(right, column, "JOIN USING", "right")?; + pairs.push(JoinUsingColumn { + name: left.columns[left_index].name.clone(), + left_index, + right_index, + }); + seen.push(column.clone()); } + Ok(pairs) } - if let Some(index) = matched_index { - row.get(index) - .cloned() - .ok_or_else(|| DbError::internal("row is shorter than its bindings")) - } else { - Err(DbError::sql(format!("unknown column {column}"))) + JoinConstraint::Natural => { + let mut pairs = Vec::new(); + for column in visible_column_names(left) { + if !visible_column_exists(right, &column) { + continue; + } + let left_index = + resolve_visible_join_column(left, &column, "NATURAL JOIN", "left")?; + let right_index = + resolve_visible_join_column(right, &column, "NATURAL JOIN", "right")?; + pairs.push(JoinUsingColumn { + name: left.columns[left_index].name.clone(), + left_index, + right_index, + }); + } + Ok(pairs) } } +} - pub(super) fn apply_virtual_generated_columns( - &self, - table: &TableSchema, - row: &mut [Value], - ) -> Result<()> { - if generated_columns_are_stored(table) { - return Ok(()); - } - let mut base_values = row.to_vec(); - for (index, column) in table.columns.iter().enumerate() { - let Some(generated_sql) = &column.generated_sql else { - continue; - }; - if column.generated_stored { - base_values[index] = row - .get(index) - .cloned() - .ok_or_else(|| DbError::internal("row is shorter than table schema"))?; - continue; +fn resolve_join_using_columns_for_schemas( + left_columns: &[ColumnBinding], + right_columns: &[ColumnBinding], + constraint: &JoinConstraint, + _left_table: &TableSchema, + _right_table: &TableSchema, +) -> Result> { + match constraint { + JoinConstraint::Using(names) => { + let mut result = Vec::new(); + for name in names { + let left_idx = left_columns + .iter() + .position(|c| identifiers_equal(&c.name, name)) + .ok_or_else(|| { + DbError::sql(format!("column \"{name}\" not found in left table")) + })?; + let right_idx = right_columns + .iter() + .position(|c| identifiers_equal(&c.name, name)) + .ok_or_else(|| { + DbError::sql(format!("column \"{name}\" not found in right table")) + })?; + result.push(JoinUsingColumn { + name: left_columns[left_idx].name.clone(), + left_index: left_idx, + right_index: right_idx, + }); } - let expr = crate::sql::parser::parse_expression_sql(generated_sql)?; - let dataset = table_row_dataset(table, &base_values, &table.name); - let eval_row = dataset.rows.first().map(Vec::as_slice).unwrap_or(&[]); - let value = self.eval_expr(&expr, &dataset, eval_row, &[], &BTreeMap::new(), None)?; - let cast_value = self::constraints::coerce_column_value(column, value)?; - if let Some(slot) = row.get_mut(index) { - *slot = cast_value.clone(); - } else { - return Err(DbError::internal("row is shorter than table schema")); + Ok(result) + } + JoinConstraint::Natural => { + let mut result = Vec::new(); + for lc in left_columns.iter() { + if let Some(right_idx) = right_columns + .iter() + .position(|rc| identifiers_equal(&rc.name, &lc.name)) + { + let left_idx = left_columns + .iter() + .position(|c| identifiers_equal(&c.name, &lc.name)) + .ok_or_else(|| { + DbError::internal( + "internal: NATURAL join column is missing on left table", + ) + })?; + result.push(JoinUsingColumn { + name: left_columns[left_idx].name.clone(), + left_index: left_idx, + right_index: right_idx, + }); + } } - base_values[index] = cast_value; + Ok(result) } - Ok(()) + JoinConstraint::On(_) => Ok(Vec::new()), } } +impl EngineRuntime {} + pub(crate) fn statement_is_read_only(statement: &Statement) -> bool { matches!(statement, Statement::Query(_) | Statement::Explain(_)) } diff --git a/crates/decentdb/src/exec/paged_tables.rs b/crates/decentdb/src/exec/paged_tables.rs new file mode 100644 index 00000000..1b1960b9 --- /dev/null +++ b/crates/decentdb/src/exec/paged_tables.rs @@ -0,0 +1,2158 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +#[cfg(test)] +pub(crate) fn paged_row_append_plan_count() -> u64 { + PAGED_ROW_APPEND_PLAN_COUNT.with(std::cell::Cell::get) +} + +pub(crate) fn try_apply_paged_row_changes_to_manifest_update_only( + manifest: &TablePageManifest, + row_changes: &BTreeMap>>, +) -> Result> { + if row_changes.is_empty() { + return Ok(None); + } + + let mut planned_changes = Vec::with_capacity(row_changes.len()); + for (row_id, change) in row_changes { + let Some(next_values) = change.as_ref() else { + return Ok(None); + }; + let Some((_, entry)) = manifest.rows.entry_for_row_id(*row_id)? else { + return Ok(None); + }; + if entry.is_overlay { + return Ok(None); + } + let chunk_index = usize::try_from(entry.chunk_index).map_err(|_| { + DbError::corruption("paged table chunk index exceeded chunk list length") + })?; + planned_changes.push((*row_id, chunk_index, next_values.clone())); + } + + let mut updated_manifest = manifest.clone(); + Arc::make_mut(&mut updated_manifest.rows).sparse_mut()?; + let chunks = Arc::make_mut(&mut updated_manifest.chunks); + let tombstoned_row_ids = Arc::make_mut(&mut updated_manifest.tombstoned_row_ids); + + for (row_id, chunk_index, next_values) in planned_changes { + let chunk = chunks.get_mut(chunk_index).ok_or_else(|| { + DbError::corruption("paged table chunk index exceeded chunk list length") + })?; + if chunk.tombstoned_row_ids.contains(&row_id) { + return Ok(None); + } + + let overlay_payload = chunk.overlay_payload.get_or_insert_with(|| { + let mut payload = Vec::with_capacity(TABLE_PAYLOAD_MAGIC.len() + 4 + 128); + payload.extend_from_slice(TABLE_PAYLOAD_MAGIC); + payload.extend_from_slice(&0_u32.to_le_bytes()); + Arc::new(payload) + }); + let mut encoded_values = Vec::with_capacity(128); + Row::encode_values_into(&next_values, &mut encoded_values)?; + append_encoded_table_payload_row(Arc::make_mut(overlay_payload), row_id, &encoded_values)?; + Arc::make_mut(&mut chunk.tombstoned_row_ids).insert(row_id); + chunk.overlay_pointer = None; + chunk.overlay_checksum = None; + tombstoned_row_ids.insert(row_id); + } + + Ok(Some(updated_manifest)) +} + +pub(crate) fn patch_manifest_table_next_row_id( + payload: &mut [u8], + offset: usize, + next_row_id: i64, +) -> Result<()> { + let end = offset + .checked_add(8) + .ok_or_else(|| DbError::internal("manifest next_row_id offset overflow"))?; + if end > payload.len() { + return Err(DbError::internal( + "manifest next_row_id offset exceeded payload length", + )); + } + payload[offset..end].copy_from_slice(&next_row_id.to_le_bytes()); + Ok(()) +} + +pub(crate) fn patch_manifest_table_state( + payload: &mut [u8], + offset: usize, + state: PersistedTableState, +) -> Result<()> { + let end = offset + .checked_add(13) + .ok_or_else(|| DbError::internal("manifest table-state offset overflow"))?; + if end > payload.len() { + return Err(DbError::internal( + "manifest table-state offset exceeded payload length", + )); + } + payload[offset..offset + 4].copy_from_slice(&state.checksum.to_le_bytes()); + payload[offset + 4..offset + 8].copy_from_slice(&state.pointer.head_page_id.to_le_bytes()); + payload[offset + 8..offset + 12].copy_from_slice(&state.pointer.logical_len.to_le_bytes()); + payload[offset + 12] = state.pointer.flags; + Ok(()) +} + +pub(crate) fn patch_manifest_table_pk_index_root( + payload: &mut [u8], + offset: usize, + pk_index_root: Option, +) -> Result<()> { + let end = offset + .checked_add(4) + .ok_or_else(|| DbError::internal("manifest pk_index_root offset overflow"))?; + if end > payload.len() { + return Err(DbError::internal( + "manifest pk_index_root offset exceeded payload length", + )); + } + payload[offset..end].copy_from_slice(&pk_index_root.unwrap_or(0).to_le_bytes()); + Ok(()) +} + +pub(crate) fn paged_table_target_chunk_bytes(page_size: u32) -> usize { + (page_size as usize) + .saturating_mul(PAGED_TABLE_TARGET_CHUNK_PAGES) + .max(TABLE_PAYLOAD_MAGIC.len() + 4) +} + +pub(crate) fn paged_table_checkpoint_compaction_min_bytes(page_size: u32) -> usize { + paged_table_target_chunk_bytes(page_size).saturating_div(2) +} + +pub(crate) fn apply_paged_row_deletions_to_manifest( + manifest: &TablePageManifest, + deleted_row_ids: &BTreeSet, +) -> Result { + if deleted_row_ids.is_empty() { + return Ok(manifest.clone()); + } + + if let Some(updated) = + try_apply_paged_row_deletions_to_manifest_without_base_decode(manifest, deleted_row_ids)? + { + return Ok(updated); + } + + // Partition deleted row ids by the chunk that owns them, using the + // manifest entry index. This avoids decoding any base payload row during a + // pure bulk delete: base rows are immutable, so tombstoning by id is + // sufficient, and only overlay rows that are updated-then-deleted need to + // be decoded and dropped. + let mut deletes_by_chunk: Vec> = (0..manifest.chunks.len()) + .map(|_| BTreeSet::new()) + .collect(); + for &row_id in deleted_row_ids { + let Some(chunk_index) = manifest.chunk_index_for_row_id(row_id) else { + // Row id is not present in the manifest (already gone or never + // existed). Skip it; callers already validated existence. + continue; + }; + if let Some(set) = deletes_by_chunk.get_mut(chunk_index) { + set.insert(row_id); + } + } + + let mut new_chunks = Vec::with_capacity(manifest.chunks.len()); + let mut changed_chunk_indexes = BTreeSet::new(); + for (chunk_index, chunk) in manifest.chunks.iter().enumerate() { + let Some(chunk_deletes) = deletes_by_chunk.get(chunk_index) else { + new_chunks.push(chunk.clone()); + continue; + }; + if chunk_deletes.is_empty() && chunk.overlay_payload.is_none() { + new_chunks.push(chunk.clone()); + continue; + } + + let mut new_tombstones: BTreeSet = chunk.tombstoned_row_ids.iter().copied().collect(); + let mut chunk_changed = false; + let mut overlay_rows: BTreeMap = BTreeMap::new(); + + // Drop overlay rows that are being deleted; keep the rest verbatim. + if let Some(overlay_payload) = &chunk.overlay_payload { + let previous_overlay_rows = decode_table_payload_rows(overlay_payload.as_slice())?; + for previous_row in previous_overlay_rows { + if chunk_deletes.contains(&previous_row.row_id) { + chunk_changed = true; + } else { + overlay_rows.insert(previous_row.row_id, previous_row); + } + } + } + + // Tombstone every deleted id that lives in this chunk's base payload. + for &id in chunk_deletes { + if new_tombstones.insert(id) { + chunk_changed = true; + } + } + + if !chunk_changed { + new_chunks.push(chunk.clone()); + continue; + } + + let overlay_payload = if overlay_rows.is_empty() { + None + } else { + let rows: Vec = overlay_rows.into_values().collect(); + Some(Arc::new(encode_table_payload(&TableData::from_rows(rows))?)) + }; + + new_chunks.push(TablePageManifestChunk { + pointer: chunk.pointer, + checksum: chunk.checksum, + row_count: chunk.row_count, + payload: Arc::clone(&chunk.payload), + tombstoned_row_ids: Arc::new(new_tombstones), + overlay_pointer: None, + overlay_checksum: None, + overlay_payload, + }); + changed_chunk_indexes.insert(chunk_index); + } + + rebuild_table_page_manifest_after_sparse_chunk_changes( + manifest, + new_chunks, + &changed_chunk_indexes, + ) +} + +pub(crate) fn try_apply_paged_row_deletions_to_manifest_without_base_decode( + manifest: &TablePageManifest, + deleted_row_ids: &BTreeSet, +) -> Result> { + let mut planned_deletions = Vec::new(); + try_reserve_paged_directory( + &mut planned_deletions, + deleted_row_ids.len(), + "paged row deletions", + )?; + for &row_id in deleted_row_ids { + let Some((entry_index, entry)) = manifest.rows.entry_for_row_id(row_id)? else { + continue; + }; + if entry.is_overlay { + return Ok(None); + } + let chunk_index = usize::try_from(entry.chunk_index).map_err(|_| { + DbError::corruption("paged table chunk index exceeded chunk list length") + })?; + planned_deletions.push((entry_index, chunk_index, row_id)); + } + + if planned_deletions.is_empty() { + return Ok(Some(manifest.clone())); + } + + planned_deletions.sort_unstable_by_key(|(entry_index, _, _)| *entry_index); + + let mut updated_manifest = manifest.clone(); + let chunks = Arc::make_mut(&mut updated_manifest.chunks); + let tombstoned_row_ids = Arc::make_mut(&mut updated_manifest.tombstoned_row_ids); + + let mut remaining_deletions = planned_deletions.iter().peekable(); + let rebuilt_len = manifest.rows.len().saturating_sub(planned_deletions.len()); + let mut rebuilt_rows = Vec::new(); + try_reserve_paged_directory(&mut rebuilt_rows, rebuilt_len, "sparse paged row entries")?; + for (entry_index, entry) in manifest.rows.iter().enumerate() { + let entry = entry?; + if remaining_deletions + .peek() + .is_some_and(|(delete_entry_index, _, _)| *delete_entry_index == entry_index) + { + remaining_deletions.next(); + continue; + } + rebuilt_rows.push(entry); + } + updated_manifest.rows = Arc::new(TablePageDirectory::Sparse(rebuilt_rows)); + + for &(_, chunk_index, row_id) in &planned_deletions { + let chunk = chunks.get_mut(chunk_index).ok_or_else(|| { + DbError::corruption("paged table chunk index exceeded chunk list length") + })?; + chunk.row_count = chunk + .row_count + .checked_sub(1) + .ok_or_else(|| DbError::corruption("paged table chunk row count underflow"))?; + Arc::make_mut(&mut chunk.tombstoned_row_ids).insert(row_id); + tombstoned_row_ids.insert(row_id); + } + + Ok(Some(updated_manifest)) +} + +pub(crate) fn apply_paged_row_changes_to_manifest( + manifest: &TablePageManifest, + row_changes: &BTreeMap>>, +) -> Result { + if row_changes.is_empty() { + return Ok(manifest.clone()); + } + if let Some(updated) = + try_apply_paged_row_changes_to_manifest_update_only(manifest, row_changes)? + { + return Ok(updated); + } + + let mut changes_by_chunk: Vec>>> = (0..manifest.chunks.len()) + .map(|_| BTreeMap::new()) + .collect(); + for (row_id, change) in row_changes { + let Some(chunk_index) = manifest.chunk_index_for_row_id(*row_id) else { + continue; + }; + if let Some(chunk_changes) = changes_by_chunk.get_mut(chunk_index) { + chunk_changes.insert(*row_id, change); + } + } + + let mut new_chunks = Vec::with_capacity(manifest.chunks.len()); + let mut changed_chunk_indexes = BTreeSet::new(); + for (chunk_index, chunk) in manifest.chunks.iter().enumerate() { + let Some(chunk_changes) = changes_by_chunk.get(chunk_index) else { + new_chunks.push(chunk.clone()); + continue; + }; + if chunk_changes.is_empty() { + new_chunks.push(chunk.clone()); + continue; + } + + let mut new_tombstones: BTreeSet = chunk.tombstoned_row_ids.iter().copied().collect(); + // Use BTreeMap so each row_id appears at most once in the overlay. + let mut overlay_rows: BTreeMap = BTreeMap::new(); + let mut reactivated_base_rows = BTreeSet::new(); + let mut chunk_changed = false; + + // Scan the base payload: tombstone touched rows and queue their + // replacements in the overlay map. + let previous_rows = decode_table_payload_rows(chunk.payload.as_slice())?; + for previous_row in previous_rows { + match chunk_changes.get(&previous_row.row_id).copied() { + Some(Some(next_values)) => { + chunk_changed = true; + if chunk.tombstoned_row_ids.contains(&previous_row.row_id) + && previous_row.values == *next_values + { + new_tombstones.remove(&previous_row.row_id); + reactivated_base_rows.insert(previous_row.row_id); + overlay_rows.remove(&previous_row.row_id); + } else { + new_tombstones.insert(previous_row.row_id); + overlay_rows.insert( + previous_row.row_id, + StoredRow { + row_id: previous_row.row_id, + values: next_values.clone(), + }, + ); + } + } + Some(None) => { + chunk_changed = true; + new_tombstones.insert(previous_row.row_id); + } + None => {} + } + } + + // Scan any existing overlay: replace rows that are re-updated, + // keep untouched rows, drop rows that are deleted. + if let Some(overlay_payload) = &chunk.overlay_payload { + let previous_overlay_rows = decode_table_payload_rows(overlay_payload.as_slice())?; + for previous_row in previous_overlay_rows { + match chunk_changes.get(&previous_row.row_id).copied() { + Some(Some(next_values)) => { + chunk_changed = true; + if reactivated_base_rows.contains(&previous_row.row_id) { + overlay_rows.remove(&previous_row.row_id); + } else { + overlay_rows.insert( + previous_row.row_id, + StoredRow { + row_id: previous_row.row_id, + values: next_values.clone(), + }, + ); + } + } + Some(None) => { + chunk_changed = true; + overlay_rows.remove(&previous_row.row_id); + } + None => { + overlay_rows + .entry(previous_row.row_id) + .or_insert(previous_row); + } + } + } + } + + if !chunk_changed { + new_chunks.push(chunk.clone()); + continue; + } + + let overlay_payload = if overlay_rows.is_empty() { + None + } else { + let rows: Vec = overlay_rows.into_values().collect(); + Some(Arc::new(encode_table_payload(&TableData::from_rows(rows))?)) + }; + + new_chunks.push(TablePageManifestChunk { + pointer: chunk.pointer, + checksum: chunk.checksum, + row_count: chunk.row_count, + payload: Arc::clone(&chunk.payload), + tombstoned_row_ids: Arc::new(new_tombstones), + overlay_pointer: None, + overlay_checksum: None, + overlay_payload, + }); + changed_chunk_indexes.insert(chunk_index); + } + + rebuild_table_page_manifest_after_sparse_chunk_changes( + manifest, + new_chunks, + &changed_chunk_indexes, + ) +} + +pub(crate) fn rebuild_table_page_manifest_after_sparse_chunk_changes( + manifest: &TablePageManifest, + mut new_chunks: Vec, + changed_chunk_indexes: &BTreeSet, +) -> Result { + if changed_chunk_indexes.is_empty() { + return Ok(manifest.clone()); + } + + let tombstoned_row_ids = new_chunks + .iter() + .flat_map(|chunk| chunk.tombstoned_row_ids.iter().copied()) + .collect::>(); + let mut rows = Vec::new(); + try_reserve_paged_directory(&mut rows, manifest.rows.len(), "sparse paged row entries")?; + for entry in manifest.rows.iter() { + let entry = entry?; + if !changed_chunk_indexes.contains(&(entry.chunk_index as usize)) { + rows.push(entry); + } + } + for chunk_index in changed_chunk_indexes { + let Some(chunk) = new_chunks.get(*chunk_index) else { + return Err(DbError::corruption( + "paged table changed chunk index exceeded chunk list length", + )); + }; + let chunk_rows = table_page_entries_for_chunk(*chunk_index, chunk)?; + if let Some(chunk) = new_chunks.get_mut(*chunk_index) { + chunk.row_count = chunk_rows.len(); + } + rows.extend(chunk_rows); + } + rows.sort_by_key(|entry| entry.row_id); + #[cfg(debug_assertions)] + { + for window in rows.windows(2) { + assert_ne!( + window[0].row_id, window[1].row_id, + "duplicate row_id in TablePageManifest rows" + ); + } + } + + Ok(TablePageManifest { + chunks: Arc::new(new_chunks), + rows: Arc::new(TablePageDirectory::Sparse(rows)), + tombstoned_row_ids: Arc::new(tombstoned_row_ids), + }) +} + +pub(crate) fn read_table_page_manifest_from_state( + store: &S, + state: PersistedTableState, +) -> Result { + if state.pointer.head_page_id == 0 || state.pointer.logical_len == 0 { + return TablePageManifest::from_chunks(Vec::new()); + } + if state.pointer.is_table_paged_manifest() { + return TablePageManifest::from_chunks(read_paged_table_chunk_payloads(store, state)?); + } + let payload = Arc::new(read_overflow(store, state.pointer)?); + if crc32c_parts(&[payload.as_slice()]) != state.checksum { + return Err(DbError::corruption("table payload checksum mismatch")); + } + TablePageManifest::from_payload(payload) +} + +pub(crate) fn table_page_manifest_chunk_is_plain(chunk: &TablePageManifestChunk) -> bool { + chunk.tombstoned_row_ids.is_empty() + && chunk.overlay_pointer.is_none() + && chunk.overlay_checksum.is_none() + && chunk.overlay_payload.is_none() +} + +pub(crate) fn table_page_manifest_chunk_visible_row_count( + chunk: &TablePageManifestChunk, +) -> Result { + let base_physical = read_table_payload_row_count_from_bytes(&chunk.payload)?; + let overlay_physical = chunk + .overlay_payload + .as_ref() + .map(|payload| read_table_payload_row_count_from_bytes(payload)) + .transpose()? + .unwrap_or(0); + Ok(base_physical + .saturating_sub(chunk.tombstoned_row_ids.len()) + .saturating_add(overlay_physical)) +} + +pub(crate) fn table_page_manifest_with_persisted_chunks( + current: &TablePageManifest, + persisted_chunks: &[TablePageManifestChunk], +) -> TablePageManifest { + TablePageManifest { + chunks: Arc::new(persisted_chunks.to_vec()), + rows: Arc::clone(¤t.rows), + tombstoned_row_ids: Arc::clone(¤t.tombstoned_row_ids), + } +} + +pub(crate) fn try_append_only_paged_table_from_manifest( + store: &mut S, + previous_state: PersistedTableState, + manifest: &TablePageManifest, +) -> Result)>> { + if previous_state.pointer.head_page_id == 0 || !previous_state.pointer.is_table_paged_manifest() + { + return Ok(None); + } + if manifest.chunks.is_empty() { + return Ok(None); + } + + let manifest_payload = read_overflow(store, previous_state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != previous_state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); + } + let previous_manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + if previous_manifest.chunks.is_empty() || manifest.chunks.len() < previous_manifest.chunks.len() + { + return Ok(None); + } + + let previous_tail_index = previous_manifest.chunks.len() - 1; + let mut first_changed_index = None; + let mut current_checksums = Vec::with_capacity(manifest.chunks.len()); + for (index, current_chunk) in manifest.chunks.iter().enumerate() { + if !table_page_manifest_chunk_is_plain(current_chunk) { + return Ok(None); + } + let checksum = crc32c_parts(&[current_chunk.payload.as_slice()]); + current_checksums.push(checksum); + let Some(previous_chunk) = previous_manifest.chunks.get(index) else { + continue; + }; + if !persisted_paged_chunk_is_plain(previous_chunk) { + return Ok(None); + } + if previous_chunk.checksum == checksum + && previous_chunk.row_count == current_chunk.row_count + { + continue; + } + if index != previous_tail_index { + return Ok(None); + } + first_changed_index = Some(index); + } + + if first_changed_index.is_none() && manifest.chunks.len() == previous_manifest.chunks.len() { + return Ok(None); + } + + let mut new_chunks = Vec::with_capacity(manifest.chunks.len()); + let mut persisted_chunks = Vec::with_capacity(manifest.chunks.len()); + + for (index, current_chunk) in manifest.chunks.iter().enumerate() { + let checksum = current_checksums[index]; + if let Some(previous_chunk) = previous_manifest.chunks.get(index) { + if previous_chunk.checksum == checksum + && previous_chunk.row_count == current_chunk.row_count + { + new_chunks.push(previous_chunk.clone()); + persisted_chunks.push(persisted_chunk_from_current( + previous_chunk.pointer, + previous_chunk.checksum, + previous_chunk.row_count, + current_chunk, + )); + continue; + } + + let pointer = rewrite_overflow( + store, + previous_chunk.pointer, + current_chunk.payload.as_slice(), + CompressionMode::Never, + )?; + new_chunks.push(PersistedTableChunkState { + pointer, + checksum, + row_count: current_chunk.row_count, + tombstoned_row_ids: Vec::new(), + overlay_pointer: None, + overlay_checksum: None, + }); + persisted_chunks.push(persisted_chunk_from_current( + pointer, + checksum, + current_chunk.row_count, + current_chunk, + )); + continue; + } + + let pointer = write_overflow( + store, + current_chunk.payload.as_slice(), + CompressionMode::Never, + )?; + new_chunks.push(PersistedTableChunkState { + pointer, + checksum, + row_count: current_chunk.row_count, + tombstoned_row_ids: Vec::new(), + overlay_pointer: None, + overlay_checksum: None, + }); + persisted_chunks.push(persisted_chunk_from_current( + pointer, + checksum, + current_chunk.row_count, + current_chunk, + )); + } + + let updated_manifest_payload = + encode_paged_table_manifest_payload(&PersistedPagedTableManifest { chunks: new_chunks })?; + let checksum = crc32c_parts(&[updated_manifest_payload.as_slice()]); + let pointer = rewrite_overflow( + store, + previous_state.pointer.with_table_paged_manifest(false), + &updated_manifest_payload, + CompressionMode::Never, + )? + .with_table_paged_manifest(true); + let tail = read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default(); + + Ok(Some(( + PersistedTableState { + pointer, + checksum, + row_count: manifest.row_count(), + tail, + pk_index_root: previous_state.pk_index_root, + }, + persisted_chunks, + ))) +} + +pub(crate) fn paged_table_state_needs_checkpoint_compaction( + store: &S, + state: PersistedTableState, +) -> Result { + if state.pointer.head_page_id == 0 || !state.pointer.is_table_paged_manifest() { + return Ok(false); + } + + let manifest_payload = read_overflow(store, state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); + } + let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + let chunk_compaction_min_bytes = paged_table_checkpoint_compaction_min_bytes(store.page_size()); + for chunk in &manifest.chunks { + if !chunk.tombstoned_row_ids.is_empty() || chunk.overlay_pointer.is_some() { + return Ok(true); + } + if chunk.pointer.head_page_id != 0 + && !chunk.pointer.is_compressed() + && usize::try_from(chunk.pointer.logical_len) + .ok() + .is_some_and(|len| len >= chunk_compaction_min_bytes) + { + return Ok(true); + } + } + + Ok(!state.pointer.is_compressed() + && usize::try_from(state.pointer.logical_len) + .ok() + .is_some_and(|len| len >= AUTO_MIN_PAYLOAD_BYTES)) +} + +pub(crate) fn rewrite_paged_table_from_resident( + store: &mut S, + previous_state: PersistedTableState, + data: &TableData, + page_size: u32, +) -> Result { + if previous_state.pointer.head_page_id == 0 || !previous_state.pointer.is_table_paged_manifest() + { + let encoded_chunks = encode_paged_table_chunks(data, page_size)?; + return persist_paged_table(store, previous_state, &encoded_chunks, data.rows.len()); + } + + let manifest_payload = read_overflow(store, previous_state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != previous_state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); + } + let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + let mut current_rows_by_id = Int64Map::default(); + for row in data.visible_rows() { + current_rows_by_id.insert(row.row_id, row); + } + + let mut seen_old_row_ids = std::collections::BTreeSet::new(); + let mut new_chunks = Vec::with_capacity(manifest.chunks.len()); + let mut replaced_chunk_pointers = Vec::new(); + let mut changed = false; + + for chunk in manifest.chunks { + let payload = read_overflow(store, chunk.pointer)?; + if crc32c_parts(&[payload.as_slice()]) != chunk.checksum { + return Err(DbError::corruption("paged table chunk checksum mismatch")); + } + let previous_rows = decode_table_payload_rows(payload.as_slice())?; + let mut current_chunk_rows = Vec::with_capacity(previous_rows.len()); + let mut chunk_changed = false; + + for previous_row in &previous_rows { + seen_old_row_ids.insert(previous_row.row_id); + if let Some(current_row) = current_rows_by_id.get(&previous_row.row_id).copied() { + if current_row.values != previous_row.values { + chunk_changed = true; + } + current_chunk_rows.push(current_row.clone()); + } else { + chunk_changed = true; + } + } + + if !chunk_changed { + new_chunks.push(chunk); + continue; + } + + changed = true; + replaced_chunk_pointers.push(chunk.pointer); + for encoded_chunk in encode_paged_table_chunks_from_rows(¤t_chunk_rows, page_size)? { + let pointer = write_overflow(store, &encoded_chunk.payload, CompressionMode::Never)?; + new_chunks.push(PersistedTableChunkState { + pointer, + checksum: encoded_chunk.checksum, + row_count: encoded_chunk.row_count, + tombstoned_row_ids: Vec::new(), + overlay_pointer: None, + overlay_checksum: None, + }); + } + } + + let appended_rows = data + .visible_rows() + .filter(|row| !seen_old_row_ids.contains(&row.row_id)) + .cloned() + .collect::>(); + if !appended_rows.is_empty() { + changed = true; + for encoded_chunk in encode_paged_table_chunks_from_rows(&appended_rows, page_size)? { + let pointer = write_overflow(store, &encoded_chunk.payload, CompressionMode::Never)?; + new_chunks.push(PersistedTableChunkState { + pointer, + checksum: encoded_chunk.checksum, + row_count: encoded_chunk.row_count, + tombstoned_row_ids: Vec::new(), + overlay_pointer: None, + overlay_checksum: None, + }); + } + } + + if !changed { + return Ok(previous_state); + } + + if new_chunks.is_empty() { + free_persisted_table_bytes(store, previous_state)?; + return Ok(PersistedTableState { + pointer: OverflowPointer { + head_page_id: 0, + logical_len: 0, + flags: 0, + }, + checksum: 0, + row_count: 0, + tail: OverflowTailInfo::default(), + pk_index_root: previous_state.pk_index_root, + }); + } + + let updated_manifest_payload = + encode_paged_table_manifest_payload(&PersistedPagedTableManifest { chunks: new_chunks })?; + let checksum = crc32c_parts(&[updated_manifest_payload.as_slice()]); + let pointer = rewrite_overflow( + store, + previous_state.pointer.with_table_paged_manifest(false), + &updated_manifest_payload, + CompressionMode::Never, + )? + .with_table_paged_manifest(true); + let tail = read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default(); + + for replaced_pointer in replaced_chunk_pointers { + if replaced_pointer.head_page_id != 0 { + free_overflow(store, replaced_pointer.head_page_id)?; + } + } + + Ok(PersistedTableState { + pointer, + checksum, + row_count: data.row_count(), + tail, + pk_index_root: previous_state.pk_index_root, + }) +} + +/// Delete-only variant of [`rewrite_paged_table_from_resident`]. When the +/// transaction only deleted rows (no updates, no appends), the surviving rows +/// are a strict subset of the previous on-disk rows. Chunks that contain no +/// deleted row id are byte-for-byte unchanged and can be reused verbatim +/// without decoding their row values; only chunks that actually hold deleted +/// rows are re-encoded. This avoids decoding every row's values during a bulk +/// delete on a paged table, which previously dominated commit wall time. +pub(crate) fn rewrite_paged_table_from_resident_delete_only( + store: &mut S, + previous_state: PersistedTableState, + data: &TableData, + page_size: u32, + deleted_row_ids: &BTreeSet, +) -> Result { + if previous_state.pointer.head_page_id == 0 || !previous_state.pointer.is_table_paged_manifest() + { + let encoded_chunks = encode_paged_table_chunks(data, page_size)?; + return persist_paged_table(store, previous_state, &encoded_chunks, data.rows.len()); + } + + let manifest_payload = read_overflow(store, previous_state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != previous_state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); + } + let manifest = decode_paged_table_manifest_payload(&manifest_payload)?; + + // Index the surviving rows by id so we can re-encode affected chunks from + // the resident data without re-reading them from disk. + let mut current_rows_by_id = Int64Map::default(); + for row in data.visible_rows() { + current_rows_by_id.insert(row.row_id, row); + } + + let mut new_chunks = Vec::with_capacity(manifest.chunks.len()); + let mut replaced_chunk_pointers = Vec::new(); + let mut changed = false; + + for chunk in manifest.chunks { + let payload = read_overflow(store, chunk.pointer)?; + if crc32c_parts(&[payload.as_slice()]) != chunk.checksum { + return Err(DbError::corruption("paged table chunk checksum mismatch")); + } + // Fast path: scan row ids only (skip value decode) to determine whether + // this chunk contains any deleted row. If not, reuse the chunk as-is. + let chunk_row_ids = scan_table_payload_row_ids(payload.as_slice())?; + let chunk_has_deletes = chunk_row_ids.iter().any(|id| deleted_row_ids.contains(id)); + if !chunk_has_deletes { + new_chunks.push(chunk); + continue; + } + + changed = true; + replaced_chunk_pointers.push(chunk.pointer); + let previous_rows = decode_table_payload_rows(payload.as_slice())?; + let mut current_chunk_rows = Vec::with_capacity(previous_rows.len()); + for previous_row in &previous_rows { + if let Some(current_row) = current_rows_by_id.get(&previous_row.row_id).copied() { + current_chunk_rows.push(current_row.clone()); + } + } + for encoded_chunk in encode_paged_table_chunks_from_rows(¤t_chunk_rows, page_size)? { + let pointer = write_overflow(store, &encoded_chunk.payload, CompressionMode::Never)?; + new_chunks.push(PersistedTableChunkState { + pointer, + checksum: encoded_chunk.checksum, + row_count: encoded_chunk.row_count, + tombstoned_row_ids: Vec::new(), + overlay_pointer: None, + overlay_checksum: None, + }); + } + } + + if !changed { + return Ok(previous_state); + } + + if new_chunks.is_empty() { + free_persisted_table_bytes(store, previous_state)?; + return Ok(PersistedTableState { + pointer: OverflowPointer { + head_page_id: 0, + logical_len: 0, + flags: 0, + }, + checksum: 0, + row_count: 0, + tail: OverflowTailInfo::default(), + pk_index_root: previous_state.pk_index_root, + }); + } + + let updated_manifest_payload = + encode_paged_table_manifest_payload(&PersistedPagedTableManifest { chunks: new_chunks })?; + let checksum = crc32c_parts(&[updated_manifest_payload.as_slice()]); + let pointer = rewrite_overflow( + store, + previous_state.pointer.with_table_paged_manifest(false), + &updated_manifest_payload, + CompressionMode::Never, + )? + .with_table_paged_manifest(true); + let tail = read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default(); + + for replaced_pointer in replaced_chunk_pointers { + if replaced_pointer.head_page_id != 0 { + free_overflow(store, replaced_pointer.head_page_id)?; + } + } + + Ok(PersistedTableState { + pointer, + checksum, + row_count: data.row_count(), + tail, + pk_index_root: previous_state.pk_index_root, + }) +} + +pub(crate) fn rewrite_paged_table_from_manifest( + store: &mut S, + previous_state: PersistedTableState, + manifest: &TablePageManifest, +) -> Result<(PersistedTableState, Vec)> { + if manifest.chunks.is_empty() { + if previous_state.pointer.head_page_id != 0 { + free_persisted_table_bytes(store, previous_state)?; + } + return Ok(( + PersistedTableState { + pointer: OverflowPointer { + head_page_id: 0, + logical_len: 0, + flags: 0, + }, + checksum: 0, + row_count: 0, + tail: OverflowTailInfo::default(), + pk_index_root: previous_state.pk_index_root, + }, + Vec::new(), + )); + } + + let previous_chunks = if previous_state.pointer.head_page_id != 0 + && previous_state.pointer.is_table_paged_manifest() + { + let manifest_payload = read_overflow(store, previous_state.pointer)?; + if crc32c_parts(&[manifest_payload.as_slice()]) != previous_state.checksum { + return Err(DbError::corruption( + "paged table manifest checksum mismatch", + )); + } + decode_paged_table_manifest_payload(&manifest_payload)?.chunks + } else { + Vec::new() + }; + let mut previous_payloads = None; + let mut reused_previous = vec![false; previous_chunks.len()]; + let mut replaced_overlay_pointers = Vec::new(); + + let mut new_chunks = Vec::with_capacity(manifest.chunks.len()); + let mut persisted_chunks = Vec::with_capacity(manifest.chunks.len()); + + for (current_index, current_chunk) in manifest.chunks.iter().enumerate() { + if let Some(chunk_state) = previous_chunks.get(current_index) { + if persisted_chunk_metadata_matches_current(chunk_state, current_chunk) { + reused_previous[current_index] = true; + persisted_chunks.push(TablePageManifestChunk { + pointer: chunk_state.pointer, + checksum: chunk_state.checksum, + row_count: chunk_state.row_count, + payload: Arc::clone(¤t_chunk.payload), + tombstoned_row_ids: Arc::clone(¤t_chunk.tombstoned_row_ids), + overlay_pointer: chunk_state.overlay_pointer, + overlay_checksum: chunk_state.overlay_checksum, + overlay_payload: current_chunk.overlay_payload.clone(), + }); + new_chunks.push(chunk_state.clone()); + continue; + } + if chunk_state.pointer.head_page_id != 0 + && chunk_state.pointer == current_chunk.pointer + && chunk_state.checksum == current_chunk.checksum + { + reused_previous[current_index] = true; + let current_overlay_checksum = current_chunk + .overlay_payload + .as_ref() + .map(|payload| crc32c_parts(&[payload.as_slice()])); + let (overlay_pointer, overlay_checksum) = match ( + ¤t_chunk.overlay_payload, + current_chunk.overlay_pointer, + current_chunk.overlay_checksum, + ) { + (Some(_), Some(pointer), Some(checksum)) + if Some(pointer) == chunk_state.overlay_pointer + && Some(checksum) == chunk_state.overlay_checksum => + { + (Some(pointer), Some(checksum)) + } + (Some(overlay_payload), _, _) => { + let pointer = write_overflow( + store, + overlay_payload.as_slice(), + CompressionMode::Never, + )?; + let checksum = current_overlay_checksum.ok_or_else(|| { + DbError::internal("overlay checksum missing for paged table chunk") + })?; + (Some(pointer), Some(checksum)) + } + (None, _, _) => (None, None), + }; + if let Some(previous_overlay_pointer) = chunk_state.overlay_pointer { + if Some(previous_overlay_pointer) != overlay_pointer + && previous_overlay_pointer.head_page_id != 0 + { + replaced_overlay_pointers.push(previous_overlay_pointer); + } + } + let visible = table_page_manifest_chunk_visible_row_count(current_chunk)?; + new_chunks.push(PersistedTableChunkState { + pointer: chunk_state.pointer, + checksum: chunk_state.checksum, + row_count: visible, + tombstoned_row_ids: current_chunk.tombstoned_row_ids.iter().copied().collect(), + overlay_pointer, + overlay_checksum, + }); + persisted_chunks.push(TablePageManifestChunk { + pointer: chunk_state.pointer, + checksum: chunk_state.checksum, + row_count: visible, + payload: Arc::clone(¤t_chunk.payload), + tombstoned_row_ids: Arc::clone(¤t_chunk.tombstoned_row_ids), + overlay_pointer, + overlay_checksum, + overlay_payload: current_chunk.overlay_payload.clone(), + }); + continue; + } + } + + if previous_payloads.is_none() { + if previous_state.pointer.head_page_id != 0 + && previous_state.pointer.is_table_paged_manifest() + { + previous_payloads = Some(read_paged_table_chunk_payloads(store, previous_state)?); + } else { + previous_payloads = Some(Vec::new()); + } + } + let reusable_previous = previous_payloads + .as_ref() + .expect("previous payloads loaded"); + let checksum = crc32c_parts(&[current_chunk.payload.as_slice()]); + let current_overlay_checksum = current_chunk + .overlay_payload + .as_ref() + .map(|payload| crc32c_parts(&[payload.as_slice()])); + let reused_index = reusable_previous + .iter() + .enumerate() + .find_map(|(index, payload)| { + let chunk_state = previous_chunks.get(index)?; + let overlay_match = match ( + &payload.overlay_payload, + ¤t_chunk.overlay_payload, + chunk_state.overlay_checksum, + current_overlay_checksum, + ) { + (None, None, None, None) => true, + (Some(previous), Some(current), Some(previous_checksum), Some(checksum)) => { + previous_checksum == checksum && previous.as_slice() == current.as_slice() + } + _ => false, + }; + (!reused_previous[index] + && chunk_state.checksum == checksum + && payload.payload.as_slice() == current_chunk.payload.as_slice() + && chunk_state.row_count == current_chunk.row_count + && chunk_state.tombstoned_row_ids.len() + == current_chunk.tombstoned_row_ids.len() + && chunk_state + .tombstoned_row_ids + .iter() + .all(|id| current_chunk.tombstoned_row_ids.contains(id)) + && overlay_match) + .then_some(index) + }); + if let Some(index) = reused_index { + reused_previous[index] = true; + let chunk_state = previous_chunks[index].clone(); + persisted_chunks.push(TablePageManifestChunk { + pointer: chunk_state.pointer, + checksum: chunk_state.checksum, + row_count: chunk_state.row_count, + payload: Arc::clone(¤t_chunk.payload), + tombstoned_row_ids: Arc::clone(¤t_chunk.tombstoned_row_ids), + overlay_pointer: chunk_state.overlay_pointer, + overlay_checksum: chunk_state.overlay_checksum, + overlay_payload: current_chunk.overlay_payload.clone(), + }); + new_chunks.push(chunk_state); + continue; + } + + let pointer = write_overflow(store, ¤t_chunk.payload, CompressionMode::Never)?; + let (overlay_pointer, overlay_checksum) = + if let Some(overlay_payload) = ¤t_chunk.overlay_payload { + let overlay_pointer = + write_overflow(store, overlay_payload.as_slice(), CompressionMode::Never)?; + let overlay_checksum = crc32c_parts(&[overlay_payload.as_slice()]); + (Some(overlay_pointer), Some(overlay_checksum)) + } else { + (None, None) + }; + let visible = table_page_manifest_chunk_visible_row_count(current_chunk)?; + new_chunks.push(PersistedTableChunkState { + pointer, + checksum, + row_count: visible, + tombstoned_row_ids: current_chunk.tombstoned_row_ids.iter().copied().collect(), + overlay_pointer, + overlay_checksum, + }); + persisted_chunks.push(TablePageManifestChunk { + pointer, + checksum, + row_count: visible, + payload: Arc::clone(¤t_chunk.payload), + tombstoned_row_ids: Arc::clone(¤t_chunk.tombstoned_row_ids), + overlay_pointer, + overlay_checksum, + overlay_payload: current_chunk.overlay_payload.clone(), + }); + } + + if new_chunks == previous_chunks { + return Ok((previous_state, persisted_chunks)); + } + + let updated_manifest_payload = + encode_paged_table_manifest_payload(&PersistedPagedTableManifest { chunks: new_chunks })?; + let checksum = crc32c_parts(&[updated_manifest_payload.as_slice()]); + let pointer = rewrite_overflow( + store, + previous_state.pointer.with_table_paged_manifest(false), + &updated_manifest_payload, + CompressionMode::Never, + )? + .with_table_paged_manifest(true); + let tail = read_uncompressed_overflow_tail(store, pointer)?.unwrap_or_default(); + + for (index, chunk_state) in previous_chunks.iter().enumerate() { + if reused_previous[index] || chunk_state.pointer.head_page_id == 0 { + continue; + } + free_overflow(store, chunk_state.pointer.head_page_id)?; + if let Some(overlay_pointer) = chunk_state.overlay_pointer { + if overlay_pointer.head_page_id != 0 { + free_overflow(store, overlay_pointer.head_page_id)?; + } + } + } + for overlay_pointer in replaced_overlay_pointers { + if overlay_pointer.head_page_id != 0 { + free_overflow(store, overlay_pointer.head_page_id)?; + } + } + + Ok(( + PersistedTableState { + pointer, + checksum, + row_count: manifest.row_count(), + tail, + pk_index_root: previous_state.pk_index_root, + }, + persisted_chunks, + )) +} + +pub(crate) fn splice_updated_rows_payload_in_place( + payload: &mut [u8], + data: &TableData, + dirty_indices: &[usize], +) -> Result> { + const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; + + if payload.len() < HEADER_LEN || payload[..8] != *TABLE_PAYLOAD_MAGIC { + return Ok(None); + } + let old_row_count = + u32::from_le_bytes(payload[8..12].try_into().expect("row-count header length")) as usize; + if old_row_count != data.rows.len() { + return Ok(None); + } + + let mut sorted_dirty: Vec = dirty_indices.to_vec(); + sorted_dirty.sort_unstable(); + sorted_dirty.dedup(); + if sorted_dirty.is_empty() { + return Ok(Some(SpliceDirtyRange { + first_dirty_byte: payload.len(), + last_dirty_byte: payload.len(), + })); + } + + let mut row_spans: Vec<(usize, usize)> = Vec::with_capacity(sorted_dirty.len()); + let mut scan_offset = HEADER_LEN; + let mut dirty_cursor = 0; + let mut row_idx = 0; + while dirty_cursor < sorted_dirty.len() && scan_offset + 12 <= payload.len() { + if row_idx >= old_row_count { + break; + } + let row_id = i64::from_le_bytes( + payload[scan_offset..scan_offset + 8] + .try_into() + .expect("row id length"), + ); + let row_data_len = u32::from_le_bytes( + payload[scan_offset + 8..scan_offset + 12] + .try_into() + .expect("row data len"), + ) as usize; + let row_end = scan_offset.saturating_add(12).saturating_add(row_data_len); + if row_end > payload.len() { + return Ok(None); + } + if row_idx == sorted_dirty[dirty_cursor] { + let Some(row) = data.rows.get(row_idx) else { + return Ok(None); + }; + if row.row_id != row_id { + return Ok(None); + } + row_spans.push((scan_offset, row_end)); + dirty_cursor += 1; + if dirty_cursor == sorted_dirty.len() { + break; + } + } + scan_offset = row_end; + row_idx += 1; + } + if row_spans.len() != sorted_dirty.len() { + return Ok(None); + } + + let mut encoded_rows: Vec> = Vec::with_capacity(sorted_dirty.len()); + let mut encoded_row = Vec::with_capacity(128); + for (span_idx, &dirty_row) in sorted_dirty.iter().enumerate() { + let Some(row) = data.rows.get(dirty_row) else { + return Ok(None); + }; + let (span_start, span_end) = row_spans[span_idx]; + let old_row_body_len = span_end.saturating_sub(span_start).saturating_sub(12); + encoded_row.clear(); + Row::encode_values_into(&row.values, &mut encoded_row)?; + if encoded_row.len() > old_row_body_len { + return Ok(None); + } + encoded_rows.push(encoded_row.clone()); + } + + for (span_idx, encoded_row) in encoded_rows.iter().enumerate() { + let (span_start, span_end) = row_spans[span_idx]; + let body_start = span_start.saturating_add(12); + let body_written_end = body_start.saturating_add(encoded_row.len()); + payload[body_start..body_written_end].copy_from_slice(encoded_row); + payload[body_written_end..span_end].fill(0); + } + + Ok(Some(SpliceDirtyRange { + first_dirty_byte: row_spans + .first() + .map_or(0, |span| span.0.saturating_add(12)), + last_dirty_byte: row_spans.last().map_or(payload.len(), |span| span.1), + })) +} + +/// ADR 0200: mark the given row ids as deleted in place by setting the +/// tombstone flag on each slot's `row_body_len` field. The body bytes are left +/// untouched, so only four bytes per deleted row change and the payload length +/// is unchanged. This collapses a scattered delete from "rewrite every byte +/// after the first deletion" down to "patch one length field per deleted row". +/// +/// Returns the dirty byte ranges to persist, or `None` when the payload is not +/// a recognizable resident table payload or a targeted row id is absent / already +/// tombstoned — in which case the caller falls back to the splice / full +/// re-encode path. +pub(crate) fn tombstone_deleted_rows_payload_in_place( + payload: &mut [u8], + deleted_row_ids: &BTreeSet, +) -> Result>>> { + const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; + + if deleted_row_ids.is_empty() { + return Ok(Some(Vec::new())); + } + if payload.len() < HEADER_LEN || payload[..8] != *TABLE_PAYLOAD_MAGIC { + return Ok(None); + } + let row_count = + u32::from_le_bytes(payload[8..12].try_into().expect("row-count header length")) as usize; + + let mut remaining = deleted_row_ids.len(); + let mut dirty_ranges = Vec::with_capacity(deleted_row_ids.len()); + let mut offset = HEADER_LEN; + let mut scanned_rows = 0usize; + while remaining > 0 && offset + 12 <= payload.len() { + if scanned_rows >= row_count { + break; + } + let row_id = i64::from_le_bytes( + payload[offset..offset + 8] + .try_into() + .expect("row id length"), + ); + let len_field_offset = offset + 8; + let raw_len = u32::from_le_bytes( + payload[len_field_offset..len_field_offset + 4] + .try_into() + .expect("row data len"), + ); + let (is_tombstone, body_len) = split_table_payload_row_len(raw_len); + let Some(row_end) = len_field_offset + .checked_add(4) + .and_then(|value| value.checked_add(body_len)) + else { + return Ok(None); + }; + if row_end > payload.len() { + return Ok(None); + } + if !is_tombstone && deleted_row_ids.contains(&row_id) { + let flagged = raw_len | TABLE_PAYLOAD_ROW_TOMBSTONE_FLAG; + payload[len_field_offset..len_field_offset + 4].copy_from_slice(&flagged.to_le_bytes()); + dirty_ranges.push(len_field_offset..len_field_offset + 4); + remaining -= 1; + } + offset = row_end; + scanned_rows += 1; + } + + if remaining > 0 { + // A targeted row id was not found as a live slot. Fall back so the + // caller re-encodes from the authoritative resident rows. + return Ok(None); + } + Ok(Some(dirty_ranges)) +} + +pub(crate) fn tombstone_deleted_rows_payload_by_locator( + payload: &mut [u8], + deleted_row_ids: &BTreeSet, + locators: &Int64Map, +) -> Result>>> { + const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; + + if deleted_row_ids.is_empty() { + return Ok(Some(Vec::new())); + } + if payload.len() < HEADER_LEN || payload[..8] != *TABLE_PAYLOAD_MAGIC { + return Ok(None); + } + + let mut dirty_ranges = Vec::with_capacity(deleted_row_ids.len()); + for row_id in deleted_row_ids { + let Some(&len_field_offset) = locators.get(row_id) else { + return Ok(None); + }; + let len_field_offset = len_field_offset as usize; + if len_field_offset < 8 || len_field_offset + 4 > payload.len() { + return Ok(None); + } + let row_id_offset = len_field_offset - 8; + let actual_row_id = i64::from_le_bytes( + payload[row_id_offset..row_id_offset + 8] + .try_into() + .expect("row id length"), + ); + if actual_row_id != *row_id { + return Ok(None); + } + let raw_len = u32::from_le_bytes( + payload[len_field_offset..len_field_offset + 4] + .try_into() + .expect("row data len"), + ); + let (is_tombstone, body_len) = split_table_payload_row_len(raw_len); + let Some(row_end) = len_field_offset + .checked_add(4) + .and_then(|value| value.checked_add(body_len)) + else { + return Ok(None); + }; + if row_end > payload.len() || is_tombstone { + return Ok(None); + } + + let flagged = raw_len | TABLE_PAYLOAD_ROW_TOMBSTONE_FLAG; + payload[len_field_offset..len_field_offset + 4].copy_from_slice(&flagged.to_le_bytes()); + dirty_ranges.push(len_field_offset..len_field_offset + 4); + } + Ok(Some(dirty_ranges)) +} + +pub(crate) fn tombstone_deleted_rows_cached_payload_by_locator( + payload: &mut [u8], + deleted_row_ids: &BTreeSet, + locators: &Int64Map, + previous_checksum: u32, +) -> Result>, u32)>> { + const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; + + if deleted_row_ids.is_empty() { + return Ok(Some((Vec::new(), previous_checksum))); + } + if payload.len() < HEADER_LEN || payload[..8] != *TABLE_PAYLOAD_MAGIC { + return Ok(None); + } + + let mut patches = Vec::with_capacity(deleted_row_ids.len()); + let mut span_start = usize::MAX; + let mut span_end = 0usize; + for row_id in deleted_row_ids { + let Some(&len_field_offset) = locators.get(row_id) else { + return Ok(None); + }; + let len_field_offset = len_field_offset as usize; + if len_field_offset < 8 || len_field_offset + 4 > payload.len() { + return Ok(None); + } + let row_id_offset = len_field_offset - 8; + let actual_row_id = i64::from_le_bytes( + payload[row_id_offset..row_id_offset + 8] + .try_into() + .expect("row id length"), + ); + if actual_row_id != *row_id { + return Ok(None); + } + let raw_len = u32::from_le_bytes( + payload[len_field_offset..len_field_offset + 4] + .try_into() + .expect("row data len"), + ); + let (is_tombstone, body_len) = split_table_payload_row_len(raw_len); + let Some(row_end) = len_field_offset + .checked_add(4) + .and_then(|value| value.checked_add(body_len)) + else { + return Ok(None); + }; + if row_end > payload.len() || is_tombstone { + return Ok(None); + } + let new_bytes = (raw_len | TABLE_PAYLOAD_ROW_TOMBSTONE_FLAG).to_le_bytes(); + span_start = span_start.min(len_field_offset); + span_end = span_end.max(len_field_offset + 4); + patches.push((len_field_offset, new_bytes)); + } + + if patches.is_empty() { + return Ok(Some((Vec::new(), previous_checksum))); + } + let old_span = payload[span_start..span_end].to_vec(); + let mut dirty_ranges = Vec::with_capacity(patches.len()); + for (offset, new_bytes) in patches { + if payload[offset..offset + 4] != new_bytes { + payload[offset..offset + 4].copy_from_slice(&new_bytes); + dirty_ranges.push(offset..offset + 4); + } + } + let checksum = crc32c_patch_bytes( + previous_checksum, + payload.len(), + span_start, + &old_span, + &payload[span_start..span_end], + ) + .ok_or_else(|| DbError::internal("resident tombstone checksum patch failed"))?; + Ok(Some((dirty_ranges, checksum))) +} + +pub(crate) fn tombstone_deleted_rows_overflow_by_locator( + store: &mut S, + previous_state: PersistedTableState, + chain_cache: &OverflowChainCache, + deleted_row_ids: &BTreeSet, + locators: &Int64Map, + live_row_count: usize, +) -> Result> { + if deleted_row_ids.is_empty() { + return Ok(Some((previous_state, chain_cache.clone()))); + } + let pointer = previous_state.pointer; + if pointer.head_page_id == 0 + || pointer.logical_len == 0 + || pointer.is_compressed() + || pointer.is_table_paged_manifest() + { + return Ok(None); + } + if locators.is_empty() { + return Ok(None); + } + let dead_after = locators.len().saturating_sub(live_row_count); + if live_row_count == 0 || dead_after > live_row_count { + return Ok(None); + } + + let logical_len = pointer.logical_len as usize; + let mut patch_bytes = Vec::with_capacity(deleted_row_ids.len()); + for row_id in deleted_row_ids { + let Some(&len_field_offset) = locators.get(row_id) else { + return Ok(None); + }; + let len_field_offset = len_field_offset as usize; + if len_field_offset < 8 || len_field_offset + 4 > logical_len { + return Ok(None); + } + + let mut row_id_bytes = [0_u8; 8]; + if !read_overflow_cached_logical_bytes( + store, + pointer, + &chain_cache.page_ids, + len_field_offset - 8, + &mut row_id_bytes, + )? { + return Ok(None); + } + let actual_row_id = i64::from_le_bytes(row_id_bytes); + if actual_row_id != *row_id { + return Ok(None); + } + + let mut len_bytes = [0_u8; 4]; + if !read_overflow_cached_logical_bytes( + store, + pointer, + &chain_cache.page_ids, + len_field_offset, + &mut len_bytes, + )? { + return Ok(None); + } + let raw_len = u32::from_le_bytes(len_bytes); + let (is_tombstone, body_len) = split_table_payload_row_len(raw_len); + let Some(row_end) = len_field_offset + .checked_add(4) + .and_then(|value| value.checked_add(body_len)) + else { + return Ok(None); + }; + if row_end > logical_len || is_tombstone { + return Ok(None); + } + + patch_bytes.push(( + len_field_offset, + (raw_len | TABLE_PAYLOAD_ROW_TOMBSTONE_FLAG).to_le_bytes(), + )); + } + + let patches = patch_bytes + .iter() + .map(|(offset, bytes)| OverflowBytePatch { + offset: *offset, + bytes: bytes.as_slice(), + }) + .collect::>(); + let (pointer, new_chain_cache, tail, checksum) = + rewrite_overflow_cached_with_sparse_byte_patches( + store, + pointer, + previous_state.checksum, + &chain_cache.page_ids, + &patches, + )?; + Ok(Some(( + PersistedTableState { + pointer, + checksum, + row_count: live_row_count, + tail, + pk_index_root: previous_state.pk_index_root, + }, + new_chain_cache, + ))) +} + +pub(crate) fn splice_deleted_rows_payload_in_place( + payload: &mut Vec, + data: &TableData, + deleted_row_ids: &BTreeSet, +) -> Result>>> { + const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; + + if deleted_row_ids.is_empty() { + return Ok(Some(single_dirty_range(payload.len()..payload.len()))); + } + if payload.len() < HEADER_LEN || payload[..8] != *TABLE_PAYLOAD_MAGIC { + return Ok(None); + } + let old_row_count = + u32::from_le_bytes(payload[8..12].try_into().expect("row-count header length")) as usize; + if old_row_count != data.rows.len().saturating_add(deleted_row_ids.len()) { + return Ok(None); + } + + let mut deleted_spans: Vec<(usize, usize)> = Vec::with_capacity(deleted_row_ids.len()); + let mut remaining = deleted_row_ids.len(); + let mut scan_offset = HEADER_LEN; + let mut scanned_rows = 0usize; + while remaining > 0 && scan_offset + 12 <= payload.len() { + if scanned_rows >= old_row_count { + break; + } + let row_id = i64::from_le_bytes( + payload[scan_offset..scan_offset + 8] + .try_into() + .expect("row id length"), + ); + let row_data_len = u32::from_le_bytes( + payload[scan_offset + 8..scan_offset + 12] + .try_into() + .expect("row data len"), + ) as usize; + let row_end = scan_offset.saturating_add(12).saturating_add(row_data_len); + if row_end > payload.len() { + return Ok(None); + } + if deleted_row_ids.contains(&row_id) { + deleted_spans.push((scan_offset, row_end)); + remaining -= 1; + } + scan_offset = row_end; + scanned_rows += 1; + } + + if remaining > 0 || deleted_spans.len() != deleted_row_ids.len() { + return Ok(None); + } + + let original_len = payload.len(); + let first_deleted_byte = deleted_spans.first().map_or(HEADER_LEN, |span| span.0); + payload[8..12].copy_from_slice( + &u32::try_from(data.rows.len()) + .map_err(|_| DbError::constraint("table row count exceeds u32"))? + .to_le_bytes(), + ); + + let mut copy_from = HEADER_LEN; + let mut write_at = HEADER_LEN; + for (span_start, span_end) in deleted_spans { + if copy_from < span_start { + if copy_from != write_at { + payload.copy_within(copy_from..span_start, write_at); + } + write_at += span_start - copy_from; + } + copy_from = span_end; + } + if copy_from < original_len { + let tail_len = original_len - copy_from; + if copy_from != write_at { + payload.copy_within(copy_from..original_len, write_at); + } + write_at += tail_len; + } + payload.truncate(write_at); + + let mut ranges = single_dirty_range(8..HEADER_LEN); + if !payload.is_empty() { + let tail_dirty_start = first_deleted_byte.saturating_sub(1).min(payload.len()); + if tail_dirty_start < payload.len() { + ranges.push(tail_dirty_start..payload.len()); + } + } + Ok(Some(ranges)) +} + +pub(crate) fn splice_updated_rows_payload( + old: &[u8], + data: &TableData, + dirty_indices: &[usize], +) -> Result { + const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; + + if old.len() < HEADER_LEN || old[..8] != *TABLE_PAYLOAD_MAGIC { + let payload = encode_table_payload(data)?; + let payload_len = payload.len(); + return Ok(SpliceResult { + payload, + first_dirty_byte: 0, + last_dirty_byte: payload_len, + pk_locator_preserved: false, + }); + } + let old_row_count = + u32::from_le_bytes(old[8..12].try_into().expect("row-count header length")) as usize; + if old_row_count != data.rows.len() { + // Row count changed (e.g. concurrent insert/delete after the cache + // was stored) — fall back to full encode for safety. + let payload = encode_table_payload(data)?; + let payload_len = payload.len(); + return Ok(SpliceResult { + payload, + first_dirty_byte: 0, + last_dirty_byte: payload_len, + pk_locator_preserved: false, + }); + } + + // Fast path: only a handful of rows changed. Scan the old payload to + // locate byte ranges of each dirty row, then splice new encodings in. + // + // Row wire format: + // row_id (8 bytes, i64 LE) + // row_data_len (4 bytes, u32 LE) + // row_data (row_data_len bytes) + // + // We need the byte offset of each dirty row (and the one after it) so + // we can copy unchanged prefix / suffix regions. + + // Sort dirty indices so we splice left-to-right. + let mut sorted_dirty: Vec = dirty_indices.to_vec(); + sorted_dirty.sort_unstable(); + sorted_dirty.dedup(); + + // Scan the old payload to locate dirty row byte ranges. + // row_spans[i] = (start, end) byte offsets in `old` for dirty row i. + let mut row_spans: Vec<(usize, usize)> = Vec::with_capacity(sorted_dirty.len()); + let mut scan_offset = HEADER_LEN; + let mut dirty_cursor = 0; + let mut row_idx = 0; + while dirty_cursor < sorted_dirty.len() && scan_offset + 12 <= old.len() { + if row_idx >= old_row_count { + break; + } + let rd_len = u32::from_le_bytes( + old[scan_offset + 8..scan_offset + 12] + .try_into() + .expect("row data len"), + ) as usize; + let row_end = scan_offset + 12 + rd_len; + if row_end > old.len() { + let payload = encode_table_payload(data)?; + let payload_len = payload.len(); + return Ok(SpliceResult { + payload, + first_dirty_byte: 0, + last_dirty_byte: payload_len, + pk_locator_preserved: false, + }); + } + if row_idx == sorted_dirty[dirty_cursor] { + row_spans.push((scan_offset, row_end)); + dirty_cursor += 1; + if dirty_cursor == sorted_dirty.len() { + break; + } + } + scan_offset = row_end; + row_idx += 1; + } + + if row_spans.len() != sorted_dirty.len() { + // Could not find all dirty rows in the old payload; fall back. + let payload = encode_table_payload(data)?; + let payload_len = payload.len(); + return Ok(SpliceResult { + payload, + first_dirty_byte: 0, + last_dirty_byte: payload_len, + pk_locator_preserved: false, + }); + } + + if row_spans.is_empty() { + return Ok(SpliceResult { + payload: old.to_vec(), + first_dirty_byte: 0, + last_dirty_byte: old.len(), + pk_locator_preserved: false, + }); + } + + let mut can_preserve_body = true; + let mut encoded_rows: Vec> = Vec::with_capacity(sorted_dirty.len()); + let mut encoded_row = Vec::with_capacity(128); + for (span_idx, &dirty_row) in sorted_dirty.iter().enumerate() { + if dirty_row >= data.rows.len() { + let payload = encode_table_payload(data)?; + let payload_len = payload.len(); + return Ok(SpliceResult { + payload, + first_dirty_byte: 0, + last_dirty_byte: payload_len, + pk_locator_preserved: false, + }); + } + let (span_start, span_end) = row_spans[span_idx]; + let old_row_body_len = span_end.saturating_sub(span_start).saturating_sub(12); + + let row = &data.rows[dirty_row]; + encoded_row.clear(); + Row::encode_values_into(&row.values, &mut encoded_row)?; + if encoded_row.len() > old_row_body_len { + can_preserve_body = false; + } + encoded_rows.push(encoded_row.clone()); + } + + let mut output = Vec::with_capacity(if can_preserve_body { + old.len() + } else { + old.len().saturating_add(sorted_dirty.len() * 32) + }); + output.extend_from_slice(&old[..8]); // magic + encode_u32(&mut output, data.rows.len() as u32); + + let first_dirty_byte = if can_preserve_body { + row_spans.first().map_or(0, |s| s.0.saturating_add(12)) + } else { + row_spans.first().map_or(0, |s| s.0) + }; + let last_dirty_byte = if can_preserve_body { + row_spans.last().map_or(first_dirty_byte, |s| s.1) + } else { + old.len() + }; + + let mut copy_from = HEADER_LEN; + for (span_idx, &dirty_row) in sorted_dirty.iter().enumerate() { + let (span_start, span_end) = row_spans[span_idx]; + let old_row_body_len = span_end.saturating_sub(span_start).saturating_sub(12); + let encoded_row = &encoded_rows[span_idx]; + + // Copy unchanged bytes before this dirty row. + if copy_from < span_start { + output.extend_from_slice(&old[copy_from..span_start]); + } + + // Encode the updated row. + let row = &data.rows[dirty_row]; + encode_i64(&mut output, row.row_id); + if can_preserve_body { + let row_body_len = u32::try_from(old_row_body_len) + .map_err(|_| DbError::constraint("table row body length exceeds u32"))?; + output.extend_from_slice(&row_body_len.to_le_bytes()); + output.extend_from_slice(encoded_row); + output.extend(std::iter::repeat_n( + 0u8, + old_row_body_len.saturating_sub(encoded_row.len()), + )); + } else { + encode_u32( + &mut output, + u32::try_from(encoded_row.len()) + .map_err(|_| DbError::constraint("table row body length exceeds u32"))?, + ); + output.extend_from_slice(encoded_row); + } + + copy_from = span_end; + } + // Copy any remaining unchanged tail. + if copy_from < old.len() { + output.extend_from_slice(&old[copy_from..]); + } + + Ok(SpliceResult { + payload: output, + first_dirty_byte, + last_dirty_byte, + pk_locator_preserved: can_preserve_body, + }) +} + +pub(crate) fn splice_deleted_rows_payload( + old: &[u8], + data: &TableData, + deleted_row_ids: &BTreeSet, +) -> Result { + const HEADER_LEN: usize = 8 /* magic */ + 4 /* row_count */; + + if deleted_row_ids.is_empty() { + return Ok(SpliceResult { + payload: old.to_vec(), + first_dirty_byte: old.len(), + last_dirty_byte: old.len(), + pk_locator_preserved: false, + }); + } + if old.len() < HEADER_LEN || old[..8] != *TABLE_PAYLOAD_MAGIC { + let payload = encode_table_payload(data)?; + let payload_len = payload.len(); + return Ok(SpliceResult { + payload, + first_dirty_byte: 0, + last_dirty_byte: payload_len, + pk_locator_preserved: false, + }); + } + let old_row_count = + u32::from_le_bytes(old[8..12].try_into().expect("row-count header length")) as usize; + if old_row_count != data.rows.len().saturating_add(deleted_row_ids.len()) { + let payload = encode_table_payload(data)?; + let payload_len = payload.len(); + return Ok(SpliceResult { + payload, + first_dirty_byte: 0, + last_dirty_byte: payload_len, + pk_locator_preserved: false, + }); + } + + let mut deleted_spans: Vec<(usize, usize)> = Vec::with_capacity(deleted_row_ids.len()); + let mut remaining = deleted_row_ids.len(); + let mut scan_offset = HEADER_LEN; + while remaining > 0 && scan_offset + 12 <= old.len() { + let row_id = i64::from_le_bytes( + old[scan_offset..scan_offset + 8] + .try_into() + .expect("row id length"), + ); + let row_data_len = u32::from_le_bytes( + old[scan_offset + 8..scan_offset + 12] + .try_into() + .expect("row data len"), + ) as usize; + let row_end = scan_offset.saturating_add(12).saturating_add(row_data_len); + if row_end > old.len() { + let payload = encode_table_payload(data)?; + let payload_len = payload.len(); + return Ok(SpliceResult { + payload, + first_dirty_byte: 0, + last_dirty_byte: payload_len, + pk_locator_preserved: false, + }); + } + if deleted_row_ids.contains(&row_id) { + deleted_spans.push((scan_offset, row_end)); + remaining -= 1; + } + scan_offset = row_end; + } + + if remaining > 0 || deleted_spans.len() != deleted_row_ids.len() { + let payload = encode_table_payload(data)?; + let payload_len = payload.len(); + return Ok(SpliceResult { + payload, + first_dirty_byte: 0, + last_dirty_byte: payload_len, + pk_locator_preserved: false, + }); + } + + let first_dirty_byte = deleted_spans.first().map_or(0, |span| span.0); + let mut output = Vec::with_capacity(old.len()); + output.extend_from_slice(&old[..TABLE_PAYLOAD_MAGIC.len()]); + encode_u32(&mut output, data.rows.len() as u32); + + let mut copy_from = HEADER_LEN; + for (span_start, span_end) in deleted_spans { + if copy_from < span_start { + output.extend_from_slice(&old[copy_from..span_start]); + } + copy_from = span_end; + } + if copy_from < old.len() { + output.extend_from_slice(&old[copy_from..]); + } + + let last_dirty_byte = output.len(); + Ok(SpliceResult { + payload: output, + first_dirty_byte, + last_dirty_byte, + pk_locator_preserved: false, + }) +} + +pub(crate) fn manifest_chunk_index_for_row_position( + chunks: &[PersistedTableChunkState], + row_position: usize, +) -> Option { + let mut start = 0usize; + for (index, chunk) in chunks.iter().enumerate() { + let end = start.saturating_add(chunk.row_count); + if row_position < end { + return Some(index); + } + start = end; + } + None +} + +impl EngineRuntime { + pub(crate) fn deferred_paged_row_locator_caches_mut( + &mut self, + ) -> &mut BTreeMap> { + Arc::make_mut(&mut self.deferred_paged_row_locator_caches) + } + #[cfg(test)] + pub(crate) fn deferred_paged_row_locator_cache_is_dense_for_tests( + &self, + table_name: &str, + ) -> Option { + let canonical = map_key_ci(self.deferred_paged_row_locator_caches.as_ref(), table_name)?; + self.deferred_paged_row_locator_caches + .get(&canonical) + .map(|cache| cache.locators.is_dense()) + } + #[cfg(test)] + pub(crate) fn deferred_paged_row_locator_cache_sparse_len_for_tests( + &self, + table_name: &str, + ) -> Option { + let canonical = map_key_ci(self.deferred_paged_row_locator_caches.as_ref(), table_name)?; + self.deferred_paged_row_locator_caches + .get(&canonical) + .map(|cache| cache.locators.sparse_len()) + } + pub(crate) fn manifest_payload(&mut self) -> Result<&[u8]> { + let use_template = + self.manifest_template.as_ref().is_some_and(|template| { + template.schema_cookie == self.catalog.schema_cookie + && template.table_next_row_id_offsets.len() == self.catalog.tables.len() + && template.table_state_offsets.len() == self.catalog.tables.len() + && template.table_pk_index_root_offsets.len() == self.catalog.tables.len() + && self.catalog.tables.keys().all(|table_name| { + template.table_next_row_id_offsets.contains_key(table_name) + }) + && self + .catalog + .tables + .keys() + .all(|table_name| template.table_state_offsets.contains_key(table_name)) + && self.catalog.tables.keys().all(|table_name| { + template + .table_pk_index_root_offsets + .contains_key(table_name) + }) + }); + + if !use_template { + let encoded = encode_manifest_payload_with_offsets(self, &self.persisted_tables)?; + self.manifest_template = Some(ManifestTemplate { + schema_cookie: self.catalog.schema_cookie, + table_next_row_id_offsets: encoded.table_next_row_id_offsets, + table_state_offsets: encoded.table_state_offsets, + table_pk_index_root_offsets: encoded.table_pk_index_root_offsets, + bytes: encoded.bytes, + }); + } + + let template = self + .manifest_template + .as_mut() + .ok_or_else(|| DbError::internal("manifest template was not initialized"))?; + for (table_name, offset) in &template.table_next_row_id_offsets { + let next_row_id = self + .catalog + .tables + .get(table_name) + .map(|table| table.next_row_id) + .ok_or_else(|| { + DbError::internal(format!( + "manifest next_row_id offset referenced unknown table {table_name}" + )) + })?; + patch_manifest_table_next_row_id(&mut template.bytes, *offset, next_row_id)?; + } + for (table_name, offset) in &template.table_state_offsets { + let state = self + .persisted_tables + .get(table_name) + .copied() + .unwrap_or_default(); + patch_manifest_table_state(&mut template.bytes, *offset, state)?; + } + for (table_name, offset) in &template.table_pk_index_root_offsets { + let pk_index_root = self + .catalog + .tables + .get(table_name) + .map(|table| table.pk_index_root) + .ok_or_else(|| { + DbError::internal(format!( + "manifest pk_index_root offset referenced unknown table {table_name}" + )) + })?; + patch_manifest_table_pk_index_root(&mut template.bytes, *offset, pk_index_root)?; + } + Ok(template.bytes.as_slice()) + } +} diff --git a/crates/decentdb/src/exec/runtime_eval.rs b/crates/decentdb/src/exec/runtime_eval.rs new file mode 100644 index 00000000..8e2f1a4b --- /dev/null +++ b/crates/decentdb/src/exec/runtime_eval.rs @@ -0,0 +1,2901 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +pub(crate) fn window_key_from_positions(row: &[Value], positions: &[usize]) -> Result> { + if let [position] = positions { + let value = row + .get(*position) + .ok_or_else(|| DbError::internal("window row is shorter than its bindings"))?; + return row_identity(std::slice::from_ref(value)); + } + row_identity(&values_from_positions(row, positions)?) +} + +pub(super) fn compute_index_key( + runtime: &EngineRuntime, + index: &IndexSchema, + table: &TableSchema, + row_values: &[Value], +) -> Result> { + compute_index_key_with_predicate(runtime, index, table, row_values, None) +} + +/// Like [`compute_index_key`], but optionally accepts a pre-parsed predicate +/// expression. See [`prepare_index_predicate_expr`] and +/// [`row_satisfies_index_predicate_with_expr`]. +pub(super) fn compute_index_key_with_predicate( + runtime: &EngineRuntime, + index: &IndexSchema, + table: &TableSchema, + row_values: &[Value], + pre_parsed_predicate: Option<&Expr>, +) -> Result> { + if !row_satisfies_index_predicate_with_expr( + runtime, + index, + table, + row_values, + pre_parsed_predicate, + )? { + return Ok(None); + } + if btree_uses_typed_int64_keys(index, table) { + let [column] = index.columns.as_slice() else { + return Err(DbError::internal( + "typed INT64 runtime indexes require exactly one indexed column", + )); + }; + if let Some(column_name) = &column.column_name { + let position = column_position(table, column_name).ok_or_else(|| { + DbError::constraint(format!("index column {} does not exist", column_name)) + })?; + let Value::Int64(value) = row_values + .get(position) + .ok_or_else(|| DbError::internal("row is shorter than table schema"))? + else { + return Err(DbError::internal( + "typed INT64 runtime index expected an INT64 row value", + )); + }; + return Ok(Some(RuntimeBtreeKey::Int64(*value))); + } + } + if btree_uses_typed_uuid_keys(index, table) { + let [column] = index.columns.as_slice() else { + return Err(DbError::internal( + "typed UUID runtime indexes require exactly one indexed column", + )); + }; + if let Some(column_name) = &column.column_name { + let position = column_position(table, column_name).ok_or_else(|| { + DbError::constraint(format!("index column {} does not exist", column_name)) + })?; + let Value::Uuid(value) = row_values + .get(position) + .ok_or_else(|| DbError::internal("row is shorter than table schema"))? + else { + return Err(DbError::internal( + "typed UUID runtime index expected a UUID row value", + )); + }; + return Ok(Some(RuntimeBtreeKey::Uuid(*value))); + } + } + if let Some(value) = compute_single_column_index_key_fast(index, table, row_values)? { + if index.unique && matches!(value, Value::Null) { + return Ok(None); + } + return Ok(Some(RuntimeBtreeKey::Encoded(encode_runtime_index_key( + value, + )?))); + } + if let Some(positions) = plain_index_column_positions(index, table) { + if positions.len() > 1 { + let values = positions + .iter() + .map(|position| { + row_values + .get(*position) + .cloned() + .ok_or_else(|| DbError::internal("row is shorter than table schema")) + }) + .collect::>>()?; + if index.unique && values.iter().any(|value| matches!(value, Value::Null)) { + return Ok(None); + } + return Ok(Some(RuntimeBtreeKey::Encoded(RuntimeEncodedKey::from_vec( + Row::new(values).encode()?, + )))); + } + } + let values = compute_index_values(runtime, index, table, row_values)?; + if index.unique && values.iter().any(|value| matches!(value, Value::Null)) { + return Ok(None); + } + let key = if values.len() == 1 { + encode_runtime_index_key(&values[0])? + } else { + RuntimeEncodedKey::from_vec(Row::new(values).encode()?) + }; + Ok(Some(RuntimeBtreeKey::Encoded(key))) +} + +/// Fast path for single-column btree indexes whose only column is a plain +/// stored column (no expression, no virtual generated column). Reads the value +/// directly by position without building a `Dataset` or cloning the full row, +/// which is the hot path for index maintenance during bulk DML. +pub(crate) fn compute_single_column_index_key_fast<'a>( + index: &IndexSchema, + table: &TableSchema, + row_values: &'a [Value], +) -> Result> { + if index.columns.len() != 1 { + return Ok(None); + } + let Some(column) = index.columns.first() else { + return Ok(None); + }; + if column.expression_sql.is_some() { + return Ok(None); + } + let Some(column_name) = &column.column_name else { + return Ok(None); + }; + let Some(position) = column_position(table, column_name) else { + return Ok(None); + }; + // Virtual generated columns are not stored in `row_values`, so they must go + // through the materializing path. Stored generated columns are present. + if generated_columns_are_stored(table) { + // All generated columns are stored: safe to read by position. + } else if table + .columns + .get(position) + .is_some_and(|col| col.generated_sql.is_some() && !col.generated_stored) + { + return Ok(None); + } + let Some(value) = row_values.get(position) else { + return Ok(None); + }; + Ok(Some(value)) +} + +pub(super) fn compute_index_values( + runtime: &EngineRuntime, + index: &IndexSchema, + table: &TableSchema, + row_values: &[Value], +) -> Result> { + let row_materialized = if generated_columns_are_stored(table) { + Cow::Borrowed(row_values) + } else { + let mut materialized = row_values.to_vec(); + runtime.apply_virtual_generated_columns(table, &mut materialized)?; + Cow::Owned(materialized) + }; + let row_for_eval = row_materialized.as_ref(); + let dataset = table_row_dataset(table, row_for_eval, &table.name); + let bindings = dataset.rows.first().map(Vec::as_slice).unwrap_or(&[]); + index + .columns + .iter() + .map(|column| { + if let Some(column_name) = &column.column_name { + let position = column_position(table, column_name).ok_or_else(|| { + DbError::constraint(format!("index column {} does not exist", column_name)) + })?; + Ok(row_for_eval[position].clone()) + } else if let Some(expression_sql) = &column.expression_sql { + let expr = crate::sql::parser::parse_expression_sql(expression_sql)?; + runtime.eval_expr(&expr, &dataset, bindings, &[], &BTreeMap::new(), None) + } else { + Err(DbError::constraint("index column definition is empty")) + } + }) + .collect() +} + +pub(crate) fn sort_dataset_by_projection_order( + runtime: Option<&EngineRuntime>, + dataset: &mut Dataset, + order_by: &[SimpleOrderByPlan], +) -> Result<()> { + let mut sort_error = None; + dataset.rows_mut().sort_by(|left, right| { + for order in order_by { + let ordering = compare_values_with_runtime_collation( + runtime, + &left[order.projection_index], + &right[order.projection_index], + order.collation.clone(), + ); + match ordering { + Ok(std::cmp::Ordering::Equal) => continue, + Ok(ordering) => { + return if order.descending { + ordering.reverse() + } else { + ordering + }; + } + Err(error) => { + if sort_error.is_none() { + sort_error = Some(error); + } + return std::cmp::Ordering::Equal; + } + } + } + std::cmp::Ordering::Equal + }); + if let Some(error) = sort_error { + return Err(error); + } + Ok(()) +} + +impl EngineRuntime { + pub(crate) fn evaluate_set_operation( + &self, + op: crate::sql::ast::SetOperation, + all: bool, + left: Dataset, + right: Dataset, + ) -> Result { + if left.columns.len() != right.columns.len() { + return Err(DbError::sql( + "set operations require matching column counts", + )); + } + let columns = left.columns.clone(); + let left_rows = left.into_rows(); + let right_rows = right.into_rows(); + let rows = match op { + crate::sql::ast::SetOperation::Union => { + let mut rows = left_rows; + rows.extend(right_rows); + if !all { + deduplicate_rows(rows)? + } else { + rows + } + } + crate::sql::ast::SetOperation::Intersect => { + let right_counts = count_row_identities(&right_rows)?; + let mut rows = Vec::new(); + if all { + let mut remaining = right_counts; + for row in left_rows { + let identity = row_identity(&row)?; + if consume_row_identity_count(&mut remaining, &identity) { + rows.push(row); + } + } + } else { + for row in left_rows { + let identity = row_identity(&row)?; + if right_counts.contains_key(&identity) { + rows.push(row); + } + } + rows = deduplicate_rows(rows)?; + } + rows + } + crate::sql::ast::SetOperation::Except => { + let right_counts = count_row_identities(&right_rows)?; + let mut rows = Vec::new(); + if all { + let mut remaining = right_counts; + for row in left_rows { + let identity = row_identity(&row)?; + if !consume_row_identity_count(&mut remaining, &identity) { + rows.push(row); + } + } + } else { + for row in left_rows { + let identity = row_identity(&row)?; + if !right_counts.contains_key(&identity) { + rows.push(row); + } + } + rows = deduplicate_rows(rows)?; + } + rows + } + }; + Ok(Dataset::with_rows(columns, rows)) + } + pub(crate) fn project_dataset( + &self, + dataset: &Dataset, + items: &[SelectItem], + params: &[Value], + ctes: &BTreeMap, + excluded: Option<&Dataset>, + ) -> Result { + if !self.security_masks_active()? { + if let Some(projected) = try_project_simple_select_items(dataset, items)? { + return Ok(projected); + } + } + let window_values = self.compute_projection_window_values(dataset, items, params, ctes)?; + let mut columns = Vec::new(); + for (index, item) in items.iter().enumerate() { + match item { + SelectItem::Expr { expr, alias } => columns.push(ColumnBinding::visible( + None, + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + )), + SelectItem::Wildcard => columns.extend( + dataset + .columns + .iter() + .filter(|binding| !binding.hidden) + .map(ColumnBinding::as_output), + ), + SelectItem::QualifiedWildcard(table) => columns.extend( + dataset + .columns + .iter() + .filter(|column| { + !column.hidden && column.table.as_deref() == Some(table.as_str()) + }) + .map(ColumnBinding::as_output), + ), + } + } + let mut rows = Vec::with_capacity(dataset.rows.len()); + for (row_index, row) in dataset.rows.iter().enumerate() { + let mut output = Vec::new(); + for (item_index, item) in items.iter().enumerate() { + match item { + SelectItem::Expr { expr, .. } => match expr { + Expr::RowNumber { .. } | Expr::WindowFunction { .. } => output.push( + window_values[item_index] + .as_ref() + .and_then(|values| values.get(row_index)) + .cloned() + .ok_or_else(|| { + DbError::internal("window-function values were not precomputed") + })?, + ), + Expr::Column { table, column } if excluded.is_none() => { + let binding = dataset + .columns + .iter() + .find(|binding| { + identifiers_equal(&binding.name, column) + && match table { + Some(table) => binding + .table + .as_deref() + .is_some_and(|name| identifiers_equal(name, table)), + None => true, + } + }) + .cloned() + .unwrap_or_else(|| { + ColumnBinding::visible(table.clone(), column.clone()) + }); + let value = + self.eval_expr(expr, dataset, row, params, ctes, excluded)?; + output.push(self.masked_output_value( + &binding, &value, dataset, row, params, ctes, + )?); + } + _ => { + output.push(self.eval_expr(expr, dataset, row, params, ctes, excluded)?) + } + }, + SelectItem::Wildcard => { + for (binding, value) in dataset.columns.iter().zip(row) { + if !binding.hidden { + output.push(self.masked_output_value( + binding, value, dataset, row, params, ctes, + )?); + } + } + } + SelectItem::QualifiedWildcard(table) => { + for (binding, value) in dataset.columns.iter().zip(row) { + if !binding.hidden && binding.table.as_deref() == Some(table.as_str()) { + output.push(self.masked_output_value( + binding, value, dataset, row, params, ctes, + )?); + } + } + } + } + } + rows.push(output); + } + Ok(Dataset::with_rows(columns, rows)) + } + fn compute_projection_window_values( + &self, + dataset: &Dataset, + items: &[SelectItem], + params: &[Value], + ctes: &BTreeMap, + ) -> Result>>> { + let mut window_values = vec![None; items.len()]; + for item_index in 0..items.len() { + if window_values[item_index].is_some() { + continue; + } + match &items[item_index] { + SelectItem::Expr { + expr: + Expr::RowNumber { + partition_by, + order_by, + frame, + }, + .. + } => { + if let Some(peer_index) = Self::find_row_number_lag_peer(items, item_index) { + let (row_number_values, lag_values) = self.compute_row_number_lag_values( + WindowEvalContext { + dataset, + params, + ctes, + }, + partition_by, + order_by, + peer_index, + items, + )?; + window_values[item_index] = Some(row_number_values); + window_values[peer_index] = Some(lag_values); + continue; + } + if let Some(peer_index) = Self::find_row_number_rank_peer(items, item_index) { + let (row_number_values, rank_values) = self + .compute_row_number_rank_values( + dataset, + partition_by, + order_by, + params, + ctes, + )?; + window_values[item_index] = Some(row_number_values); + window_values[peer_index] = Some(rank_values); + continue; + } + window_values[item_index] = Some(self.compute_row_number_values( + dataset, + partition_by, + order_by, + frame.as_ref(), + params, + ctes, + )?); + } + SelectItem::Expr { + expr: + Expr::WindowFunction { + name, + args, + partition_by, + order_by, + frame, + distinct, + star, + }, + .. + } => { + if let Some(peer_index) = Self::find_rank_dense_rank_peer(items, item_index) { + let (rank_values, dense_rank_values) = self + .compute_rank_dense_rank_values( + dataset, + partition_by, + order_by, + params, + ctes, + )?; + if name.eq_ignore_ascii_case("rank") { + window_values[item_index] = Some(rank_values); + window_values[peer_index] = Some(dense_rank_values); + } else { + window_values[item_index] = Some(dense_rank_values); + window_values[peer_index] = Some(rank_values); + } + continue; + } + if name.eq_ignore_ascii_case("lag") { + if let Some(peer_index) = Self::find_lag_row_number_peer(items, item_index) + { + let (row_number_values, lag_values) = self + .compute_row_number_lag_values( + WindowEvalContext { + dataset, + params, + ctes, + }, + partition_by, + order_by, + item_index, + items, + )?; + window_values[item_index] = Some(lag_values); + window_values[peer_index] = Some(row_number_values); + continue; + } + } + if name.eq_ignore_ascii_case("rank") { + if let Some(peer_index) = Self::find_rank_row_number_peer(items, item_index) + { + let (row_number_values, rank_values) = self + .compute_row_number_rank_values( + dataset, + partition_by, + order_by, + params, + ctes, + )?; + window_values[item_index] = Some(rank_values); + window_values[peer_index] = Some(row_number_values); + continue; + } + } + window_values[item_index] = Some(self.compute_window_function_values( + dataset, + name, + args, + partition_by, + order_by, + frame.as_ref(), + *distinct, + *star, + params, + ctes, + )?); + } + _ => {} + } + } + Ok(window_values) + } + fn find_row_number_lag_peer(items: &[SelectItem], item_index: usize) -> Option { + let SelectItem::Expr { + expr: + Expr::RowNumber { + partition_by, + order_by, + frame, + }, + .. + } = items.get(item_index)? + else { + return None; + }; + items.iter().enumerate().find_map(|(peer_index, item)| { + if peer_index == item_index { + return None; + } + let SelectItem::Expr { + expr: + Expr::WindowFunction { + name, + args, + partition_by: peer_partition_by, + order_by: peer_order_by, + frame: peer_frame, + distinct, + star, + }, + .. + } = item + else { + return None; + }; + (!*distinct + && !*star + && name.eq_ignore_ascii_case("lag") + && args.len() == 1 + && peer_partition_by == partition_by + && peer_order_by == order_by + && peer_frame == frame) + .then_some(peer_index) + }) + } + fn find_lag_row_number_peer(items: &[SelectItem], item_index: usize) -> Option { + let SelectItem::Expr { + expr: + Expr::WindowFunction { + name, + args, + partition_by, + order_by, + frame, + distinct, + star, + }, + .. + } = items.get(item_index)? + else { + return None; + }; + if *distinct || *star || !name.eq_ignore_ascii_case("lag") || args.len() != 1 { + return None; + } + items.iter().enumerate().find_map(|(peer_index, item)| { + if peer_index == item_index { + return None; + } + let SelectItem::Expr { + expr: + Expr::RowNumber { + partition_by: peer_partition_by, + order_by: peer_order_by, + frame: peer_frame, + }, + .. + } = item + else { + return None; + }; + (peer_partition_by == partition_by && peer_order_by == order_by && peer_frame == frame) + .then_some(peer_index) + }) + } + fn find_row_number_rank_peer(items: &[SelectItem], item_index: usize) -> Option { + let SelectItem::Expr { + expr: + Expr::RowNumber { + partition_by, + order_by, + frame, + }, + .. + } = items.get(item_index)? + else { + return None; + }; + items.iter().enumerate().find_map(|(peer_index, item)| { + if peer_index == item_index { + return None; + } + let SelectItem::Expr { + expr: + Expr::WindowFunction { + name, + args, + partition_by: peer_partition_by, + order_by: peer_order_by, + frame: peer_frame, + distinct, + star, + }, + .. + } = item + else { + return None; + }; + (!*distinct + && !*star + && args.is_empty() + && name.eq_ignore_ascii_case("rank") + && peer_partition_by == partition_by + && peer_order_by == order_by + && peer_frame == frame) + .then_some(peer_index) + }) + } + fn find_rank_row_number_peer(items: &[SelectItem], item_index: usize) -> Option { + let SelectItem::Expr { + expr: + Expr::WindowFunction { + name, + args, + partition_by, + order_by, + frame, + distinct, + star, + }, + .. + } = items.get(item_index)? + else { + return None; + }; + if *distinct || *star || !args.is_empty() || !name.eq_ignore_ascii_case("rank") { + return None; + } + items.iter().enumerate().find_map(|(peer_index, item)| { + if peer_index == item_index { + return None; + } + let SelectItem::Expr { + expr: + Expr::RowNumber { + partition_by: peer_partition_by, + order_by: peer_order_by, + frame: peer_frame, + }, + .. + } = item + else { + return None; + }; + (peer_partition_by == partition_by && peer_order_by == order_by && peer_frame == frame) + .then_some(peer_index) + }) + } + fn find_rank_dense_rank_peer(items: &[SelectItem], item_index: usize) -> Option { + let SelectItem::Expr { + expr: + Expr::WindowFunction { + name, + args, + partition_by, + order_by, + frame, + distinct, + star, + }, + .. + } = items.get(item_index)? + else { + return None; + }; + if *distinct || *star || !args.is_empty() { + return None; + } + let target_name = if name.eq_ignore_ascii_case("rank") { + "dense_rank" + } else if name.eq_ignore_ascii_case("dense_rank") { + "rank" + } else { + return None; + }; + items.iter().enumerate().find_map(|(peer_index, item)| { + if peer_index == item_index { + return None; + } + let SelectItem::Expr { + expr: + Expr::WindowFunction { + name: peer_name, + args: peer_args, + partition_by: peer_partition_by, + order_by: peer_order_by, + frame: peer_frame, + distinct: peer_distinct, + star: peer_star, + }, + .. + } = item + else { + return None; + }; + (!*peer_distinct + && !*peer_star + && peer_args.is_empty() + && peer_name.eq_ignore_ascii_case(target_name) + && peer_partition_by == partition_by + && peer_order_by == order_by + && peer_frame == frame) + .then_some(peer_index) + }) + } + fn window_partitions( + &self, + dataset: &Dataset, + partition_by: &[Expr], + params: &[Value], + ctes: &BTreeMap, + ) -> Result, Vec>> { + let mut partitions = BTreeMap::, Vec>::new(); + let simple_positions = simple_window_column_positions(dataset, partition_by)?; + for (row_index, row) in dataset.rows.iter().enumerate() { + let key = if partition_by.is_empty() { + vec![0] + } else if let Some(positions) = simple_positions.as_ref() { + window_key_from_positions(row, positions)? + } else { + let values = partition_by + .iter() + .map(|expr| self.eval_expr(expr, dataset, row, params, ctes, None)) + .collect::>>()?; + row_identity(&values)? + }; + partitions.entry(key).or_default().push(row_index); + } + Ok(partitions) + } + fn sorted_window_partition( + &self, + dataset: &Dataset, + indices: Vec, + order_by: &[crate::sql::ast::OrderBy], + params: &[Value], + ctes: &BTreeMap, + ) -> Result> { + let mut sorted = Vec::with_capacity(indices.len()); + let simple_order_positions = simple_window_order_column_positions(dataset, order_by)?; + for row_index in indices { + let row = dataset + .rows + .get(row_index) + .map(Vec::as_slice) + .ok_or_else(|| DbError::internal("window row index is invalid"))?; + let order_keys = if let Some(positions) = simple_order_positions.as_ref() { + values_from_positions(row, positions)? + } else { + order_by + .iter() + .map(|order| self.eval_expr(&order.expr, dataset, row, params, ctes, None)) + .collect::>>()? + }; + sorted.push(WindowSortedRow { + row_index, + order_keys, + }); + } + sorted.sort_by(|left, right| compare_window_sorted_rows(left, right, order_by)); + Ok(sorted) + } + fn compute_sliding_rows_avg_values( + &self, + ctx: WindowEvalContext<'_>, + sorted: &[usize], + arg: &Expr, + preceding: usize, + results: &mut [Value], + ) -> Result<()> { + let ordered_values = sorted + .iter() + .map(|row_index| { + let row = ctx + .dataset + .rows + .get(*row_index) + .map(Vec::as_slice) + .ok_or_else(|| DbError::internal("window row index is invalid"))?; + self.eval_expr(arg, ctx.dataset, row, ctx.params, ctx.ctes, None) + }) + .collect::>>()?; + + for (ordinal, row_index) in sorted.iter().enumerate() { + let start = ordinal.saturating_sub(preceding); + let mut total_float = 0_f64; + let mut count = 0_i64; + for value in &ordered_values[start..=ordinal] { + match value { + Value::Null => {} + Value::Int64(value) => { + total_float += *value as f64; + count += 1; + } + Value::Float64(value) => { + total_float += *value; + count += 1; + } + Value::Decimal { scaled, scale } => { + total_float += (*scaled as f64) / 10_f64.powi(i32::from(*scale)); + count += 1; + } + other => { + return Err(DbError::sql(format!( + "numeric aggregate does not support {other:?}" + ))) + } + } + } + results[*row_index] = if count == 0 { + Value::Null + } else { + Value::Float64(total_float / count as f64) + }; + } + Ok(()) + } + fn compute_rank_dense_rank_values( + &self, + dataset: &Dataset, + partition_by: &[Expr], + order_by: &[crate::sql::ast::OrderBy], + params: &[Value], + ctes: &BTreeMap, + ) -> Result<(Vec, Vec)> { + let partitions = self.window_partitions(dataset, partition_by, params, ctes)?; + + let mut rank_results = vec![Value::Null; dataset.rows.len()]; + let mut dense_rank_results = vec![Value::Null; dataset.rows.len()]; + for indices in partitions.into_values() { + let sorted = self.sorted_window_partition(dataset, indices, order_by, params, ctes)?; + let mut current_rank = 1_i64; + let mut current_dense_rank = 1_i64; + for (ordinal, sorted_row) in sorted.iter().enumerate() { + if ordinal > 0 + && !window_order_keys_equal( + &sorted[ordinal - 1].order_keys, + &sorted_row.order_keys, + )? + { + current_rank = (ordinal + 1) as i64; + current_dense_rank += 1; + } + rank_results[sorted_row.row_index] = Value::Int64(current_rank); + dense_rank_results[sorted_row.row_index] = Value::Int64(current_dense_rank); + } + } + Ok((rank_results, dense_rank_results)) + } + fn compute_row_number_rank_values( + &self, + dataset: &Dataset, + partition_by: &[Expr], + order_by: &[crate::sql::ast::OrderBy], + params: &[Value], + ctes: &BTreeMap, + ) -> Result<(Vec, Vec)> { + let partitions = self.window_partitions(dataset, partition_by, params, ctes)?; + + let mut row_number_results = vec![Value::Null; dataset.rows.len()]; + let mut rank_results = vec![Value::Null; dataset.rows.len()]; + for indices in partitions.into_values() { + let sorted = self.sorted_window_partition(dataset, indices, order_by, params, ctes)?; + let mut current_rank = 1_i64; + for (ordinal, sorted_row) in sorted.iter().enumerate() { + if ordinal > 0 + && !window_order_keys_equal( + &sorted[ordinal - 1].order_keys, + &sorted_row.order_keys, + )? + { + current_rank = (ordinal + 1) as i64; + } + row_number_results[sorted_row.row_index] = Value::Int64((ordinal + 1) as i64); + rank_results[sorted_row.row_index] = Value::Int64(current_rank); + } + } + Ok((row_number_results, rank_results)) + } + fn compute_row_number_lag_values( + &self, + context: WindowEvalContext<'_>, + partition_by: &[Expr], + order_by: &[crate::sql::ast::OrderBy], + lag_item_index: usize, + items: &[SelectItem], + ) -> Result<(Vec, Vec)> { + let dataset = context.dataset; + let SelectItem::Expr { + expr: Expr::WindowFunction { args, .. }, + .. + } = items + .get(lag_item_index) + .ok_or_else(|| DbError::internal("window lag item index is invalid"))? + else { + return Err(DbError::internal("window lag item index is invalid")); + }; + let lag_expr = args + .first() + .ok_or_else(|| DbError::internal("window lag expression is missing"))?; + let partitions = + self.window_partitions(dataset, partition_by, context.params, context.ctes)?; + + let mut row_number_results = vec![Value::Null; dataset.rows.len()]; + let mut lag_results = vec![Value::Null; dataset.rows.len()]; + for indices in partitions.into_values() { + let sorted = self.sorted_window_partition( + dataset, + indices, + order_by, + context.params, + context.ctes, + )?; + + let ordered_values = sorted + .iter() + .map(|sorted_row| { + self.eval_expr( + lag_expr, + dataset, + &dataset.rows[sorted_row.row_index], + context.params, + context.ctes, + None, + ) + }) + .collect::>>()?; + for (ordinal, sorted_row) in sorted.iter().enumerate() { + row_number_results[sorted_row.row_index] = Value::Int64((ordinal + 1) as i64); + lag_results[sorted_row.row_index] = ordinal + .checked_sub(1) + .and_then(|previous| ordered_values.get(previous)) + .cloned() + .unwrap_or(Value::Null); + } + } + Ok((row_number_results, lag_results)) + } + fn compute_row_number_values( + &self, + dataset: &Dataset, + partition_by: &[Expr], + order_by: &[crate::sql::ast::OrderBy], + _frame: Option<&crate::sql::ast::WindowFrame>, + params: &[Value], + ctes: &BTreeMap, + ) -> Result> { + let partitions = self.window_partitions(dataset, partition_by, params, ctes)?; + + let mut row_numbers = vec![Value::Null; dataset.rows.len()]; + for indices in partitions.into_values() { + let sorted = self.sorted_window_partition(dataset, indices, order_by, params, ctes)?; + + for (ordinal, sorted_row) in sorted.into_iter().enumerate() { + row_numbers[sorted_row.row_index] = Value::Int64((ordinal + 1) as i64); + } + } + Ok(row_numbers) + } + #[allow(clippy::too_many_arguments)] + fn compute_window_function_values( + &self, + dataset: &Dataset, + name: &str, + args: &[Expr], + partition_by: &[Expr], + order_by: &[crate::sql::ast::OrderBy], + frame: Option<&crate::sql::ast::WindowFrame>, + _distinct: bool, + _star: bool, + params: &[Value], + ctes: &BTreeMap, + ) -> Result> { + let partitions = self.window_partitions(dataset, partition_by, params, ctes)?; + + let mut results = vec![Value::Null; dataset.rows.len()]; + for indices in partitions.into_values() { + let sorted_rows = + self.sorted_window_partition(dataset, indices, order_by, params, ctes)?; + let sorted = sorted_rows + .iter() + .map(|row| row.row_index) + .collect::>(); + let order_keys = sorted_rows + .iter() + .map(|row| row.order_keys.clone()) + .collect::>(); + if name == "avg" && !_distinct && !_star && args.len() == 1 { + if let Some(preceding) = rows_preceding_current_frame(frame) { + self.compute_sliding_rows_avg_values( + WindowEvalContext { + dataset, + params, + ctes, + }, + &sorted, + &args[0], + preceding, + &mut results, + )?; + continue; + } + } + let (peer_starts, peer_ends) = compute_window_peer_bounds(&order_keys)?; + + match name { + "rank" => { + if _distinct || _star { + return Err(DbError::sql("RANK does not support DISTINCT or *")); + } + let mut current_rank = 1_i64; + for (ordinal, row_index) in sorted.iter().enumerate() { + if ordinal > 0 + && !window_order_keys_equal( + &order_keys[ordinal - 1], + &order_keys[ordinal], + )? + { + current_rank = (ordinal + 1) as i64; + } + results[*row_index] = Value::Int64(current_rank); + } + } + "dense_rank" => { + if _distinct || _star { + return Err(DbError::sql("DENSE_RANK does not support DISTINCT or *")); + } + let mut current_rank = 1_i64; + for (ordinal, row_index) in sorted.iter().enumerate() { + if ordinal > 0 + && !window_order_keys_equal( + &order_keys[ordinal - 1], + &order_keys[ordinal], + )? + { + current_rank += 1; + } + results[*row_index] = Value::Int64(current_rank); + } + } + "percent_rank" => { + if _distinct || _star || !args.is_empty() { + return Err(DbError::sql("PERCENT_RANK expects no arguments")); + } + if sorted.len() == 1 { + results[sorted[0]] = Value::Float64(0.0); + continue; + } + let mut current_rank = 1_i64; + let denominator = (sorted.len() - 1) as f64; + for (ordinal, row_index) in sorted.iter().enumerate() { + if ordinal > 0 + && !window_order_keys_equal( + &order_keys[ordinal - 1], + &order_keys[ordinal], + )? + { + current_rank = (ordinal + 1) as i64; + } + let value = (current_rank - 1) as f64 / denominator; + results[*row_index] = Value::Float64(value); + } + } + "cume_dist" => { + if _distinct || _star || !args.is_empty() { + return Err(DbError::sql("CUME_DIST expects no arguments")); + } + let partition_len = sorted.len() as f64; + let mut ordinal = 0_usize; + while ordinal < sorted.len() { + let peer_end = peer_ends[ordinal]; + let value = Value::Float64((peer_end + 1) as f64 / partition_len); + for peer_ordinal in ordinal..=peer_end { + results[sorted[peer_ordinal]] = value.clone(); + } + ordinal = peer_end + 1; + } + } + "ntile" => { + if _distinct || _star || args.len() != 1 { + return Err(DbError::sql("NTILE expects exactly 1 argument")); + } + let first_row = dataset + .rows + .get(sorted[0]) + .map(Vec::as_slice) + .ok_or_else(|| DbError::internal("window row index is invalid"))?; + let buckets = match self + .eval_expr(&args[0], dataset, first_row, params, ctes, None)? + { + Value::Int64(value) if value > 0 => usize::try_from(value) + .map_err(|_| DbError::sql("NTILE bucket count is out of range"))?, + Value::Int64(_) => { + return Err(DbError::sql("NTILE bucket count must be greater than 0")) + } + Value::Null => { + return Err(DbError::sql("NTILE bucket count cannot be NULL")) + } + other => { + return Err(DbError::sql(format!( + "NTILE bucket count must be INT64, got {other:?}" + ))) + } + }; + let partition_len = sorted.len(); + let base_size = partition_len / buckets; + let extra = partition_len % buckets; + for (ordinal, row_index) in sorted.iter().enumerate() { + let bucket = if ordinal < (base_size + 1) * extra { + (ordinal / (base_size + 1)) + 1 + } else { + ((ordinal - (base_size + 1) * extra) / base_size.max(1)) + extra + 1 + }; + results[*row_index] = Value::Int64(bucket as i64); + } + } + "lag" | "lead" => { + if _distinct || _star { + return Err(DbError::sql(format!( + "{} does not support DISTINCT or *", + name.to_ascii_uppercase() + ))); + } + if args.is_empty() || args.len() > 3 { + return Err(DbError::sql(format!( + "{} expects 1 to 3 arguments", + name.to_ascii_uppercase() + ))); + } + let offset = match args.get(1) { + Some(expr) => { + match self.eval_expr(expr, dataset, &[], params, ctes, None)? { + Value::Int64(value) if value >= 0 => value as usize, + Value::Int64(_) => { + return Err(DbError::sql(format!( + "{} offset must be non-negative", + name.to_ascii_uppercase() + ))) + } + other => { + return Err(DbError::sql(format!( + "{} offset must be INT64, got {other:?}", + name.to_ascii_uppercase() + ))) + } + } + } + None => 1, + }; + let ordered_values = sorted + .iter() + .map(|row_index| { + self.eval_expr( + &args[0], + dataset, + &dataset.rows[*row_index], + params, + ctes, + None, + ) + }) + .collect::>>()?; + for (ordinal, row_index) in sorted.iter().enumerate() { + let target_ordinal = if name == "lag" { + ordinal.checked_sub(offset) + } else { + ordinal + .checked_add(offset) + .filter(|target| *target < sorted.len()) + }; + results[*row_index] = if let Some(target_ordinal) = target_ordinal { + ordered_values[target_ordinal].clone() + } else if let Some(default_expr) = args.get(2) { + self.eval_expr( + default_expr, + dataset, + &dataset.rows[*row_index], + params, + ctes, + None, + )? + } else { + Value::Null + }; + } + } + "first_value" | "last_value" => { + if _distinct || _star { + return Err(DbError::sql(format!( + "{} does not support DISTINCT or *", + name.to_ascii_uppercase() + ))); + } + if args.len() != 1 { + return Err(DbError::sql(format!( + "{} expects exactly 1 argument", + name.to_ascii_uppercase() + ))); + } + let ordered_values = sorted + .iter() + .map(|row_index| { + self.eval_expr( + &args[0], + dataset, + &dataset.rows[*row_index], + params, + ctes, + None, + ) + }) + .collect::>>()?; + for (ordinal, row_index) in sorted.iter().enumerate() { + let frame_range = self.window_frame_bounds_for_row( + dataset, + &sorted, + order_by, + &peer_starts, + &peer_ends, + ordinal, + frame, + params, + ctes, + )?; + results[*row_index] = if let Some((frame_start, frame_end)) = frame_range { + if name == "first_value" { + ordered_values[frame_start].clone() + } else { + ordered_values[frame_end].clone() + } + } else { + Value::Null + }; + } + } + "nth_value" => { + if _distinct || _star { + return Err(DbError::sql("NTH_VALUE does not support DISTINCT or *")); + } + if args.len() != 2 { + return Err(DbError::sql( + "NTH_VALUE expects exactly 2 arguments".to_string(), + )); + } + let position = + match self.eval_expr(&args[1], dataset, &[], params, ctes, None)? { + Value::Int64(value) if value >= 1 => value as usize, + Value::Int64(_) => { + return Err(DbError::sql("NTH_VALUE position must be >= 1")) + } + other => { + return Err(DbError::sql(format!( + "NTH_VALUE position must be INT64, got {other:?}" + ))) + } + }; + let ordered_values = sorted + .iter() + .map(|row_index| { + self.eval_expr( + &args[0], + dataset, + &dataset.rows[*row_index], + params, + ctes, + None, + ) + }) + .collect::>>()?; + for (ordinal, row_index) in sorted.iter().enumerate() { + let frame_range = self.window_frame_bounds_for_row( + dataset, + &sorted, + order_by, + &peer_starts, + &peer_ends, + ordinal, + frame, + params, + ctes, + )?; + results[*row_index] = if let Some((frame_start, frame_end)) = frame_range { + frame_start + .checked_add(position.saturating_sub(1)) + .filter(|index| *index <= frame_end) + .and_then(|index| ordered_values.get(index)) + .cloned() + .unwrap_or(Value::Null) + } else { + Value::Null + }; + } + } + "count" | "sum" | "avg" | "min" | "max" | "total" | "stddev" | "stddev_samp" + | "stddev_pop" | "variance" | "var_samp" | "var_pop" | "bool_and" | "bool_or" + | "group_concat" | "string_agg" => { + for (ordinal, row_index) in sorted.iter().enumerate() { + let frame_range = self.window_frame_bounds_for_row( + dataset, + &sorted, + order_by, + &peer_starts, + &peer_ends, + ordinal, + frame, + params, + ctes, + )?; + results[*row_index] = self.eval_window_aggregate( + name, + args, + _distinct, + _star, + dataset, + &sorted, + frame_range, + params, + ctes, + )?; + } + } + other => { + return Err(DbError::sql(format!( + "unsupported window function {}", + other.to_ascii_uppercase() + ))) + } + } + } + Ok(results) + } + #[allow(clippy::too_many_arguments)] + fn window_frame_bounds_for_row( + &self, + dataset: &Dataset, + sorted: &[usize], + order_by: &[crate::sql::ast::OrderBy], + peer_starts: &[usize], + peer_ends: &[usize], + ordinal: usize, + frame: Option<&crate::sql::ast::WindowFrame>, + params: &[Value], + ctes: &BTreeMap, + ) -> Result> { + if sorted.is_empty() { + return Ok(None); + } + + if frame.is_none() { + if order_by.is_empty() { + return Ok(Some((0, sorted.len() - 1))); + } + return Ok(Some((0, peer_ends[ordinal]))); + } + + let frame = frame.ok_or_else(|| DbError::internal("window frame is missing"))?; + let row_index = *sorted + .get(ordinal) + .ok_or_else(|| DbError::internal("window row index is invalid"))?; + let row = dataset + .rows + .get(row_index) + .map(Vec::as_slice) + .ok_or_else(|| DbError::internal("window row index is invalid"))?; + let default_end = crate::sql::ast::WindowFrameBound::CurrentRow; + let end_bound = frame.end.as_ref().unwrap_or(&default_end); + let start = self.window_frame_bound_index( + dataset, + row, + &frame.start, + true, + ordinal, + sorted.len(), + peer_starts, + peer_ends, + frame.unit, + params, + ctes, + )?; + let end = self.window_frame_bound_index( + dataset, + row, + end_bound, + false, + ordinal, + sorted.len(), + peer_starts, + peer_ends, + frame.unit, + params, + ctes, + )?; + normalize_window_frame_range(start, end, sorted.len()) + } + #[allow(clippy::too_many_arguments)] + fn window_frame_bound_index( + &self, + dataset: &Dataset, + row: &[Value], + bound: &crate::sql::ast::WindowFrameBound, + start: bool, + ordinal: usize, + partition_len: usize, + peer_starts: &[usize], + peer_ends: &[usize], + unit: crate::sql::ast::WindowFrameUnit, + params: &[Value], + ctes: &BTreeMap, + ) -> Result { + let partition_len = i64::try_from(partition_len) + .map_err(|_| DbError::internal("window partition is too large"))?; + let ordinal = + i64::try_from(ordinal).map_err(|_| DbError::internal("window ordinal is too large"))?; + match (unit, bound) { + ( + crate::sql::ast::WindowFrameUnit::Range, + crate::sql::ast::WindowFrameBound::Preceding(_) + | crate::sql::ast::WindowFrameBound::Following(_), + ) => Err(DbError::sql( + "RANGE frames with offset bounds are not supported yet", + )), + (_, crate::sql::ast::WindowFrameBound::UnboundedPreceding) => Ok(0), + (_, crate::sql::ast::WindowFrameBound::UnboundedFollowing) => { + if start { + Ok(partition_len) + } else { + Ok(partition_len - 1) + } + } + ( + crate::sql::ast::WindowFrameUnit::Rows, + crate::sql::ast::WindowFrameBound::CurrentRow, + ) => Ok(ordinal), + ( + crate::sql::ast::WindowFrameUnit::Range, + crate::sql::ast::WindowFrameBound::CurrentRow, + ) => { + if start { + i64::try_from(peer_starts[ordinal as usize]) + .map_err(|_| DbError::internal("window peer start is too large")) + } else { + i64::try_from(peer_ends[ordinal as usize]) + .map_err(|_| DbError::internal("window peer end is too large")) + } + } + ( + crate::sql::ast::WindowFrameUnit::Rows, + crate::sql::ast::WindowFrameBound::Preceding(offset), + ) => { + let offset = self.eval_window_frame_offset(dataset, row, offset, params, ctes)?; + Ok(ordinal - offset) + } + ( + crate::sql::ast::WindowFrameUnit::Rows, + crate::sql::ast::WindowFrameBound::Following(offset), + ) => { + let offset = self.eval_window_frame_offset(dataset, row, offset, params, ctes)?; + Ok(ordinal + offset) + } + } + } + fn eval_window_frame_offset( + &self, + dataset: &Dataset, + row: &[Value], + offset: &Expr, + params: &[Value], + ctes: &BTreeMap, + ) -> Result { + match self.eval_expr(offset, dataset, row, params, ctes, None)? { + Value::Int64(value) if value >= 0 => Ok(value), + Value::Int64(_) => Err(DbError::sql( + "window frame offset must be a non-negative integer", + )), + Value::Null => Err(DbError::sql("window frame offset cannot be NULL")), + other => Err(DbError::sql(format!( + "window frame offset must be INT64, got {other:?}" + ))), + } + } + #[allow(clippy::too_many_arguments)] + fn eval_window_aggregate( + &self, + name: &str, + args: &[Expr], + distinct: bool, + star: bool, + dataset: &Dataset, + sorted_partition: &[usize], + frame_range: Option<(usize, usize)>, + params: &[Value], + ctes: &BTreeMap, + ) -> Result { + let aggregate_ctx = AggregateEvalContext { + runtime: self, + dataset, + params, + ctes, + }; + let empty_indexes: [usize; 0] = []; + let row_indexes = if let Some((start, end)) = frame_range { + sorted_partition + .get(start..=end) + .ok_or_else(|| DbError::internal("window frame range is invalid"))? + } else { + &empty_indexes + }; + + match name { + "count" => { + if star { + if distinct { + return Err(DbError::sql("COUNT(DISTINCT *) is not supported")); + } + return Ok(Value::Int64(row_indexes.len() as i64)); + } + if args.len() != 1 { + return Err(DbError::sql("COUNT expects exactly 1 argument")); + } + if distinct { + let mut vals = Vec::new(); + for row_index in row_indexes { + let row = dataset + .rows + .get(*row_index) + .map(Vec::as_slice) + .ok_or_else(|| DbError::internal("window row index is invalid"))?; + let val = self.eval_expr(&args[0], dataset, row, params, ctes, None)?; + if !matches!(val, Value::Null) { + vals.push(val); + } + } + vals.sort_by(|a, b| compare_values(a, b).unwrap_or(std::cmp::Ordering::Equal)); + vals.dedup_by(|a, b| { + compare_values(a, b).unwrap_or(std::cmp::Ordering::Equal) + == std::cmp::Ordering::Equal + }); + Ok(Value::Int64(vals.len() as i64)) + } else { + let mut count = 0_i64; + for row_index in row_indexes { + let row = dataset + .rows + .get(*row_index) + .map(Vec::as_slice) + .ok_or_else(|| DbError::internal("window row index is invalid"))?; + if !matches!( + self.eval_expr(&args[0], dataset, row, params, ctes, None)?, + Value::Null + ) { + count += 1; + } + } + Ok(Value::Int64(count)) + } + } + "sum" => { + if star || args.len() != 1 { + return Err(DbError::sql("SUM expects exactly 1 argument")); + } + aggregate_numeric( + &aggregate_ctx, + row_indexes, + &args[0], + NumericAgg::Sum, + distinct, + ) + } + "avg" => { + if star || args.len() != 1 { + return Err(DbError::sql("AVG expects exactly 1 argument")); + } + aggregate_numeric( + &aggregate_ctx, + row_indexes, + &args[0], + NumericAgg::Avg, + distinct, + ) + } + "total" => { + if star || args.len() != 1 { + return Err(DbError::sql("TOTAL expects exactly 1 argument")); + } + aggregate_numeric( + &aggregate_ctx, + row_indexes, + &args[0], + NumericAgg::Total, + distinct, + ) + } + "stddev" | "stddev_samp" => { + if star || args.len() != 1 { + return Err(DbError::sql("STDDEV expects exactly 1 argument")); + } + aggregate_variance( + &aggregate_ctx, + row_indexes, + &args[0], + VarianceAgg::StddevSamp, + distinct, + ) + } + "stddev_pop" => { + if star || args.len() != 1 { + return Err(DbError::sql("STDDEV_POP expects exactly 1 argument")); + } + aggregate_variance( + &aggregate_ctx, + row_indexes, + &args[0], + VarianceAgg::StddevPop, + distinct, + ) + } + "variance" | "var_samp" => { + if star || args.len() != 1 { + return Err(DbError::sql("VAR_SAMP expects exactly 1 argument")); + } + aggregate_variance( + &aggregate_ctx, + row_indexes, + &args[0], + VarianceAgg::VarSamp, + distinct, + ) + } + "var_pop" => { + if star || args.len() != 1 { + return Err(DbError::sql("VAR_POP expects exactly 1 argument")); + } + aggregate_variance( + &aggregate_ctx, + row_indexes, + &args[0], + VarianceAgg::VarPop, + distinct, + ) + } + "bool_and" => { + if star || args.len() != 1 { + return Err(DbError::sql("BOOL_AND expects exactly 1 argument")); + } + aggregate_bool( + &aggregate_ctx, + row_indexes, + &args[0], + BoolAgg::And, + distinct, + ) + } + "bool_or" => { + if star || args.len() != 1 { + return Err(DbError::sql("BOOL_OR expects exactly 1 argument")); + } + aggregate_bool(&aggregate_ctx, row_indexes, &args[0], BoolAgg::Or, distinct) + } + "min" => { + if star || args.len() != 1 { + return Err(DbError::sql("MIN expects exactly 1 argument")); + } + aggregate_extreme(self, dataset, row_indexes, &args[0], params, ctes, true) + } + "max" => { + if star || args.len() != 1 { + return Err(DbError::sql("MAX expects exactly 1 argument")); + } + aggregate_extreme(self, dataset, row_indexes, &args[0], params, ctes, false) + } + name @ ("group_concat" | "string_agg") => { + if star { + return Err(DbError::sql(format!( + "{} does not support *", + name.to_ascii_uppercase() + ))); + } + if distinct { + return Err(DbError::sql(format!( + "{} DISTINCT is not supported in window context", + name.to_ascii_uppercase() + ))); + } + aggregate_group_concat(&aggregate_ctx, row_indexes, args, false, &[], name) + } + other => Err(DbError::sql(format!( + "unsupported aggregate window function {other}" + ))), + } + } + pub(crate) fn evaluate_grouped_select( + &self, + select: &Select, + dataset: Dataset, + params: &[Value], + ctes: &BTreeMap, + ) -> Result { + if select.group_by.iter().any(expr_contains_collation) { + return Err(DbError::sql( + "COLLATE in GROUP BY keys is not supported in this compatibility slice", + )); + } + let mut groups = BTreeMap::, Vec>::new(); + if dataset.rows.is_empty() && select.group_by.is_empty() { + groups.insert(Vec::new(), Vec::new()); + } else { + for (row_index, row) in dataset.rows.iter().enumerate() { + let key_values = select + .group_by + .iter() + .map(|expr| self.eval_expr(expr, &dataset, row, params, ctes, None)) + .collect::>>()?; + groups + .entry(row_identity(&key_values)?) + .or_default() + .push(row_index); + } + } + let columns = select + .projection + .iter() + .enumerate() + .map(|(index, item)| match item { + SelectItem::Expr { expr, alias } => ColumnBinding::visible( + None, + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + ), + SelectItem::Wildcard => ColumnBinding::visible(None, format!("col{}", index + 1)), + SelectItem::QualifiedWildcard(_) => { + ColumnBinding::visible(None, format!("col{}", index + 1)) + } + }) + .collect::>(); + let mut rows = Vec::new(); + for group_row_indexes in groups.into_values() { + if let Some(having) = &select.having { + if !matches!( + self.eval_group_expr(having, &dataset, &group_row_indexes, params, ctes)?, + Value::Bool(true) + ) { + continue; + } + } + let mut output = Vec::new(); + for item in &select.projection { + match item { + SelectItem::Expr { expr, .. } => output.push(self.eval_group_expr( + expr, + &dataset, + &group_row_indexes, + params, + ctes, + )?), + SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => { + return Err(DbError::sql( + "wildcards are not supported in grouped SELECT output", + )) + } + } + } + rows.push(output); + } + Ok(Dataset::with_rows(columns, rows)) + } + pub(crate) fn sort_dataset( + &self, + dataset: &mut Dataset, + order_by: &[crate::sql::ast::OrderBy], + params: &[Value], + ctes: &BTreeMap, + ) -> Result<()> { + if order_by.is_empty() || dataset.rows.len() <= 1 { + return Ok(()); + } + let eval_dataset = Dataset::with_rows(dataset.columns.clone(), Vec::new()); + let projected_order_indexes = order_by + .iter() + .map(|order| projected_dataset_order_column_index(dataset, &order.expr)) + .collect::>(); + let sort_keys = dataset + .rows + .iter() + .map(|row| { + order_by + .iter() + .zip(&projected_order_indexes) + .map(|(order, projected_index)| { + if let Some(index) = projected_index { + row.get(*index).cloned().unwrap_or(Value::Null) + } else { + self.eval_expr(&order.expr, &eval_dataset, row, params, ctes, None) + .unwrap_or(Value::Null) + } + }) + .collect::>() + }) + .collect::>(); + + let mut sort_error = None; + let mut order = (0..dataset.rows.len()).collect::>(); + order.sort_by(|left_index, right_index| { + let left_key = &sort_keys[*left_index]; + let right_key = &sort_keys[*right_index]; + for (order_clause, (left_value, right_value)) in + order_by.iter().zip(left_key.iter().zip(right_key.iter())) + { + let ordering = match compare_values_with_runtime_collation( + Some(self), + left_value, + right_value, + order_clause.collation.clone(), + ) { + Ok(ordering) => ordering, + Err(error) => { + if sort_error.is_none() { + sort_error = Some(error); + } + std::cmp::Ordering::Equal + } + }; + if ordering != std::cmp::Ordering::Equal { + return if order_clause.descending { + ordering.reverse() + } else { + ordering + }; + } + } + left_index.cmp(right_index) + }); + if let Some(error) = sort_error { + return Err(error); + } + + let mut rows = dataset + .take_rows() + .into_iter() + .map(Some) + .collect::>(); + dataset.set_rows( + order + .into_iter() + .map(|row_index| { + rows.get_mut(row_index) + .and_then(Option::take) + .ok_or_else(|| DbError::internal("sorted row index is invalid")) + }) + .collect::>>()?, + ); + Ok(()) + } + pub(crate) fn eval_constant_i64( + &self, + expr: &Expr, + params: &[Value], + ctes: &BTreeMap, + ) -> Result { + match self.eval_expr(expr, &Dataset::empty(), &[], params, ctes, None)? { + Value::Int64(value) => Ok(value), + other => Err(DbError::sql(format!( + "expected integer constant, got {other:?}" + ))), + } + } + fn eval_group_membership_value( + &self, + expr: &Expr, + dataset: &Dataset, + group_row_indexes: &[usize], + params: &[Value], + ctes: &BTreeMap, + ) -> Result { + match expr { + Expr::Row(items) => Ok(MembershipValue::Row( + items + .iter() + .map(|item| { + self.eval_group_expr(item, dataset, group_row_indexes, params, ctes) + }) + .collect::>>()?, + )), + _ => Ok(MembershipValue::Scalar(self.eval_group_expr( + expr, + dataset, + group_row_indexes, + params, + ctes, + )?)), + } + } + fn eval_membership_value( + &self, + expr: &Expr, + dataset: &Dataset, + row: &[Value], + params: &[Value], + ctes: &BTreeMap, + excluded: Option<&Dataset>, + ) -> Result { + match expr { + Expr::Row(items) => Ok(MembershipValue::Row( + items + .iter() + .map(|item| self.eval_expr(item, dataset, row, params, ctes, excluded)) + .collect::>>()?, + )), + _ => Ok(MembershipValue::Scalar( + self.eval_expr(expr, dataset, row, params, ctes, excluded)?, + )), + } + } + pub(crate) fn eval_group_expr( + &self, + expr: &Expr, + dataset: &Dataset, + group_row_indexes: &[usize], + params: &[Value], + ctes: &BTreeMap, + ) -> Result { + let aggregate_ctx = AggregateEvalContext { + runtime: self, + dataset, + params, + ctes, + }; + match expr { + Expr::Aggregate { + name, + args, + star, + distinct, + order_by, + within_group, + } => match name.as_str() { + "array_agg" | "median" | "percentile_cont" | "percentile_disc" => { + match name.as_str() { + "array_agg" => { + if *within_group { + return Err(DbError::sql( + "ARRAY_AGG does not support WITHIN GROUP", + )); + } + if *star || args.len() != 1 { + return Err(DbError::sql("ARRAY_AGG expects exactly 1 argument")); + } + aggregate_array_agg( + &aggregate_ctx, + group_row_indexes, + &args[0], + *distinct, + order_by, + ) + } + "median" => { + if *within_group { + return Err(DbError::sql( + "MEDIAN does not support WITHIN GROUP; use MEDIAN(expr)", + )); + } + if !order_by.is_empty() { + return Err(DbError::sql( + "MEDIAN does not support aggregate ORDER BY", + )); + } + if *star || args.len() != 1 { + return Err(DbError::sql("MEDIAN expects exactly 1 argument")); + } + aggregate_median(&aggregate_ctx, group_row_indexes, &args[0], *distinct) + } + "percentile_cont" => { + if !*within_group { + return Err(DbError::sql( + "PERCENTILE_CONT requires WITHIN GROUP (ORDER BY ...)", + )); + } + if *distinct { + return Err(DbError::sql( + "PERCENTILE_CONT does not support DISTINCT", + )); + } + if *star || args.len() != 1 { + return Err(DbError::sql( + "PERCENTILE_CONT expects exactly 1 argument", + )); + } + aggregate_percentile_cont( + self, + dataset, + group_row_indexes, + &args[0], + order_by, + params, + ctes, + ) + } + "percentile_disc" => { + if !*within_group { + return Err(DbError::sql( + "PERCENTILE_DISC requires WITHIN GROUP (ORDER BY ...)", + )); + } + if *distinct { + return Err(DbError::sql( + "PERCENTILE_DISC does not support DISTINCT", + )); + } + if *star || args.len() != 1 { + return Err(DbError::sql( + "PERCENTILE_DISC expects exactly 1 argument", + )); + } + aggregate_percentile_disc( + self, + dataset, + group_row_indexes, + &args[0], + order_by, + params, + ctes, + ) + } + _ => Err(DbError::sql(format!( + "unsupported aggregate function {}", + name.to_ascii_uppercase() + ))), + } + } + name if *within_group => Err(DbError::sql(format!( + "{} does not support WITHIN GROUP", + name.to_ascii_uppercase() + ))), + name if !order_by.is_empty() && !matches!(name, "group_concat" | "string_agg") => { + Err(DbError::sql(format!( + "{} does not support aggregate ORDER BY", + name.to_ascii_uppercase() + ))) + } + "count" => { + if *star { + Ok(Value::Int64(group_row_indexes.len() as i64)) + } else if *distinct { + let mut vals = Vec::new(); + for row_index in group_row_indexes { + let row = + dataset.rows.get(*row_index).map(Vec::as_slice).ok_or_else( + || DbError::internal("group row index is invalid"), + )?; + let val = self.eval_expr(&args[0], dataset, row, params, ctes, None)?; + if !matches!(val, Value::Null) { + vals.push(val); + } + } + vals.sort_by(|a, b| { + compare_values(a, b).unwrap_or(std::cmp::Ordering::Equal) + }); + vals.dedup_by(|a, b| { + compare_values(a, b).unwrap_or(std::cmp::Ordering::Equal) + == std::cmp::Ordering::Equal + }); + Ok(Value::Int64(vals.len() as i64)) + } else { + let mut count = 0_i64; + for row_index in group_row_indexes { + let row = + dataset.rows.get(*row_index).map(Vec::as_slice).ok_or_else( + || DbError::internal("group row index is invalid"), + )?; + if !matches!( + self.eval_expr(&args[0], dataset, row, params, ctes, None)?, + Value::Null + ) { + count += 1; + } + } + Ok(Value::Int64(count)) + } + } + "sum" => aggregate_numeric( + &aggregate_ctx, + group_row_indexes, + &args[0], + NumericAgg::Sum, + *distinct, + ), + "avg" => aggregate_numeric( + &aggregate_ctx, + group_row_indexes, + &args[0], + NumericAgg::Avg, + *distinct, + ), + "total" => aggregate_numeric( + &aggregate_ctx, + group_row_indexes, + &args[0], + NumericAgg::Total, + *distinct, + ), + "stddev" | "stddev_samp" => aggregate_variance( + &aggregate_ctx, + group_row_indexes, + &args[0], + VarianceAgg::StddevSamp, + *distinct, + ), + "stddev_pop" => aggregate_variance( + &aggregate_ctx, + group_row_indexes, + &args[0], + VarianceAgg::StddevPop, + *distinct, + ), + "variance" | "var_samp" => aggregate_variance( + &aggregate_ctx, + group_row_indexes, + &args[0], + VarianceAgg::VarSamp, + *distinct, + ), + "var_pop" => aggregate_variance( + &aggregate_ctx, + group_row_indexes, + &args[0], + VarianceAgg::VarPop, + *distinct, + ), + "bool_and" => aggregate_bool( + &aggregate_ctx, + group_row_indexes, + &args[0], + BoolAgg::And, + *distinct, + ), + "bool_or" => aggregate_bool( + &aggregate_ctx, + group_row_indexes, + &args[0], + BoolAgg::Or, + *distinct, + ), + "min" => aggregate_extreme( + self, + dataset, + group_row_indexes, + &args[0], + params, + ctes, + true, + ), + "max" => aggregate_extreme( + self, + dataset, + group_row_indexes, + &args[0], + params, + ctes, + false, + ), + name @ ("group_concat" | "string_agg") => aggregate_group_concat( + &aggregate_ctx, + group_row_indexes, + args, + *distinct, + order_by, + name, + ), + other => { + let mut arg_rows = Vec::with_capacity(group_row_indexes.len()); + for row_index in group_row_indexes { + let row = dataset + .rows + .get(*row_index) + .map(Vec::as_slice) + .ok_or_else(|| DbError::internal("group row index is invalid"))?; + let values = args + .iter() + .map(|arg| self.eval_expr(arg, dataset, row, params, ctes, None)) + .collect::>>()?; + arg_rows.push(values); + } + if let Some(value) = + crate::extensions::invoke_aggregate_from_runtime(self, other, arg_rows)? + { + return Ok(value); + } + Err(DbError::sql(format!( + "unsupported aggregate function {other}" + ))) + } + }, + Expr::Unary { op, expr } => { + let value = self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes)?; + match op { + UnaryOp::Not => Ok(match truthy(&value) { + Some(value) => Value::Bool(!value), + None => Value::Null, + }), + UnaryOp::Negate => match value { + Value::Int64(value) => Ok(Value::Int64(-value)), + Value::Float64(value) => Ok(Value::Float64(-value)), + Value::Null => Ok(Value::Null), + other => Err(DbError::sql(format!("cannot negate {other:?}"))), + }, + } + } + Expr::Binary { left, op, right } => { + let collation = expr_collation(left).or_else(|| expr_collation(right)); + eval_binary_with_collation( + Some(self), + op, + self.eval_group_expr(left, dataset, group_row_indexes, params, ctes)?, + self.eval_group_expr(right, dataset, group_row_indexes, params, ctes)?, + collation, + ) + } + Expr::Between { + expr, + low, + high, + negated, + } => { + let value = self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes)?; + let low = self.eval_group_expr(low, dataset, group_row_indexes, params, ctes)?; + let high = self.eval_group_expr(high, dataset, group_row_indexes, params, ctes)?; + if matches!(value, Value::Null) + || matches!(low, Value::Null) + || matches!(high, Value::Null) + { + return Ok(Value::Null); + } + let collation = expr_collation(expr); + let in_range = compare_values_with_runtime_collation( + Some(self), + &value, + &low, + collation.clone(), + )? != std::cmp::Ordering::Less + && compare_values_with_runtime_collation(Some(self), &value, &high, collation)? + != std::cmp::Ordering::Greater; + Ok(Value::Bool(if *negated { !in_range } else { in_range })) + } + Expr::InList { + expr, + items, + negated, + } => { + let value = self.eval_group_membership_value( + expr, + dataset, + group_row_indexes, + params, + ctes, + )?; + if membership_value_has_nulls(&value) { + return Ok(Value::Null); + } + let mut saw_null = false; + for item in items { + let candidate = self.eval_group_membership_value( + item, + dataset, + group_row_indexes, + params, + ctes, + )?; + match compare_membership_values(&value, &candidate)? { + Some(true) => return Ok(Value::Bool(!*negated)), + Some(false) => {} + None => saw_null = true, + } + } + if saw_null { + Ok(Value::Null) + } else { + Ok(Value::Bool(*negated)) + } + } + Expr::Like { + expr, + pattern, + escape, + case_insensitive, + negated, + .. + } => { + let left = self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes)?; + let right = + self.eval_group_expr(pattern, dataset, group_row_indexes, params, ctes)?; + let escape = escape + .as_ref() + .map(|expr| { + self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes) + }) + .transpose()?; + eval_like(left, right, escape, *case_insensitive, *negated) + } + Expr::IsNull { expr, negated } => { + let is_null = matches!( + self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes)?, + Value::Null + ); + Ok(Value::Bool(if *negated { !is_null } else { is_null })) + } + Expr::Function { name, args } => { + let args_contain_aggregate = args.iter().any(expr_contains_aggregate) + || args.iter().try_fold(false, |found, arg| { + if found { + Ok(true) + } else { + expr_contains_runtime_extension_aggregate(self, arg) + } + })?; + if !args_contain_aggregate { + let mut arg_rows = Vec::with_capacity(group_row_indexes.len()); + for row_index in group_row_indexes { + let row = dataset + .rows + .get(*row_index) + .map(Vec::as_slice) + .ok_or_else(|| DbError::internal("group row index is invalid"))?; + let values = args + .iter() + .map(|arg| self.eval_expr(arg, dataset, row, params, ctes, None)) + .collect::>>()?; + arg_rows.push(values); + } + if let Some(value) = + crate::extensions::invoke_aggregate_from_runtime(self, name, arg_rows)? + { + return Ok(value); + } + } + let row = if let Some(row_index) = group_row_indexes.first().copied() { + dataset + .rows + .get(row_index) + .map(Vec::as_slice) + .ok_or_else(|| DbError::internal("group row index is invalid"))? + } else { + &[] + }; + let values = args + .iter() + .map(|arg| self.eval_group_expr(arg, dataset, group_row_indexes, params, ctes)) + .collect::>>()?; + match name.as_str() { + "coalesce" => Ok(values + .into_iter() + .find(|value| !matches!(value, Value::Null)) + .unwrap_or(Value::Null)), + "nullif" => { + if values.len() != 2 { + return Err(DbError::sql("NULLIF expects exactly two arguments")); + } + if compare_values(&values[0], &values[1])? == std::cmp::Ordering::Equal { + Ok(Value::Null) + } else { + Ok(values[0].clone()) + } + } + "length" => unary_text_fn(values, |value| value.len().to_string()) + .and_then(|value| cast_value(value, crate::catalog::ColumnType::Int64)), + "lower" => unary_text_fn(values, |value| value.to_ascii_lowercase()), + "upper" => unary_text_fn(values, |value| value.to_ascii_uppercase()), + "trim" => unary_text_fn(values, |value| value.trim().to_string()), + other => self.eval_expr( + &Expr::Function { + name: other.to_string(), + args: args.to_vec(), + }, + dataset, + row, + params, + ctes, + None, + ), + } + } + Expr::Case { + operand, + branches, + else_expr, + } => { + let operand_value = operand + .as_deref() + .map(|expr| { + self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes) + }) + .transpose()?; + for (condition, result) in branches { + let matches = if let Some(operand_value) = &operand_value { + compare_values( + operand_value, + &self.eval_group_expr( + condition, + dataset, + group_row_indexes, + params, + ctes, + )?, + )? == std::cmp::Ordering::Equal + } else { + matches!( + self.eval_group_expr( + condition, + dataset, + group_row_indexes, + params, + ctes, + )?, + Value::Bool(true) + ) + }; + if matches { + return self.eval_group_expr( + result, + dataset, + group_row_indexes, + params, + ctes, + ); + } + } + else_expr + .as_deref() + .map(|expr| { + self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes) + }) + .transpose()? + .map_or(Ok(Value::Null), Ok) + } + Expr::Cast { expr, target_type } => cast_value( + self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes)?, + *target_type, + ), + Expr::Collate { expr, .. } => { + self.eval_group_expr(expr, dataset, group_row_indexes, params, ctes) + } + Expr::Row(_) => Err(DbError::sql( + "row values are only supported in IN comparisons", + )), + Expr::RowNumber { .. } | Expr::WindowFunction { .. } => Err(DbError::sql( + "window functions cannot be nested inside grouped expressions", + )), + _ => { + let row = if let Some(row_index) = group_row_indexes.first().copied() { + dataset + .rows + .get(row_index) + .map(Vec::as_slice) + .ok_or_else(|| DbError::internal("group row index is invalid"))? + } else { + &[] + }; + self.eval_expr(expr, dataset, row, params, ctes, None) + } + } + } + pub(crate) fn eval_expr( + &self, + expr: &Expr, + dataset: &Dataset, + row: &[Value], + params: &[Value], + ctes: &BTreeMap, + excluded: Option<&Dataset>, + ) -> Result { + match expr { + Expr::Literal(value) => Ok(value.clone()), + Expr::Column { table, column } => { + self.resolve_column(dataset, row, table.as_deref(), column, excluded) + } + Expr::Parameter(number) => params + .get(number.saturating_sub(1)) + .cloned() + .ok_or_else(|| DbError::sql(format!("missing value for parameter ${number}"))), + Expr::Unary { op, expr } => { + let value = self.eval_expr(expr, dataset, row, params, ctes, excluded)?; + match op { + UnaryOp::Not => Ok(match truthy(&value) { + Some(value) => Value::Bool(!value), + None => Value::Null, + }), + UnaryOp::Negate => match value { + Value::Int64(value) => Ok(Value::Int64(-value)), + Value::Float64(value) => Ok(Value::Float64(-value)), + Value::Null => Ok(Value::Null), + other => Err(DbError::sql(format!("cannot negate {other:?}"))), + }, + } + } + Expr::Binary { left, op, right } => { + let collation = expr_collation(left).or_else(|| expr_collation(right)); + let left = self.eval_expr(left, dataset, row, params, ctes, excluded)?; + let right = self.eval_expr(right, dataset, row, params, ctes, excluded)?; + eval_binary_with_collation(Some(self), op, left, right, collation) + } + Expr::Between { + expr, + low, + high, + negated, + } => { + let value = self.eval_expr(expr, dataset, row, params, ctes, excluded)?; + let low = self.eval_expr(low, dataset, row, params, ctes, excluded)?; + let high = self.eval_expr(high, dataset, row, params, ctes, excluded)?; + if matches!(value, Value::Null) + || matches!(low, Value::Null) + || matches!(high, Value::Null) + { + return Ok(Value::Null); + } + let collation = expr_collation(expr); + let in_range = compare_values_with_runtime_collation( + Some(self), + &value, + &low, + collation.clone(), + )? != std::cmp::Ordering::Less + && compare_values_with_runtime_collation(Some(self), &value, &high, collation)? + != std::cmp::Ordering::Greater; + Ok(Value::Bool(if *negated { !in_range } else { in_range })) + } + Expr::InList { + expr, + items, + negated, + } => { + let value = + self.eval_membership_value(expr, dataset, row, params, ctes, excluded)?; + if membership_value_has_nulls(&value) { + return Ok(Value::Null); + } + let mut saw_null = false; + for item in items { + let candidate = + self.eval_membership_value(item, dataset, row, params, ctes, excluded)?; + match compare_membership_values(&value, &candidate)? { + Some(true) => return Ok(Value::Bool(!*negated)), + Some(false) => {} + None => saw_null = true, + } + } + if saw_null { + Ok(Value::Null) + } else { + Ok(Value::Bool(*negated)) + } + } + Expr::InSubquery { + expr, + query, + negated, + } => { + let value = + self.eval_membership_value(expr, dataset, row, params, ctes, excluded)?; + if membership_value_has_nulls(&value) { + return Ok(Value::Null); + } + let subquery = self.evaluate_query_with_outer(query, params, ctes, dataset, row)?; + let expected_width = match &value { + MembershipValue::Scalar(_) => 1, + MembershipValue::Row(values) => values.len(), + }; + if subquery.columns.len() != expected_width { + return Err(DbError::sql(format!( + "IN subquery must return exactly {} column{}", + expected_width, + if expected_width == 1 { "" } else { "s" } + ))); + } + let mut saw_null = false; + for subquery_row in subquery.rows.iter() { + let candidate = if expected_width == 1 { + MembershipValue::Scalar( + subquery_row.first().cloned().unwrap_or(Value::Null), + ) + } else { + MembershipValue::Row(subquery_row.clone()) + }; + match compare_membership_values(&value, &candidate)? { + Some(true) => return Ok(Value::Bool(!*negated)), + Some(false) => {} + None => saw_null = true, + } + } + if saw_null { + Ok(Value::Null) + } else { + Ok(Value::Bool(*negated)) + } + } + Expr::CompareSubquery { + expr, + op, + quantifier, + query, + } => { + let left_value = self.eval_expr(expr, dataset, row, params, ctes, excluded)?; + let subquery = self.evaluate_query_with_outer(query, params, ctes, dataset, row)?; + if subquery.columns.len() != 1 { + return Err(DbError::sql( + "subquery comparison must return exactly one column", + )); + } + let mut saw_null = false; + let mut saw_row = false; + for subquery_row in subquery.rows.iter() { + saw_row = true; + let candidate = subquery_row.first().cloned().unwrap_or(Value::Null); + match eval_binary_with_collation( + Some(self), + op, + left_value.clone(), + candidate, + expr_collation(expr), + )? { + Value::Bool(result) => match quantifier { + SubqueryQuantifier::Any if result => return Ok(Value::Bool(true)), + SubqueryQuantifier::All if !result => return Ok(Value::Bool(false)), + _ => {} + }, + Value::Null => saw_null = true, + other => { + return Err(DbError::internal(format!( + "subquery comparison did not evaluate to boolean: {other:?}" + ))) + } + } + } + if !saw_row { + return Ok(Value::Bool(matches!(quantifier, SubqueryQuantifier::All))); + } + if saw_null { + Ok(Value::Null) + } else { + Ok(Value::Bool(matches!(quantifier, SubqueryQuantifier::All))) + } + } + Expr::ScalarSubquery(query) => { + let subquery = self.evaluate_query_with_outer(query, params, ctes, dataset, row)?; + if subquery.columns.len() != 1 { + return Err(DbError::sql( + "scalar subquery must return exactly one column", + )); + } + Ok(subquery + .rows + .first() + .and_then(|subquery_row| subquery_row.first()) + .cloned() + .unwrap_or(Value::Null)) + } + Expr::Exists(query) => Ok(Value::Bool( + !self + .evaluate_query_with_outer(query, params, ctes, dataset, row)? + .rows + .is_empty(), + )), + Expr::Like { + expr, + pattern, + escape, + case_insensitive, + negated, + .. + } => { + let left = self.eval_expr(expr, dataset, row, params, ctes, excluded)?; + let right = self.eval_expr(pattern, dataset, row, params, ctes, excluded)?; + let escape = escape + .as_ref() + .map(|expr| self.eval_expr(expr, dataset, row, params, ctes, excluded)) + .transpose()?; + eval_like(left, right, escape, *case_insensitive, *negated) + } + Expr::IsNull { expr, negated } => { + let is_null = matches!( + self.eval_expr(expr, dataset, row, params, ctes, excluded)?, + Value::Null + ); + Ok(Value::Bool(if *negated { !is_null } else { is_null })) + } + Expr::Function { name, args } => { + eval_function(self, name, args, dataset, row, params, ctes, excluded) + } + Expr::Aggregate { .. } => Err(DbError::sql( + "aggregate expressions require grouped evaluation", + )), + Expr::RowNumber { .. } | Expr::WindowFunction { .. } => Err(DbError::sql( + "window-function execution is not yet implemented", + )), + Expr::Case { + operand, + branches, + else_expr, + } => { + let operand_value = operand + .as_deref() + .map(|expr| self.eval_expr(expr, dataset, row, params, ctes, excluded)) + .transpose()?; + for (condition, result) in branches { + let matches = if let Some(operand_value) = &operand_value { + compare_values( + operand_value, + &self.eval_expr(condition, dataset, row, params, ctes, excluded)?, + )? == std::cmp::Ordering::Equal + } else { + matches!( + self.eval_expr(condition, dataset, row, params, ctes, excluded)?, + Value::Bool(true) + ) + }; + if matches { + return self.eval_expr(result, dataset, row, params, ctes, excluded); + } + } + else_expr + .as_deref() + .map(|expr| self.eval_expr(expr, dataset, row, params, ctes, excluded)) + .transpose()? + .map_or(Ok(Value::Null), Ok) + } + Expr::Cast { expr, target_type } => cast_value( + self.eval_expr(expr, dataset, row, params, ctes, excluded)?, + *target_type, + ), + Expr::Collate { expr, .. } => { + self.eval_expr(expr, dataset, row, params, ctes, excluded) + } + Expr::Row(_) => Err(DbError::sql( + "row values are only supported in IN comparisons", + )), + } + } + fn resolve_column( + &self, + dataset: &Dataset, + row: &[Value], + table: Option<&str>, + column: &str, + excluded: Option<&Dataset>, + ) -> Result { + if let Some(table_name) = table { + if identifiers_equal(table_name, "excluded") { + let excluded = excluded.ok_or_else(|| { + DbError::sql("EXCLUDED is only valid in ON CONFLICT DO UPDATE") + })?; + return self.resolve_column( + excluded, + excluded.rows.first().map(Vec::as_slice).unwrap_or(&[]), + None, + column, + None, + ); + } + } + let mut matched_index = None; + for (index, binding) in dataset.columns.iter().enumerate() { + let visible_match = table.is_some() || !binding.hidden; + if !visible_match || !identifiers_equal(&binding.name, column) { + continue; + } + if table.is_some_and(|table| { + !binding + .table + .as_deref() + .is_some_and(|binding_table| identifiers_equal(binding_table, table)) + }) { + continue; + } + if matched_index.replace(index).is_some() { + return Err(DbError::sql(format!("ambiguous column reference {column}"))); + } + } + if let Some(index) = matched_index { + row.get(index) + .cloned() + .ok_or_else(|| DbError::internal("row is shorter than its bindings")) + } else { + Err(DbError::sql(format!("unknown column {column}"))) + } + } + pub(super) fn apply_virtual_generated_columns( + &self, + table: &TableSchema, + row: &mut [Value], + ) -> Result<()> { + if generated_columns_are_stored(table) { + return Ok(()); + } + let mut base_values = row.to_vec(); + for (index, column) in table.columns.iter().enumerate() { + let Some(generated_sql) = &column.generated_sql else { + continue; + }; + if column.generated_stored { + base_values[index] = row + .get(index) + .cloned() + .ok_or_else(|| DbError::internal("row is shorter than table schema"))?; + continue; + } + let expr = crate::sql::parser::parse_expression_sql(generated_sql)?; + let dataset = table_row_dataset(table, &base_values, &table.name); + let eval_row = dataset.rows.first().map(Vec::as_slice).unwrap_or(&[]); + let value = self.eval_expr(&expr, &dataset, eval_row, &[], &BTreeMap::new(), None)?; + let cast_value = self::constraints::coerce_column_value(column, value)?; + if let Some(slot) = row.get_mut(index) { + *slot = cast_value.clone(); + } else { + return Err(DbError::internal("row is shorter than table schema")); + } + base_values[index] = cast_value; + } + Ok(()) + } +} diff --git a/crates/decentdb/src/exec/runtime_keys.rs b/crates/decentdb/src/exec/runtime_keys.rs new file mode 100644 index 00000000..8d9ea16d --- /dev/null +++ b/crates/decentdb/src/exec/runtime_keys.rs @@ -0,0 +1,1600 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +/// Runtime storage for a unique typed `INT64` index. +/// +/// Integer primary keys commonly map a contiguous key range to identical row +/// IDs. Keeping that relation as a range avoids allocating and populating a +/// hash-map entry for every row while retaining a conservative sparse fallback +/// for every other unique integer index shape (ADR 0203). +#[derive(Clone, Debug)] +pub(crate) enum UniqueInt64Keys { + DenseIdentity { start: i64, len: usize }, + Sparse(Int64Map), +} + +impl Default for UniqueInt64Keys { + fn default() -> Self { + Self::new() + } +} + +impl UniqueInt64Keys { + pub(crate) fn new() -> Self { + Self::DenseIdentity { start: 0, len: 0 } + } + + fn dense_value_at(start: i64, offset: usize) -> Option { + let offset = i128::try_from(offset).ok()?; + i64::try_from(i128::from(start) + offset).ok() + } + + pub(crate) fn dense_contains(start: i64, len: usize, key: i64) -> bool { + let offset = i128::from(key) - i128::from(start); + offset >= 0 && usize::try_from(offset).is_ok_and(|offset| offset < len) + } + + pub(crate) fn len(&self) -> usize { + match self { + Self::DenseIdentity { len, .. } => *len, + Self::Sparse(keys) => keys.len(), + } + } + + pub(crate) fn get(&self, key: &i64) -> Option { + match self { + Self::DenseIdentity { start, len } if Self::dense_contains(*start, *len, *key) => { + Some(*key) + } + Self::DenseIdentity { .. } => None, + Self::Sparse(keys) => keys.get(key).copied(), + } + } + + pub(crate) fn iter(&self) -> UniqueInt64KeysIter<'_> { + match self { + Self::DenseIdentity { start, len } => UniqueInt64KeysIter::Dense { + start: *start, + offset: 0, + len: *len, + }, + Self::Sparse(keys) => UniqueInt64KeysIter::Sparse(keys.iter()), + } + } + + fn materialize_sparse(&mut self) { + let Self::DenseIdentity { start, len } = self else { + return; + }; + let start = *start; + let len = *len; + let mut keys = HashMap::with_capacity_and_hasher(len, Int64HashBuilder::default()); + for offset in 0..len { + let Some(value) = Self::dense_value_at(start, offset) else { + debug_assert!(false, "dense INT64 identity range exceeded i64 bounds"); + break; + }; + keys.insert(value, value); + } + *self = Self::Sparse(keys); + } + + /// Insert a mapping and return the previous row ID, matching + /// `HashMap::insert` semantics. + pub(crate) fn insert(&mut self, key: i64, row_id: i64) -> Option { + match self { + Self::DenseIdentity { start, len } => { + if Self::dense_contains(*start, *len, key) { + if key == row_id { + return Some(key); + } + } else if key == row_id { + if *len == 0 { + *start = key; + *len = 1; + return None; + } + if Self::dense_value_at(*start, *len) == Some(key) { + *len = len.saturating_add(1); + return None; + } + if start.checked_sub(1) == Some(key) { + *start = key; + *len = len.saturating_add(1); + return None; + } + } + self.materialize_sparse(); + let Self::Sparse(keys) = self else { + return None; + }; + keys.insert(key, row_id) + } + Self::Sparse(keys) => keys.insert(key, row_id), + } + } + + fn remove_row_id_mapping(&mut self, row_id: i64) { + match self { + Self::DenseIdentity { start, len } if Self::dense_contains(*start, *len, row_id) => { + if *len == 1 { + *len = 0; + } else if *start == row_id { + *start = start.saturating_add(1); + *len -= 1; + } else if Self::dense_value_at(*start, len.saturating_sub(1)) == Some(row_id) { + *len -= 1; + } else { + self.materialize_sparse(); + if let Self::Sparse(keys) = self { + keys.remove(&row_id); + } + } + } + Self::DenseIdentity { .. } => {} + Self::Sparse(keys) => keys.retain(|_, existing| *existing != row_id), + } + } + + fn shrink_to_fit(&mut self) -> usize { + let Self::Sparse(keys) = self else { + return 0; + }; + let old_capacity = keys.capacity(); + keys.shrink_to_fit(); + old_capacity + .saturating_sub(keys.capacity()) + .saturating_mul(std::mem::size_of::<(i64, i64)>()) + } + + #[cfg(test)] + pub(crate) fn is_dense_identity(&self) -> bool { + matches!(self, Self::DenseIdentity { .. }) + } +} + +pub(crate) enum UniqueInt64KeysIter<'a> { + Dense { + start: i64, + offset: usize, + len: usize, + }, + Sparse(std::collections::hash_map::Iter<'a, i64, i64>), +} + +impl Iterator for UniqueInt64KeysIter<'_> { + type Item = (i64, i64); + + fn next(&mut self) -> Option { + match self { + Self::Dense { start, offset, len } => { + if *offset >= *len { + return None; + } + let value = UniqueInt64Keys::dense_value_at(*start, *offset)?; + *offset += 1; + Some((value, value)) + } + Self::Sparse(iter) => iter.next().map(|(key, row_id)| (*key, *row_id)), + } + } + + fn size_hint(&self) -> (usize, Option) { + match self { + Self::Dense { offset, len, .. } => { + let remaining = len.saturating_sub(*offset); + (remaining, Some(remaining)) + } + Self::Sparse(iter) => iter.size_hint(), + } + } +} + +impl ExactSizeIterator for UniqueInt64KeysIter<'_> {} + +pub(crate) enum RuntimeInt64RowIdsIter<'a> { + One(Option), + Contiguous { + start: i64, + offset: usize, + len: usize, + }, + Many(std::slice::Iter<'a, i64>), +} + +impl Iterator for RuntimeInt64RowIdsIter<'_> { + type Item = i64; + + fn next(&mut self) -> Option { + match self { + Self::One(row_id) => row_id.take(), + Self::Contiguous { start, offset, len } => { + if *offset >= *len { + return None; + } + let row_id = RuntimeInt64RowIds::value_at(*start, *offset)?; + *offset += 1; + Some(row_id) + } + Self::Many(row_ids) => row_ids.next().copied(), + } + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = match self { + Self::One(row_id) => usize::from(row_id.is_some()), + Self::Contiguous { offset, len, .. } => len.saturating_sub(*offset), + Self::Many(row_ids) => row_ids.len(), + }; + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for RuntimeInt64RowIdsIter<'_> {} + +/// Runtime key-domain storage for a non-unique typed `INT64` index. +/// +/// Dense mode is intentionally conservative: insertions may repeat only the +/// current final key or append its immediate successor. Gaps and out-of-order +/// key insertion materialize a sparse identity-hashed map, ensuring arbitrary +/// workloads keep general hash-map behavior while grouped benchmark-shaped +/// foreign-key indexes avoid per-key hash buckets. +#[derive(Clone, Debug)] +pub(crate) enum NonUniqueInt64Keys { + Dense { + start: i64, + postings: Vec, + }, + Sparse(Int64Map), +} + +impl Default for NonUniqueInt64Keys { + fn default() -> Self { + Self::new() + } +} + +impl NonUniqueInt64Keys { + pub(crate) fn new() -> Self { + Self::Dense { + start: 0, + postings: Vec::new(), + } + } + + pub(crate) fn len(&self) -> usize { + match self { + Self::Dense { postings, .. } => postings.len(), + Self::Sparse(keys) => keys.len(), + } + } + + pub(crate) fn get(&self, key: &i64) -> Option<&RuntimeInt64RowIds> { + match self { + Self::Dense { start, postings } + if UniqueInt64Keys::dense_contains(*start, postings.len(), *key) => + { + let offset = usize::try_from(i128::from(*key) - i128::from(*start)).ok()?; + postings.get(offset) + } + Self::Dense { .. } => None, + Self::Sparse(keys) => keys.get(key), + } + } + + fn get_mut(&mut self, key: &i64) -> Option<&mut RuntimeInt64RowIds> { + match self { + Self::Dense { start, postings } + if UniqueInt64Keys::dense_contains(*start, postings.len(), *key) => + { + let offset = usize::try_from(i128::from(*key) - i128::from(*start)).ok()?; + postings.get_mut(offset) + } + Self::Dense { .. } => None, + Self::Sparse(keys) => keys.get_mut(key), + } + } + + pub(crate) fn iter(&self) -> NonUniqueInt64KeysIter<'_> { + match self { + Self::Dense { start, postings } => NonUniqueInt64KeysIter::Dense { + start: *start, + offset: 0, + postings: postings.iter(), + }, + Self::Sparse(keys) => NonUniqueInt64KeysIter::Sparse(keys.iter()), + } + } + + fn values(&self) -> NonUniqueInt64Values<'_> { + match self { + Self::Dense { postings, .. } => NonUniqueInt64Values::Dense(postings.iter()), + Self::Sparse(keys) => NonUniqueInt64Values::Sparse(keys.values()), + } + } + + fn materialize_sparse(&mut self) { + let Self::Dense { start, postings } = self else { + return; + }; + let start = *start; + let postings = std::mem::take(postings); + let mut keys = + Int64Map::with_capacity_and_hasher(postings.len(), Int64HashBuilder::default()); + for (offset, row_ids) in postings.into_iter().enumerate() { + let Some(key) = UniqueInt64Keys::dense_value_at(start, offset) else { + debug_assert!(false, "dense non-unique INT64 range exceeded i64 bounds"); + break; + }; + keys.insert(key, row_ids); + } + *self = Self::Sparse(keys); + } + + pub(crate) fn insert_row_id(&mut self, key: i64, row_id: i64) { + match self { + Self::Dense { start, postings } => { + if postings.is_empty() { + *start = key; + postings.push(RuntimeInt64RowIds::one(row_id)); + return; + } + let last_offset = postings.len().saturating_sub(1); + if UniqueInt64Keys::dense_value_at(*start, last_offset) == Some(key) { + if let Some(posting) = postings.last_mut() { + posting.push(row_id); + } + return; + } + if UniqueInt64Keys::dense_value_at(*start, postings.len()) == Some(key) { + postings.push(RuntimeInt64RowIds::one(row_id)); + return; + } + self.materialize_sparse(); + self.insert_row_id(key, row_id); + } + Self::Sparse(keys) => match keys.entry(key) { + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(RuntimeInt64RowIds::one(row_id)); + } + std::collections::hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().push(row_id); + } + }, + } + } + + fn remove_row_id_mapping(&mut self, row_id: i64) { + match self { + Self::Dense { start, postings } => { + for posting in postings.iter_mut() { + if posting.contains(&row_id) { + posting.retain(|existing| *existing != row_id); + } + } + while postings.last().is_some_and(RuntimeInt64RowIds::is_empty) { + postings.pop(); + } + while postings.first().is_some_and(RuntimeInt64RowIds::is_empty) { + postings.remove(0); + *start = start.saturating_add(1); + } + if postings.iter().any(RuntimeInt64RowIds::is_empty) { + self.materialize_sparse(); + if let Self::Sparse(keys) = self { + keys.retain(|_, posting| !posting.is_empty()); + } + } + } + Self::Sparse(keys) => { + for posting in keys.values_mut() { + if posting.contains(&row_id) { + posting.retain(|existing| *existing != row_id); + } + } + keys.retain(|_, posting| !posting.is_empty()); + } + } + } + + fn remove_empty_key(&mut self, key: i64) { + if self.get(&key).is_none_or(|posting| !posting.is_empty()) { + return; + } + match self { + Self::Dense { start, postings } => { + let Some(offset) = usize::try_from(i128::from(key) - i128::from(*start)).ok() + else { + return; + }; + if offset == postings.len().saturating_sub(1) { + postings.pop(); + } else if offset == 0 { + postings.remove(0); + *start = start.saturating_add(1); + } else { + self.materialize_sparse(); + if let Self::Sparse(keys) = self { + keys.remove(&key); + } + } + } + Self::Sparse(keys) => { + keys.remove(&key); + } + } + } + + fn shrink_to_fit(&mut self) -> usize { + match self { + Self::Dense { postings, .. } => { + let old_capacity = postings.capacity(); + let mut freed = postings.iter_mut().fold(0usize, |freed, posting| { + freed.saturating_add(posting.shrink_to_fit()) + }); + postings.shrink_to_fit(); + freed = freed.saturating_add( + old_capacity + .saturating_sub(postings.capacity()) + .saturating_mul(std::mem::size_of::()), + ); + freed + } + Self::Sparse(keys) => { + let old_capacity = keys.capacity(); + let mut freed = keys.values_mut().fold(0usize, |freed, posting| { + freed.saturating_add(posting.shrink_to_fit()) + }); + keys.shrink_to_fit(); + freed = freed.saturating_add( + old_capacity + .saturating_sub(keys.capacity()) + .saturating_mul(std::mem::size_of::<(i64, RuntimeInt64RowIds)>()), + ); + freed + } + } + } + + #[cfg(test)] + pub(crate) fn is_dense(&self) -> bool { + matches!(self, Self::Dense { .. }) + } +} + +pub(crate) enum NonUniqueInt64KeysIter<'a> { + Dense { + start: i64, + offset: usize, + postings: std::slice::Iter<'a, RuntimeInt64RowIds>, + }, + Sparse(std::collections::hash_map::Iter<'a, i64, RuntimeInt64RowIds>), +} + +impl<'a> Iterator for NonUniqueInt64KeysIter<'a> { + type Item = (i64, &'a RuntimeInt64RowIds); + + fn next(&mut self) -> Option { + match self { + Self::Dense { + start, + offset, + postings, + } => { + let row_ids = postings.next()?; + let key = UniqueInt64Keys::dense_value_at(*start, *offset)?; + *offset += 1; + Some((key, row_ids)) + } + Self::Sparse(keys) => keys.next().map(|(key, row_ids)| (*key, row_ids)), + } + } + + fn size_hint(&self) -> (usize, Option) { + match self { + Self::Dense { postings, .. } => postings.size_hint(), + Self::Sparse(keys) => keys.size_hint(), + } + } +} + +impl ExactSizeIterator for NonUniqueInt64KeysIter<'_> {} + +pub(crate) enum NonUniqueInt64Values<'a> { + Dense(std::slice::Iter<'a, RuntimeInt64RowIds>), + Sparse(std::collections::hash_map::Values<'a, i64, RuntimeInt64RowIds>), +} + +impl<'a> Iterator for NonUniqueInt64Values<'a> { + type Item = &'a RuntimeInt64RowIds; + + fn next(&mut self) -> Option { + match self { + Self::Dense(postings) => postings.next(), + Self::Sparse(postings) => postings.next(), + } + } + + fn size_hint(&self) -> (usize, Option) { + match self { + Self::Dense(postings) => postings.size_hint(), + Self::Sparse(postings) => postings.size_hint(), + } + } +} + +impl ExactSizeIterator for NonUniqueInt64Values<'_> {} + +#[derive(Clone, Debug)] +pub(crate) enum RuntimeBtreeKeys { + UniqueEncoded(Arc>, BTreeSet), + NonUniqueEncoded(Arc, BTreeSet), + UniqueInt64(Arc, BTreeSet), + NonUniqueInt64(Arc, BTreeSet), + UniqueUuid(Arc>, BTreeSet), + NonUniqueUuid(Arc>>, BTreeSet), +} + +/// Non-unique encoded-key map plus exact state for whether any posting can +/// release capacity at commit. High-cardinality text indexes normally contain +/// only singleton values, so commit can skip an otherwise +/// linear scan over every key. +#[derive(Debug)] +pub(crate) struct RuntimeEncodedPostings { + entries: BTreeMap, + shrinkable_postings: usize, +} + +impl Clone for RuntimeEncodedPostings { + fn clone(&self) -> Self { + // Cloning a Vec is allowed to choose a capacity different from the + // source. Recompute rather than copying the count so Arc::make_mut's + // COW clone cannot leave shrink bookkeeping stale. + Self::new(self.entries.clone()) + } +} + +impl RuntimeEncodedPostings { + pub(crate) fn new(entries: BTreeMap) -> Self { + let shrinkable_postings = entries + .values() + .filter(|row_ids| row_ids.is_shrinkable()) + .count(); + Self { + entries, + shrinkable_postings, + } + } + + fn adjust_shrinkable_count(&mut self, was_shrinkable: bool, is_shrinkable: bool) { + match (was_shrinkable, is_shrinkable) { + (false, true) => self.shrinkable_postings = self.shrinkable_postings.saturating_add(1), + (true, false) => self.shrinkable_postings = self.shrinkable_postings.saturating_sub(1), + _ => {} + } + } + + pub(crate) fn insert_row_id(&mut self, key: RuntimeEncodedKey, row_id: i64) { + use std::collections::btree_map::Entry; + + let (was_shrinkable, is_shrinkable) = match self.entries.entry(key) { + Entry::Vacant(entry) => { + entry.insert(RuntimeEncodedRowIds::one(row_id)); + (false, false) + } + Entry::Occupied(mut entry) => { + let row_ids = entry.get_mut(); + let was_shrinkable = row_ids.is_shrinkable(); + row_ids.push(row_id); + (was_shrinkable, row_ids.is_shrinkable()) + } + }; + self.adjust_shrinkable_count(was_shrinkable, is_shrinkable); + } + + pub(crate) fn remove_row_id_everywhere(&mut self, row_id: i64) { + for row_ids in self.entries.values_mut() { + row_ids.retain(|existing| *existing != row_id); + } + self.entries.retain(|_, row_ids| !row_ids.is_empty()); + self.shrinkable_postings = self + .entries + .values() + .filter(|row_ids| row_ids.is_shrinkable()) + .count(); + } + + pub(crate) fn remove_row_id_for_key(&mut self, key: &[u8], row_id: i64) { + let Some(row_ids) = self.entries.get_mut(key) else { + return; + }; + let was_shrinkable = row_ids.is_shrinkable(); + row_ids.retain(|existing| *existing != row_id); + let is_empty = row_ids.is_empty(); + let is_shrinkable = !is_empty && row_ids.is_shrinkable(); + if is_empty { + self.entries.remove(key); + } + self.adjust_shrinkable_count(was_shrinkable, is_shrinkable); + } + + pub(crate) fn shrink_to_fit(&mut self) -> usize { + if self.shrinkable_postings == 0 { + return 0; + } + let freed = self.entries.values_mut().fold(0usize, |freed, row_ids| { + freed.saturating_add(row_ids.shrink_to_fit()) + }); + // Vec::shrink_to_fit is explicitly best-effort. Recompute from the + // allocator's actual post-shrink capacities so zero can never become a + // false-clean state that permanently suppresses later compaction. + self.shrinkable_postings = self + .entries + .values() + .filter(|row_ids| row_ids.is_shrinkable()) + .count(); + freed + } + + #[cfg(test)] + pub(crate) fn shrinkable_posting_count(&self) -> usize { + self.shrinkable_postings + } +} + +impl std::ops::Deref for RuntimeEncodedPostings { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.entries + } +} + +/// Row IDs stored beneath one encoded key in a non-unique runtime index. +/// +/// Encoded indexes are commonly declared non-unique even when their data is +/// high-cardinality. On 64-bit targets, keeping the first row ID inline avoids +/// a heap allocation for every such key while preserving the insertion order +/// used by index scans; a second row promotes the singleton to the existing +/// `Vec` layout. Supported 32-bit targets use `Vec` directly because an +/// `i64`-carrying enum would exceed the former three-word object footprint. +#[cfg(target_pointer_width = "64")] +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum RuntimeEncodedRowIds { + One(i64), + Many(Vec), +} + +#[cfg(target_pointer_width = "32")] +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(transparent)] +pub(crate) struct RuntimeEncodedRowIds(Vec); + +impl RuntimeEncodedRowIds { + pub(crate) fn one(row_id: i64) -> Self { + #[cfg(target_pointer_width = "64")] + { + Self::One(row_id) + } + #[cfg(target_pointer_width = "32")] + { + Self(vec![row_id]) + } + } + + #[cfg(test)] + pub(crate) fn many(row_ids: Vec) -> Self { + #[cfg(target_pointer_width = "64")] + { + Self::Many(row_ids) + } + #[cfg(target_pointer_width = "32")] + { + Self(row_ids) + } + } + + pub(crate) fn as_slice(&self) -> &[i64] { + #[cfg(target_pointer_width = "64")] + { + match self { + Self::One(row_id) => std::slice::from_ref(row_id), + Self::Many(row_ids) => row_ids.as_slice(), + } + } + #[cfg(target_pointer_width = "32")] + { + self.0.as_slice() + } + } + + pub(crate) fn push(&mut self, row_id: i64) { + #[cfg(target_pointer_width = "64")] + { + match self { + Self::One(first_row_id) => { + // Match Vec's small-allocation growth behavior so postings + // with a few duplicates do not immediately reallocate. + let mut row_ids = Vec::with_capacity(4); + row_ids.push(*first_row_id); + row_ids.push(row_id); + *self = Self::Many(row_ids); + } + Self::Many(row_ids) => row_ids.push(row_id), + } + } + #[cfg(target_pointer_width = "32")] + { + self.0.push(row_id); + } + } + + pub(crate) fn is_shrinkable(&self) -> bool { + #[cfg(target_pointer_width = "64")] + { + match self { + Self::One(_) => false, + Self::Many(row_ids) => row_ids.len() == 1 || row_ids.capacity() > row_ids.len(), + } + } + #[cfg(target_pointer_width = "32")] + { + self.0.capacity() > self.0.len() + } + } + + pub(crate) fn retain(&mut self, retain: impl FnMut(&i64) -> bool) { + #[cfg(target_pointer_width = "64")] + { + let mut retain = retain; + match self { + Self::One(row_id) => { + if !retain(row_id) { + // Empty postings are transient: every map-owning caller + // removes the entry immediately after retaining. + *self = Self::Many(Vec::new()); + } + } + Self::Many(row_ids) => row_ids.retain(retain), + } + } + #[cfg(target_pointer_width = "32")] + { + self.0.retain(retain); + } + } + + pub(crate) fn shrink_to_fit(&mut self) -> usize { + #[cfg(target_pointer_width = "64")] + { + let Self::Many(row_ids) = self else { + return 0; + }; + let old_capacity = row_ids.capacity(); + if row_ids.len() == 1 { + let row_id = row_ids[0]; + *self = Self::One(row_id); + return old_capacity.saturating_mul(std::mem::size_of::()); + } + row_ids.shrink_to_fit(); + old_capacity + .saturating_sub(row_ids.capacity()) + .saturating_mul(std::mem::size_of::()) + } + #[cfg(target_pointer_width = "32")] + { + let old_capacity = self.0.capacity(); + self.0.shrink_to_fit(); + old_capacity + .saturating_sub(self.0.capacity()) + .saturating_mul(std::mem::size_of::()) + } + } + + #[cfg(test)] + pub(crate) fn is_inline_singleton(&self) -> bool { + #[cfg(target_pointer_width = "64")] + { + matches!(self, Self::One(_)) + } + #[cfg(target_pointer_width = "32")] + { + false + } + } +} + +impl std::ops::Deref for RuntimeEncodedRowIds { + type Target = [i64]; + + fn deref(&self) -> &Self::Target { + self.as_slice() + } +} + +impl RuntimeBtreeKeys { + fn shrink_row_id_vecs<'a>(row_ids: impl Iterator>) -> usize { + let mut freed = 0usize; + for row_ids in row_ids { + let old_capacity = row_ids.capacity(); + row_ids.shrink_to_fit(); + freed = freed.saturating_add( + old_capacity + .saturating_sub(row_ids.capacity()) + .saturating_mul(std::mem::size_of::()), + ); + } + freed + } + + pub(crate) fn shrink_to_fit_if_unique(&mut self) -> usize { + match self { + Self::UniqueEncoded(_, _) + | Self::UniqueUuid(_, _) + | Self::NonUniqueEncoded(_, _) + | Self::NonUniqueUuid(_, _) => { + let mut freed = 0usize; + match self { + Self::NonUniqueEncoded(keys, _) => { + if let Some(keys) = Arc::get_mut(keys) { + freed = freed.saturating_add(keys.shrink_to_fit()); + } + } + Self::NonUniqueUuid(keys, _) => { + if let Some(keys) = Arc::get_mut(keys) { + freed = + freed.saturating_add(Self::shrink_row_id_vecs(keys.values_mut())); + } + } + _ => {} + } + freed + } + Self::UniqueInt64(keys, _) => Arc::get_mut(keys) + .map(UniqueInt64Keys::shrink_to_fit) + .unwrap_or(0), + Self::NonUniqueInt64(keys, _) => { + let Some(keys) = Arc::get_mut(keys) else { + return 0; + }; + keys.shrink_to_fit() + } + } + } + + fn push_non_unique_row_id(row_ids: &mut Vec, row_id: i64) { + // Keep Vec's geometric growth policy on the insert hot path. The + // explicit 1.5x `reserve_exact` policy caused several extra + // reallocations for the common 50-150-row posting lists while the + // post-commit shrink pass already recovers excess capacity. + row_ids.push(row_id); + } + + fn visible_single(row_id: i64, deleted_row_ids: &BTreeSet) -> RuntimeRowIdSet<'_> { + if deleted_row_ids.is_empty() { + return RuntimeRowIdSet::Single(row_id); + } + if deleted_row_ids.contains(&row_id) { + RuntimeRowIdSet::Empty + } else { + RuntimeRowIdSet::Single(row_id) + } + } + + fn visible_many<'a>( + row_ids: &'a [i64], + deleted_row_ids: &BTreeSet, + ) -> RuntimeRowIdSet<'a> { + if deleted_row_ids.is_empty() { + return RuntimeRowIdSet::Many(row_ids); + } + let mut first_deleted = None; + for (index, row_id) in row_ids.iter().copied().enumerate() { + if deleted_row_ids.contains(&row_id) { + first_deleted = Some(index); + break; + } + } + let Some(first_deleted) = first_deleted else { + return RuntimeRowIdSet::Many(row_ids); + }; + + let mut visible = Vec::new(); + for row_id in row_ids[..first_deleted].iter().copied() { + if !deleted_row_ids.contains(&row_id) { + visible.push(row_id); + } + } + for row_id in row_ids[first_deleted..].iter().copied() { + if !deleted_row_ids.contains(&row_id) { + visible.push(row_id); + } + } + if visible.is_empty() { + RuntimeRowIdSet::Empty + } else { + RuntimeRowIdSet::Owned(visible) + } + } + + fn visible_encoded_row_ids<'a>( + row_ids: &'a RuntimeEncodedRowIds, + deleted_row_ids: &'a BTreeSet, + ) -> RuntimeRowIdSet<'a> { + match row_ids.as_slice() { + [row_id] => Self::visible_single(*row_id, deleted_row_ids), + row_ids => Self::visible_many(row_ids, deleted_row_ids), + } + } + + fn visible_int64_row_ids<'a>( + row_ids: &'a RuntimeInt64RowIds, + deleted_row_ids: &'a BTreeSet, + ) -> RuntimeRowIdSet<'a> { + match row_ids { + RuntimeInt64RowIds::One(row_id) => Self::visible_single(*row_id, deleted_row_ids), + RuntimeInt64RowIds::Contiguous { start, len } => { + if deleted_row_ids.is_empty() { + return RuntimeRowIdSet::Contiguous { + start: *start, + len: *len, + }; + } + let Some(end) = len + .checked_sub(1) + .and_then(|offset| RuntimeInt64RowIds::value_at(*start, offset)) + else { + return RuntimeRowIdSet::Empty; + }; + if deleted_row_ids.range(*start..=end).next().is_none() { + return RuntimeRowIdSet::Contiguous { + start: *start, + len: *len, + }; + } + let visible = row_ids + .iter() + .filter(|row_id| !deleted_row_ids.contains(row_id)) + .collect::>(); + if visible.is_empty() { + RuntimeRowIdSet::Empty + } else { + RuntimeRowIdSet::Owned(visible) + } + } + RuntimeInt64RowIds::Many(row_ids) => Self::visible_many(row_ids, deleted_row_ids), + } + } + + pub(crate) fn row_ids_for_row_id(&self, row_id: i64) -> RuntimeRowIdSet<'_> { + match self { + Self::UniqueInt64(keys, deleted) => { + keys.get(&row_id).map_or(RuntimeRowIdSet::Empty, |row_id| { + Self::visible_single(row_id, deleted) + }) + } + Self::NonUniqueInt64(keys, deleted) => keys + .get(&row_id) + .map(|row_ids| Self::visible_int64_row_ids(row_ids, deleted)) + .unwrap_or(RuntimeRowIdSet::Empty), + Self::UniqueEncoded(..) + | Self::NonUniqueEncoded(..) + | Self::UniqueUuid(..) + | Self::NonUniqueUuid(..) => RuntimeRowIdSet::Empty, + } + } + + pub(crate) fn row_id_set_for_key(&self, key: &RuntimeBtreeKey) -> RuntimeRowIdSet<'_> { + match (self, key) { + (Self::UniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) + if deleted.is_empty() => + { + keys.get(key) + .copied() + .map_or(RuntimeRowIdSet::Empty, RuntimeRowIdSet::Single) + } + (Self::UniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => keys + .get(key) + .copied() + .map(|row_id| Self::visible_single(row_id, deleted)) + .unwrap_or(RuntimeRowIdSet::Empty), + (Self::NonUniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => keys + .get(key) + .map(|row_ids| Self::visible_encoded_row_ids(row_ids, deleted)) + .unwrap_or(RuntimeRowIdSet::Empty), + (Self::UniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) + if deleted.is_empty() => + { + keys.get(key) + .map_or(RuntimeRowIdSet::Empty, RuntimeRowIdSet::Single) + } + (Self::UniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => keys + .get(key) + .map(|row_id| Self::visible_single(row_id, deleted)) + .unwrap_or(RuntimeRowIdSet::Empty), + (Self::NonUniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => keys + .get(key) + .map(|row_ids| Self::visible_int64_row_ids(row_ids, deleted)) + .unwrap_or(RuntimeRowIdSet::Empty), + (Self::UniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) if deleted.is_empty() => { + keys.get(key) + .copied() + .map_or(RuntimeRowIdSet::Empty, RuntimeRowIdSet::Single) + } + (Self::UniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => keys + .get(key) + .copied() + .map(|row_id| Self::visible_single(row_id, deleted)) + .unwrap_or(RuntimeRowIdSet::Empty), + (Self::NonUniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) + if deleted.is_empty() => + { + keys.get(key) + .map(|row_ids| RuntimeRowIdSet::Many(row_ids.as_slice())) + .unwrap_or(RuntimeRowIdSet::Empty) + } + (Self::NonUniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => keys + .get(key) + .map(|row_ids| Self::visible_many(row_ids, deleted)) + .unwrap_or(RuntimeRowIdSet::Empty), + _ => RuntimeRowIdSet::Empty, + } + } + + pub(super) fn row_ids_for_key(&self, key: &RuntimeBtreeKey) -> Vec { + let row_ids = self.row_id_set_for_key(key); + let mut values = Vec::with_capacity(row_ids.len()); + row_ids.for_each(|row_id| values.push(row_id)); + values + } + + pub(super) fn row_ids_for_encoded_key_prefix(&self, prefix: &[Value]) -> Result> { + if prefix.is_empty() { + return Ok(Vec::new()); + } + self.row_ids_for_encoded_key_prefixes(std::slice::from_ref(&prefix)) + } + + pub(super) fn row_ids_for_encoded_key_prefixes( + &self, + prefixes: &[&[Value]], + ) -> Result> { + if prefixes.is_empty() || prefixes.iter().any(|prefix| prefix.is_empty()) { + return Ok(Vec::new()); + } + let mut row_ids = Vec::new(); + let mut collect_matching_row_ids = + |encoded_key: &[u8], entry_row_ids: &[i64]| -> Result<()> { + let mut matched = false; + for prefix in prefixes { + if Row::encoded_prefix_matches(encoded_key, prefix)? { + matched = true; + break; + } + } + if matched { + row_ids.extend(entry_row_ids.iter().copied()); + } + Ok(()) + }; + + match self { + Self::UniqueEncoded(keys, deleted) if deleted.is_empty() => { + for (encoded_key, row_id) in keys.iter() { + collect_matching_row_ids(encoded_key, std::slice::from_ref(row_id))?; + } + } + Self::UniqueEncoded(keys, deleted) => { + for (encoded_key, row_id) in keys.iter() { + if deleted.contains(row_id) { + continue; + } + collect_matching_row_ids(encoded_key, std::slice::from_ref(row_id))?; + } + } + Self::NonUniqueEncoded(keys, deleted) if deleted.is_empty() => { + for (encoded_key, entry_row_ids) in keys.iter() { + collect_matching_row_ids(encoded_key, entry_row_ids)?; + } + } + Self::NonUniqueEncoded(keys, deleted) => { + for (encoded_key, entry_row_ids) in keys.iter() { + let visible = entry_row_ids + .iter() + .copied() + .filter(|row_id| !deleted.contains(row_id)) + .collect::>(); + collect_matching_row_ids(encoded_key, &visible)?; + } + } + Self::UniqueInt64(_, _) + | Self::NonUniqueInt64(_, _) + | Self::UniqueUuid(_, _) + | Self::NonUniqueUuid(_, _) => {} + } + + Ok(row_ids) + } + + pub(crate) fn row_ids_for_value_set(&self, value: &Value) -> Result> { + match self { + Self::UniqueEncoded(_, _) | Self::NonUniqueEncoded(_, _) => { + let key = RuntimeBtreeKey::Encoded(encode_runtime_index_key(value)?); + Ok(self.row_id_set_for_key(&key)) + } + Self::UniqueInt64(_, _) | Self::NonUniqueInt64(_, _) => match value { + Value::Int64(value) => Ok(self.row_id_set_for_key(&RuntimeBtreeKey::Int64(*value))), + _ => Ok(RuntimeRowIdSet::Empty), + }, + Self::UniqueUuid(_, _) | Self::NonUniqueUuid(_, _) => match value { + Value::Uuid(value) => Ok(self.row_id_set_for_key(&RuntimeBtreeKey::Uuid(*value))), + _ => Ok(RuntimeRowIdSet::Empty), + }, + } + } + + pub(super) fn row_ids_for_value(&self, value: &Value) -> Result> { + let row_ids = self.row_ids_for_value_set(value)?; + let mut values = Vec::with_capacity(row_ids.len()); + row_ids.for_each(|row_id| values.push(row_id)); + Ok(values) + } + + pub(super) fn row_ids_for_values(&self, values: &[&Value]) -> Result> { + if values.is_empty() { + return Ok(Vec::new()); + } + let mut row_ids = Vec::new(); + match self { + Self::UniqueEncoded(keys, deleted) => { + for value in values { + let key = encode_runtime_index_key(value)?; + if let Some(row_id) = keys.get(&key) { + if !deleted.contains(row_id) { + row_ids.push(*row_id); + } + } + } + } + Self::NonUniqueEncoded(keys, deleted) => { + for value in values { + let key = encode_runtime_index_key(value)?; + if let Some(entry_row_ids) = keys.get(&key) { + row_ids.extend( + entry_row_ids + .iter() + .copied() + .filter(|row_id| !deleted.contains(row_id)), + ); + } + } + } + Self::UniqueInt64(keys, deleted) => { + for value in values { + if let Value::Int64(value) = value { + if let Some(row_id) = keys.get(value) { + if !deleted.contains(&row_id) { + row_ids.push(row_id); + } + } + } + } + } + Self::NonUniqueInt64(keys, deleted) => { + for value in values { + if let Value::Int64(value) = value { + if let Some(entry_row_ids) = keys.get(value) { + row_ids.extend( + entry_row_ids + .iter() + .filter(|row_id| !deleted.contains(row_id)), + ); + } + } + } + } + Self::UniqueUuid(keys, deleted) => { + for value in values { + if let Value::Uuid(value) = value { + if let Some(row_id) = keys.get(value) { + if !deleted.contains(row_id) { + row_ids.push(*row_id); + } + } + } + } + } + Self::NonUniqueUuid(keys, deleted) => { + for value in values { + if let Value::Uuid(value) = value { + if let Some(entry_row_ids) = keys.get(value) { + row_ids.extend( + entry_row_ids + .iter() + .copied() + .filter(|row_id| !deleted.contains(row_id)), + ); + } + } + } + } + } + Ok(row_ids) + } + + pub(crate) fn distinct_key_counts(&self) -> Vec<(RuntimeBtreeKey, usize)> { + match self { + Self::UniqueEncoded(keys, deleted) => keys + .iter() + .filter(|(_, row_id)| !deleted.contains(row_id)) + .map(|(key, _)| (RuntimeBtreeKey::Encoded(key.clone()), 1)) + .collect(), + Self::NonUniqueEncoded(keys, deleted) => keys + .iter() + .map(|(key, row_ids)| { + ( + RuntimeBtreeKey::Encoded(key.clone()), + row_ids + .iter() + .filter(|row_id| !deleted.contains(row_id)) + .count(), + ) + }) + .filter(|(_, count)| *count > 0) + .collect(), + Self::UniqueInt64(keys, deleted) => keys + .iter() + .filter(|(_, row_id)| !deleted.contains(row_id)) + .map(|(key, _)| (RuntimeBtreeKey::Int64(key), 1)) + .collect(), + Self::NonUniqueInt64(keys, deleted) => keys + .iter() + .map(|(key, row_ids)| { + ( + RuntimeBtreeKey::Int64(key), + row_ids + .iter() + .filter(|row_id| !deleted.contains(row_id)) + .count(), + ) + }) + .filter(|(_, count)| *count > 0) + .collect(), + Self::UniqueUuid(keys, deleted) => keys + .iter() + .filter(|(_, row_id)| !deleted.contains(row_id)) + .map(|(key, _)| (RuntimeBtreeKey::Uuid(*key), 1)) + .collect(), + Self::NonUniqueUuid(keys, deleted) => keys + .iter() + .map(|(key, row_ids)| { + ( + RuntimeBtreeKey::Uuid(*key), + row_ids + .iter() + .filter(|row_id| !deleted.contains(row_id)) + .count(), + ) + }) + .filter(|(_, count)| *count > 0) + .collect(), + } + } + + #[cfg(test)] + pub(super) fn contains_any(&self, key: &RuntimeBtreeKey) -> bool { + match (self, key) { + (Self::UniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => keys + .get(key) + .is_some_and(|row_id| !deleted.contains(row_id)), + (Self::NonUniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => keys + .get(key) + .is_some_and(|row_ids| row_ids.iter().any(|row_id| !deleted.contains(row_id))), + (Self::UniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => keys + .get(key) + .is_some_and(|row_id| !deleted.contains(&row_id)), + (Self::NonUniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => keys + .get(key) + .is_some_and(|row_ids| row_ids.iter().any(|row_id| !deleted.contains(&row_id))), + (Self::UniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => keys + .get(key) + .is_some_and(|row_id| !deleted.contains(row_id)), + (Self::NonUniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => keys + .get(key) + .is_some_and(|row_ids| row_ids.iter().any(|row_id| !deleted.contains(row_id))), + _ => false, + } + } + + pub(super) fn insert_row_id(&mut self, key: RuntimeBtreeKey, row_id: i64) -> Result<()> { + match (self, key) { + (Self::UniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => { + if deleted.remove(&row_id) { + Arc::make_mut(keys).retain(|_, existing| *existing != row_id); + } + if let Some(existing) = keys.get(&key).copied() { + if deleted.remove(&existing) { + Arc::make_mut(keys).insert(key, row_id); + return Ok(()); + } + return Err(DbError::internal( + "unique runtime BTREE index received a duplicate key insert", + )); + } + Arc::make_mut(keys).insert(key, row_id); + } + (Self::NonUniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => { + let keys = Arc::make_mut(keys); + if !deleted.is_empty() && deleted.remove(&row_id) { + keys.remove_row_id_everywhere(row_id); + } + keys.insert_row_id(key, row_id); + } + (Self::UniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => { + let revived = deleted.remove(&row_id); + let keys = Arc::make_mut(keys); + if revived { + if keys.get(&key) == Some(row_id) { + return Ok(()); + } + keys.remove_row_id_mapping(row_id); + } + if let Some(existing) = keys.get(&key) { + if deleted.remove(&existing) { + keys.insert(key, row_id); + return Ok(()); + } + return Err(DbError::internal( + "unique runtime BTREE index received a duplicate key insert", + )); + } + keys.insert(key, row_id); + } + (Self::NonUniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => { + let keys = Arc::make_mut(keys); + if deleted.remove(&row_id) { + keys.remove_row_id_mapping(row_id); + } + keys.insert_row_id(key, row_id); + } + (Self::UniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => { + if deleted.remove(&row_id) { + Arc::make_mut(keys).retain(|_, existing| *existing != row_id); + } + if let Some(existing) = keys.get(&key).copied() { + if deleted.remove(&existing) { + Arc::make_mut(keys).insert(key, row_id); + return Ok(()); + } + return Err(DbError::internal( + "unique runtime BTREE index received a duplicate key insert", + )); + } + Arc::make_mut(keys).insert(key, row_id); + } + (Self::NonUniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => { + let keys = Arc::make_mut(keys); + if deleted.remove(&row_id) { + for row_ids in keys.values_mut() { + row_ids.retain(|existing| *existing != row_id); + } + keys.retain(|_, row_ids| !row_ids.is_empty()); + } + Self::push_non_unique_row_id(keys.entry(key).or_default(), row_id); + } + _ => { + return Err(DbError::internal( + "runtime BTREE key type did not match the runtime index representation", + )); + } + } + Ok(()) + } + + pub(super) fn move_row_id( + &mut self, + old_key: &RuntimeBtreeKey, + new_key: RuntimeBtreeKey, + row_id: i64, + ) -> Result { + match (self, old_key, new_key) { + ( + Self::NonUniqueEncoded(keys, deleted), + RuntimeBtreeKey::Encoded(old_key), + RuntimeBtreeKey::Encoded(new_key), + ) if !deleted.contains(&row_id) => { + let keys = Arc::make_mut(keys); + keys.remove_row_id_for_key(old_key.as_slice(), row_id); + keys.insert_row_id(new_key, row_id); + Ok(true) + } + ( + Self::NonUniqueInt64(keys, deleted), + RuntimeBtreeKey::Int64(old_key), + RuntimeBtreeKey::Int64(new_key), + ) if !deleted.contains(&row_id) => { + let keys = Arc::make_mut(keys); + if let Some(row_ids) = keys.get_mut(old_key) { + row_ids.retain(|existing| *existing != row_id); + } + keys.remove_empty_key(*old_key); + keys.insert_row_id(new_key, row_id); + Ok(true) + } + ( + Self::NonUniqueUuid(keys, deleted), + RuntimeBtreeKey::Uuid(old_key), + RuntimeBtreeKey::Uuid(new_key), + ) if !deleted.contains(&row_id) => { + let keys = Arc::make_mut(keys); + if let Some(row_ids) = keys.get_mut(old_key) { + row_ids.retain(|existing| *existing != row_id); + } + if keys.get(old_key).is_some_and(Vec::is_empty) { + keys.remove(old_key); + } + Self::push_non_unique_row_id(keys.entry(new_key).or_default(), row_id); + Ok(true) + } + _ => Ok(false), + } + } + + pub(super) fn remove_row_id(&mut self, key: &RuntimeBtreeKey, row_id: i64) -> Result<()> { + match (self, key) { + (Self::UniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => { + if let Some(existing) = keys.get(key).copied() { + if existing != row_id { + return Err(DbError::internal( + "unique runtime BTREE index row-id mismatch during delete", + )); + } + deleted.insert(row_id); + } + } + (Self::NonUniqueEncoded(keys, deleted), RuntimeBtreeKey::Encoded(key)) => { + if keys + .get(key) + .is_some_and(|row_ids| row_ids.contains(&row_id)) + { + deleted.insert(row_id); + } + } + (Self::UniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => { + if let Some(existing) = keys.get(key) { + if existing != row_id { + return Err(DbError::internal( + "unique runtime BTREE index row-id mismatch during delete", + )); + } + deleted.insert(row_id); + } + } + (Self::NonUniqueInt64(keys, deleted), RuntimeBtreeKey::Int64(key)) => { + if keys + .get(key) + .is_some_and(|row_ids| row_ids.contains(&row_id)) + { + deleted.insert(row_id); + } + } + (Self::UniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => { + if let Some(existing) = keys.get(key).copied() { + if existing != row_id { + return Err(DbError::internal( + "unique runtime BTREE index row-id mismatch during delete", + )); + } + deleted.insert(row_id); + } + } + (Self::NonUniqueUuid(keys, deleted), RuntimeBtreeKey::Uuid(key)) => { + if keys + .get(key) + .is_some_and(|row_ids| row_ids.contains(&row_id)) + { + deleted.insert(row_id); + } + } + _ => { + return Err(DbError::internal( + "runtime BTREE key type did not match the runtime index representation", + )); + } + } + Ok(()) + } + + pub(super) fn mark_row_ids_deleted(&mut self, row_ids: I) + where + I: IntoIterator, + { + match self { + Self::UniqueEncoded(_, deleted) + | Self::NonUniqueEncoded(_, deleted) + | Self::UniqueInt64(_, deleted) + | Self::NonUniqueInt64(_, deleted) + | Self::UniqueUuid(_, deleted) + | Self::NonUniqueUuid(_, deleted) => { + deleted.extend(row_ids); + } + } + } + + pub(crate) fn total_row_id_count(&self) -> usize { + match self { + Self::UniqueEncoded(keys, deleted) => keys.len().saturating_sub(deleted.len()), + Self::NonUniqueEncoded(keys, deleted) => keys + .values() + .map(|row_ids| { + row_ids + .iter() + .filter(|row_id| !deleted.contains(row_id)) + .count() + }) + .sum(), + Self::UniqueInt64(keys, deleted) => keys.len().saturating_sub(deleted.len()), + Self::NonUniqueInt64(keys, deleted) if deleted.is_empty() => { + keys.values().map(RuntimeInt64RowIds::len).sum() + } + Self::NonUniqueInt64(keys, deleted) => keys + .values() + .map(|row_ids| { + row_ids + .iter() + .filter(|row_id| !deleted.contains(row_id)) + .count() + }) + .sum(), + Self::UniqueUuid(keys, deleted) => keys.len().saturating_sub(deleted.len()), + Self::NonUniqueUuid(keys, deleted) => keys + .values() + .map(|row_ids| { + row_ids + .iter() + .filter(|row_id| !deleted.contains(row_id)) + .count() + }) + .sum(), + } + } + + pub(crate) fn distinct_key_count(&self) -> usize { + match self { + Self::UniqueEncoded(keys, deleted) => keys.len().saturating_sub(deleted.len()), + Self::NonUniqueEncoded(keys, deleted) => keys + .values() + .filter(|row_ids| row_ids.iter().any(|row_id| !deleted.contains(row_id))) + .count(), + Self::UniqueInt64(keys, deleted) => keys.len().saturating_sub(deleted.len()), + Self::NonUniqueInt64(keys, deleted) => keys + .values() + .filter(|row_ids| row_ids.iter().any(|row_id| !deleted.contains(&row_id))) + .count(), + Self::UniqueUuid(keys, deleted) => keys.len().saturating_sub(deleted.len()), + Self::NonUniqueUuid(keys, deleted) => keys + .values() + .filter(|row_ids| row_ids.iter().any(|row_id| !deleted.contains(row_id))) + .count(), + } + } + + #[cfg(test)] + pub(crate) fn is_empty(&self) -> bool { + match self { + Self::UniqueEncoded(keys, deleted) => keys.len() == deleted.len(), + Self::NonUniqueEncoded(keys, deleted) => keys + .values() + .all(|row_ids| row_ids.iter().all(|row_id| deleted.contains(row_id))), + Self::UniqueInt64(keys, deleted) => keys.len() == deleted.len(), + Self::NonUniqueInt64(keys, deleted) => keys + .values() + .all(|row_ids| row_ids.iter().all(|row_id| deleted.contains(&row_id))), + Self::UniqueUuid(keys, deleted) => keys.len() == deleted.len(), + Self::NonUniqueUuid(keys, deleted) => keys + .values() + .all(|row_ids| row_ids.iter().all(|row_id| deleted.contains(row_id))), + } + } +} diff --git a/crates/decentdb/src/exec/simple_queries.rs b/crates/decentdb/src/exec/simple_queries.rs new file mode 100644 index 00000000..e6e90d83 --- /dev/null +++ b/crates/decentdb/src/exec/simple_queries.rs @@ -0,0 +1,12738 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +pub(crate) fn simple_window_column_positions( + dataset: &Dataset, + expressions: &[Expr], +) -> Result>> { + let mut positions = Vec::with_capacity(expressions.len()); + for expr in expressions { + let Expr::Column { table, column } = expr else { + return Ok(None); + }; + positions.push(resolve_dataset_column_position( + dataset, + table.as_deref(), + column, + )?); + } + Ok(Some(positions)) +} + +pub(crate) fn simple_window_order_column_positions( + dataset: &Dataset, + order_by: &[OrderBy], +) -> Result>> { + let mut positions = Vec::with_capacity(order_by.len()); + for order in order_by { + if order.collation.is_some() { + return Ok(None); + } + let Expr::Column { table, column } = &order.expr else { + return Ok(None); + }; + positions.push(resolve_dataset_column_position( + dataset, + table.as_deref(), + column, + )?); + } + Ok(Some(positions)) +} + +pub(crate) fn simple_stored_column_eq_literal_predicate( + table: &TableSchema, + row_values: &[Value], + expr: &Expr, +) -> Result> { + if !generated_columns_are_stored(table) { + return Ok(None); + } + let Expr::Binary { + left, + op: BinaryOp::Eq, + right, + } = expr + else { + return Ok(None); + }; + let Some((table_qualifier, column_name, literal_value)) = + simple_column_literal_eq(left, right).or_else(|| simple_column_literal_eq(right, left)) + else { + return Ok(None); + }; + if table_qualifier.is_some_and(|qualifier| !identifiers_equal(qualifier, &table.name)) { + return Ok(None); + } + let Some(position) = column_position(table, column_name) else { + return Ok(None); + }; + let Some(column) = table.columns.get(position) else { + return Ok(None); + }; + let Some(row_value) = row_values.get(position) else { + return Ok(None); + }; + if matches!(row_value, Value::Null) || matches!(literal_value, Value::Null) { + return Ok(Some(false)); + } + let literal_value = constraints::coerce_column_value(column, literal_value.clone())?; + Ok(Some( + compare_values(row_value, &literal_value)? == std::cmp::Ordering::Equal, + )) +} + +pub(crate) fn simple_column_literal_eq<'a>( + left: &'a Expr, + right: &'a Expr, +) -> Option<(Option<&'a str>, &'a str, &'a Value)> { + let Expr::Column { table, column } = left else { + return None; + }; + let Expr::Literal(value) = right else { + return None; + }; + Some((table.as_deref(), column.as_str(), value)) +} + +pub(crate) fn simple_btree_lookup(filter: &Expr) -> Option<(Option<&str>, &str, &Expr)> { + match filter { + Expr::Binary { left, op, right } if *op == BinaryOp::Eq => match (&**left, &**right) { + (Expr::Column { table, column }, value) if simple_btree_lookup_value_expr(value) => { + Some((table.as_deref(), column.as_str(), value)) + } + (value, Expr::Column { table, column }) if simple_btree_lookup_value_expr(value) => { + Some((table.as_deref(), column.as_str(), value)) + } + _ => None, + }, + _ => None, + } +} + +pub(crate) fn simple_btree_lookup_value_expr(expr: &Expr) -> bool { + match expr { + Expr::Literal(_) | Expr::Parameter(_) => true, + Expr::Cast { expr, .. } => simple_btree_lookup_value_expr(expr), + _ => false, + } +} + +pub(crate) fn simple_btree_lookup_terms(filter: &Expr) -> Option, &str, &Expr)>> { + fn collect<'a>( + expr: &'a Expr, + terms: &mut Vec<(Option<&'a str>, &'a str, &'a Expr)>, + ) -> Option<()> { + match expr { + Expr::Binary { + left, + op: BinaryOp::And, + right, + } => { + collect(left, terms)?; + collect(right, terms)?; + Some(()) + } + _ => { + let term = simple_btree_lookup(expr)?; + if terms + .iter() + .any(|(_, column, _)| identifiers_equal(column, term.1)) + { + return None; + } + terms.push(term); + Some(()) + } + } + } + + let mut terms = Vec::new(); + collect(filter, &mut terms)?; + (!terms.is_empty()).then_some(terms) +} + +pub(crate) fn simple_join_projection_eval_bindings( + left_table_name: &str, + left_alias: &Option, + left_schema: &TableSchema, + right_table_name: &str, + right_alias: &Option, + right_schema: &TableSchema, +) -> Vec { + let left_binding = left_alias.as_deref().unwrap_or(left_table_name); + let right_binding = right_alias.as_deref().unwrap_or(right_table_name); + let mut bindings = Vec::with_capacity(left_schema.columns.len() + right_schema.columns.len()); + bindings.extend(left_schema.columns.iter().map(|column| { + ColumnBinding::visible_source( + Some(left_binding.to_string()), + Some(left_table_name.to_string()), + column.name.clone(), + ) + })); + bindings.extend(right_schema.columns.iter().map(|column| { + ColumnBinding::visible_source( + Some(right_binding.to_string()), + Some(right_table_name.to_string()), + column.name.clone(), + ) + })); + bindings +} + +pub(crate) fn simple_grouped_projection_bindings( + group_count: usize, + aggregate_count: usize, +) -> Vec { + let mut bindings = Vec::with_capacity(group_count + aggregate_count); + bindings.extend( + (0..group_count).map(|index| { + ColumnBinding::visible(None, format!("__grouped_projection_group_{index}")) + }), + ); + bindings + .extend((0..aggregate_count).map(|index| { + ColumnBinding::visible(None, format!("__grouped_projection_agg_{index}")) + })); + bindings +} + +pub(crate) fn simple_grouped_projection_group_index( + expr: &Expr, + group_exprs: &[Expr], + table_binding: TableBindingRef<'_>, +) -> Option { + let mut matched = None; + for (index, group_expr) in group_exprs.iter().enumerate() { + if !grouped_projection_expr_matches_group_expr(expr, group_expr, table_binding) { + continue; + } + if matched.replace(index).is_some() { + return None; + } + } + matched +} + +pub(crate) fn rewrite_simple_grouped_output_expr( + expr: &Expr, + group_exprs: &[Expr], + table_name: &str, + binding_name: &str, + table_binding: TableBindingRef<'_>, + synthetic_names: &[String], + aggregate_bindings: &[SimpleGroupedNumericAggregateBinding], +) -> Option { + let group_count = group_exprs.len(); + if let Some(index) = simple_grouped_projection_group_index(expr, group_exprs, table_binding) { + return Some(Expr::Column { + table: None, + column: synthetic_names[index].clone(), + }); + } + + match expr { + Expr::Literal(_) | Expr::Parameter(_) => Some(expr.clone()), + Expr::Unary { op, expr } => Some(Expr::Unary { + op: *op, + expr: Box::new(rewrite_simple_grouped_output_expr( + expr, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?), + }), + Expr::Binary { left, op, right } => Some(Expr::Binary { + left: Box::new(rewrite_simple_grouped_output_expr( + left, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?), + op: *op, + right: Box::new(rewrite_simple_grouped_output_expr( + right, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?), + }), + Expr::Between { + expr, + low, + high, + negated, + } => Some(Expr::Between { + expr: Box::new(rewrite_simple_grouped_output_expr( + expr, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?), + low: Box::new(rewrite_simple_grouped_output_expr( + low, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?), + high: Box::new(rewrite_simple_grouped_output_expr( + high, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?), + negated: *negated, + }), + Expr::InList { + expr, + items, + negated, + } => Some(Expr::InList { + expr: Box::new(rewrite_simple_grouped_output_expr( + expr, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?), + items: items + .iter() + .map(|item| { + rewrite_simple_grouped_output_expr( + item, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + ) + }) + .collect::>>()?, + negated: *negated, + }), + Expr::Like { + expr, + pattern, + escape, + case_insensitive, + negated, + } => Some(Expr::Like { + expr: Box::new(rewrite_simple_grouped_output_expr( + expr, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?), + pattern: Box::new(rewrite_simple_grouped_output_expr( + pattern, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?), + escape: match escape.as_ref() { + Some(expr) => Some(Box::new(rewrite_simple_grouped_output_expr( + expr, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?)), + None => None, + }, + case_insensitive: *case_insensitive, + negated: *negated, + }), + Expr::IsNull { expr, negated } => Some(Expr::IsNull { + expr: Box::new(rewrite_simple_grouped_output_expr( + expr, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?), + negated: *negated, + }), + Expr::Collate { expr, collation } => Some(Expr::Collate { + expr: Box::new(rewrite_simple_grouped_output_expr( + expr, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?), + collation: collation.clone(), + }), + Expr::Function { name, args } => Some(Expr::Function { + name: name.clone(), + args: args + .iter() + .map(|arg| { + rewrite_simple_grouped_output_expr( + arg, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + ) + }) + .collect::>>()?, + }), + Expr::Case { + operand, + branches, + else_expr, + } => Some(Expr::Case { + operand: match operand.as_ref() { + Some(expr) => Some(Box::new(rewrite_simple_grouped_output_expr( + expr, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?)), + None => None, + }, + branches: branches + .iter() + .map(|(condition, value)| { + Some(( + rewrite_simple_grouped_output_expr( + condition, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?, + rewrite_simple_grouped_output_expr( + value, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?, + )) + }) + .collect::>>()?, + else_expr: match else_expr.as_ref() { + Some(expr) => Some(Box::new(rewrite_simple_grouped_output_expr( + expr, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?)), + None => None, + }, + }), + Expr::Row(items) => Some(Expr::Row( + items + .iter() + .map(|item| { + rewrite_simple_grouped_output_expr( + item, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + ) + }) + .collect::>>()?, + )), + Expr::Cast { expr, target_type } => Some(Expr::Cast { + expr: Box::new(rewrite_simple_grouped_output_expr( + expr, + group_exprs, + table_name, + binding_name, + table_binding, + synthetic_names, + aggregate_bindings, + )?), + target_type: *target_type, + }), + Expr::Aggregate { .. } => { + let index = aggregate_bindings.iter().position(|binding| { + matching_simple_grouped_aggregate_binding( + expr, + table_name, + binding_name, + std::slice::from_ref(binding), + ) + .is_some() + })?; + Some(Expr::Column { + table: None, + column: synthetic_names[group_count + index].clone(), + }) + } + Expr::Column { .. } + | Expr::RowNumber { .. } + | Expr::WindowFunction { .. } + | Expr::InSubquery { .. } + | Expr::CompareSubquery { .. } + | Expr::ScalarSubquery(_) + | Expr::Exists(_) => None, + } +} + +pub(crate) fn simple_residual_matches( + candidate: &Value, + plan: &SimpleResidualPlan, +) -> Result { + // SQL three-valued logic: any comparison with a NULL operand yields + // NULL (unknown), which a WHERE treats as false. Mirror the generic + // executor's NULL short-circuit (eval_binary, expressions.rs) so the + // residual fast path never includes rows that the generic path would + // exclude for `col <> v`, `col < v`, `col <= v` on a NULL candidate. + if matches!(candidate, Value::Null) || matches!(plan.value, Value::Null) { + return Ok(false); + } + // Incompatible-type comparisons return Err from compare_values. The + // generic executor may coerce some of these; rather than aborting the + // query on the fast path, treat the term as not satisfied so the row is + // excluded consistently with a WHERE that cannot match. + let ordering = if let Some(ordering) = simple_fast_compare_values(candidate, &plan.value) { + ordering + } else { + let Ok(ordering) = compare_values(candidate, &plan.value) else { + return Ok(false); + }; + ordering + }; + let truthy = match plan.op { + BinaryOp::Eq => ordering == std::cmp::Ordering::Equal, + BinaryOp::NotEq => ordering != std::cmp::Ordering::Equal, + BinaryOp::Gt => ordering == std::cmp::Ordering::Greater, + BinaryOp::GtEq => ordering != std::cmp::Ordering::Less, + BinaryOp::Lt => ordering == std::cmp::Ordering::Less, + BinaryOp::LtEq => ordering != std::cmp::Ordering::Greater, + _ => false, + }; + Ok(truthy) +} + +pub(crate) fn simple_fast_compare_values( + left: &Value, + right: &Value, +) -> Option { + match (left, right) { + (Value::Int64(left), Value::Int64(right)) => Some(left.cmp(right)), + (Value::Float64(left), Value::Float64(right)) => Some(left.total_cmp(right)), + (Value::DateDays(left), Value::DateDays(right)) => Some(left.cmp(right)), + (Value::TimestampMicros(left), Value::TimestampMicros(right)) => Some(left.cmp(right)), + (Value::TimeMicros(left), Value::TimeMicros(right)) => Some(left.cmp(right)), + (Value::TimestampTzMicros(left), Value::TimestampTzMicros(right)) => Some(left.cmp(right)), + _ => None, + } +} + +pub(crate) fn simple_residual_matches_all( + values: &[Value], + residual_plans: &[SimpleResidualPlan], +) -> Result { + if residual_plans.is_empty() { + return Ok(true); + } + for plan in residual_plans { + let Some(candidate) = values.get(plan.column_index) else { + return Ok(false); + }; + if !simple_residual_matches(candidate, plan)? { + return Ok(false); + } + } + Ok(true) +} + +pub(crate) fn simple_range_projection_filter( + filter: &Expr, +) -> Option> { + let mut state = SimpleRangeFilterState::default(); + collect_simple_range_projection_terms(filter, &mut state)?; + Some(SimpleRangeProjectionFilter { + table: state.table, + column: state.column?, + lower: state.lower, + upper: state.upper, + residual: state.residual, + }) +} + +pub(crate) fn simple_contains_like_projection_filter( + filter: &Expr, +) -> Option<(Option<&str>, &str, &str)> { + let Expr::Like { + expr, + pattern, + escape: None, + case_insensitive: false, + negated: false, + } = filter + else { + return None; + }; + let Expr::Column { table, column } = expr.as_ref() else { + return None; + }; + let Expr::Literal(Value::Text(pattern)) = pattern.as_ref() else { + return None; + }; + let literal = simple_contains_like_literal(pattern)?; + Some((table.as_deref(), column.as_str(), literal)) +} + +pub(crate) fn simple_contains_like_literal(pattern: &str) -> Option<&str> { + let literal = pattern.strip_prefix('%')?.strip_suffix('%')?; + if literal.is_empty() || literal.bytes().any(|byte| matches!(byte, b'%' | b'_')) { + return None; + } + Some(literal) +} + +pub(crate) fn collect_simple_range_projection_terms<'a>( + filter: &'a Expr, + state: &mut SimpleRangeFilterState<'a>, +) -> Option<()> { + match filter { + Expr::Binary { + left, + op: BinaryOp::And, + right, + } => { + collect_simple_range_projection_terms(left, state)?; + collect_simple_range_projection_terms(right, state)?; + Some(()) + } + Expr::Binary { left, op, right } => { + let bound = simple_range_projection_bound(left, *op, right) + .or_else(|| simple_range_projection_bound(right, reverse_binary_op(*op)?, left)); + if let Some((table, column, bound_kind, value_expr)) = bound { + // Determine whether this term is on the same column as the + // range column we are building. If it is on a different + // column, do not bail; fall through to the residual handling + // below so conjunctive filters like + // `rating BETWEEN 7.5 AND 9.0 AND runtime_minutes > 120` can + // still use the filtered projection fast path with a residual + // predicate instead of falling back to the generic executor. + let same_as_range_column = state + .column + .is_some_and(|existing| identifiers_equal(existing, column)); + if same_as_range_column { + if let Some(existing_table) = state.table { + if Some(existing_table) != table { + return None; + } + } else { + state.table = table; + } + match bound_kind { + SimpleRangeBoundKind::Lower(inclusive) => { + if state.lower.is_some() { + return None; + } + state.lower = Some(SimpleRangeBound { + inclusive, + value_expr, + }); + } + SimpleRangeBoundKind::Upper(inclusive) => { + if state.upper.is_some() { + return None; + } + state.upper = Some(SimpleRangeBound { + inclusive, + value_expr, + }); + } + } + return Some(()); + } + if state.column.is_some() { + // A range column is already chosen and this term is on a + // different column; treat it as a residual below. + } else { + // No range column chosen yet and this term is a range + // bound; claim it as the range column. + if let Some(existing_table) = state.table { + if Some(existing_table) != table { + return None; + } + } else { + state.table = table; + } + state.column = Some(column); + match bound_kind { + SimpleRangeBoundKind::Lower(inclusive) => { + if state.lower.is_some() { + return None; + } + state.lower = Some(SimpleRangeBound { + inclusive, + value_expr, + }); + } + SimpleRangeBoundKind::Upper(inclusive) => { + if state.upper.is_some() { + return None; + } + state.upper = Some(SimpleRangeBound { + inclusive, + value_expr, + }); + } + } + return Some(()); + } + } + // Not a range bound on the range column. Try to capture it as a + // simple residual column-vs-literal/param comparison on a + // different column so the filtered projection fast path can + // still apply the range prefilter and evaluate the residual + // inline, avoiding the generic executor for conjunctive filters + // like `rating BETWEEN 7.5 AND 9.0 AND runtime_minutes > 120`. + let (res_table, res_column, res_op, res_value) = + simple_residual_projection_bound(left, *op, right).or_else(|| { + simple_residual_projection_bound(right, reverse_binary_op(*op)?, left) + })?; + if let Some(existing_table) = state.table { + if Some(existing_table) != res_table && res_table.is_some() { + return None; + } + } + if state + .column + .is_some_and(|existing| identifiers_equal(existing, res_column)) + { + // Residual on the same column as the range would duplicate a + // bound we already captured; bail to keep semantics simple. + return None; + } + if state + .residual + .iter() + .any(|existing| identifiers_equal(existing.column, res_column)) + { + // At most one residual term per column to avoid interaction + // edge cases (e.g. two predicates on the same column). + return None; + } + state.residual.push(SimpleResidualFilterTerm { + table: res_table, + column: res_column, + op: res_op, + value_expr: res_value, + }); + Some(()) + } + _ => None, + } +} + +pub(crate) fn simple_range_projection_bound<'a>( + left: &'a Expr, + op: BinaryOp, + right: &'a Expr, +) -> Option<(Option<&'a str>, &'a str, SimpleRangeBoundKind, &'a Expr)> { + let Expr::Column { table, column } = left else { + return None; + }; + if !simple_bound_value_expr_is_constant(right) { + return None; + } + let bound_kind = match op { + BinaryOp::Gt => SimpleRangeBoundKind::Lower(false), + BinaryOp::GtEq => SimpleRangeBoundKind::Lower(true), + BinaryOp::Lt => SimpleRangeBoundKind::Upper(false), + BinaryOp::LtEq => SimpleRangeBoundKind::Upper(true), + _ => return None, + }; + Some((table.as_deref(), column.as_str(), bound_kind, right)) +} + +pub(crate) fn simple_residual_projection_bound<'a>( + left: &'a Expr, + op: BinaryOp, + right: &'a Expr, +) -> Option<(Option<&'a str>, &'a str, BinaryOp, &'a Expr)> { + let Expr::Column { table, column } = left else { + return None; + }; + if !simple_bound_value_expr_is_constant(right) { + return None; + } + if !matches!( + op, + BinaryOp::Eq + | BinaryOp::NotEq + | BinaryOp::Gt + | BinaryOp::GtEq + | BinaryOp::Lt + | BinaryOp::LtEq + ) { + return None; + } + Some((table.as_deref(), column.as_str(), op, right)) +} + +/// A range/residual bound value is "constant" if it can be evaluated once +/// without row context: a literal, a parameter, or a cast of a literal or +/// parameter (e.g. `CAST('2010-01-01' AS DATE)`, which is how the parser +/// represents typed date literals). +pub(crate) fn simple_bound_value_expr_is_constant(expr: &Expr) -> bool { + match expr { + Expr::Literal(_) | Expr::Parameter(_) => true, + Expr::Cast { expr, .. } => simple_bound_value_expr_is_constant(expr), + _ => false, + } +} + +pub(crate) fn simple_int64_constant_expr_value( + expr: &Expr, + params: &[Value], +) -> Result> { + match expr { + Expr::Literal(Value::Int64(value)) => Ok(Some(*value)), + Expr::Parameter(index) => { + let Some(value) = index.checked_sub(1).and_then(|index| params.get(index)) else { + return Err(DbError::sql(format!("missing parameter ${index}"))); + }; + match value { + Value::Int64(value) => Ok(Some(*value)), + _ => Ok(None), + } + } + _ => Ok(None), + } +} + +pub(crate) fn simple_range_bounds_match_column_type( + column_type: ColumnType, + lower_bound: Option<&SimpleRangeBoundValue>, + upper_bound: Option<&SimpleRangeBoundValue>, +) -> bool { + lower_bound.is_none_or(|bound| simple_value_matches_column_type(column_type, &bound.value)) + && upper_bound + .is_none_or(|bound| simple_value_matches_column_type(column_type, &bound.value)) +} + +pub(crate) fn simple_value_matches_column_type(column_type: ColumnType, value: &Value) -> bool { + matches!( + (column_type, value), + (ColumnType::Int64, Value::Int64(_)) + | (ColumnType::Float64, Value::Float64(_)) + | (ColumnType::Text, Value::Text(_)) + | (ColumnType::Bool, Value::Bool(_)) + | (ColumnType::Blob, Value::Blob(_)) + | (ColumnType::Decimal, Value::Decimal { .. }) + | (ColumnType::Uuid, Value::Uuid(_)) + | (ColumnType::Timestamp, Value::TimestampMicros(_)) + | (ColumnType::Enum, Value::Enum { .. }) + | (ColumnType::IpAddr, Value::IpAddr { .. }) + | (ColumnType::Cidr, Value::Cidr { .. }) + | (ColumnType::MacAddr, Value::MacAddr { .. }) + | (ColumnType::Date, Value::DateDays(_)) + | (ColumnType::Time, Value::TimeMicros(_)) + | (ColumnType::TimestampTz, Value::TimestampTzMicros(_)) + | (ColumnType::Interval, Value::Interval { .. }) + | (ColumnType::Geometry, Value::Geometry(_)) + | (ColumnType::Geography, Value::Geography(_)) + ) +} + +pub(crate) fn simple_range_bound_matches( + candidate: &Value, + lower_bound: Option<&SimpleRangeBoundValue>, + upper_bound: Option<&SimpleRangeBoundValue>, +) -> Result { + if let Some(lower_bound) = lower_bound { + let ordering = simple_fast_compare_values(candidate, &lower_bound.value) + .map(Ok) + .unwrap_or_else(|| compare_values(candidate, &lower_bound.value))?; + let lower_matches = if lower_bound.inclusive { + ordering != std::cmp::Ordering::Less + } else { + ordering == std::cmp::Ordering::Greater + }; + if !lower_matches { + return Ok(false); + } + } + if let Some(upper_bound) = upper_bound { + let ordering = simple_fast_compare_values(candidate, &upper_bound.value) + .map(Ok) + .unwrap_or_else(|| compare_values(candidate, &upper_bound.value))?; + let upper_matches = if upper_bound.inclusive { + ordering != std::cmp::Ordering::Greater + } else { + ordering == std::cmp::Ordering::Less + }; + if !upper_matches { + return Ok(false); + } + } + Ok(true) +} + +pub(crate) fn simple_int64_range_start(bound: Option<&SimpleRangeBoundValue>) -> Option { + let Some(bound) = bound else { + return Some(i64::MIN); + }; + let Value::Int64(value) = &bound.value else { + return None; + }; + let value = *value; + if bound.inclusive { + Some(value) + } else { + value.checked_add(1) + } +} + +pub(crate) fn simple_int64_range_end_exclusive( + bound: Option<&SimpleRangeBoundValue>, +) -> Option { + let Some(bound) = bound else { + return Some(i64::MAX); + }; + let Value::Int64(value) = &bound.value else { + return None; + }; + let value = *value; + if bound.inclusive { + value.checked_add(1) + } else { + Some(value) + } +} + +pub(crate) fn simple_join_equality( + on: &Expr, +) -> Option<(QualifiedColumnRef<'_>, QualifiedColumnRef<'_>)> { + let Expr::Binary { left, op, right } = on else { + return None; + }; + if *op != BinaryOp::Eq { + return None; + } + let ( + Expr::Column { + table: left_table, + column: left_column, + }, + Expr::Column { + table: right_table, + column: right_column, + }, + ) = (&**left, &**right) + else { + return None; + }; + Some(( + QualifiedColumnRef { + table: left_table.as_deref(), + column: left_column, + }, + QualifiedColumnRef { + table: right_table.as_deref(), + column: right_column, + }, + )) +} + +pub(crate) fn simple_join_equalities( + on: &Expr, +) -> Option, QualifiedColumnRef<'_>)>> { + fn collect<'a>( + expr: &'a Expr, + equalities: &mut Vec<(QualifiedColumnRef<'a>, QualifiedColumnRef<'a>)>, + ) -> Option<()> { + match expr { + Expr::Binary { left, op, right } if *op == BinaryOp::And => { + collect(left, equalities)?; + collect(right, equalities)?; + Some(()) + } + _ => { + equalities.push(simple_join_equality(expr)?); + Some(()) + } + } + } + + let mut equalities = Vec::new(); + collect(on, &mut equalities)?; + if equalities.is_empty() { + return None; + } + Some(equalities) +} + +pub(crate) fn simple_indexed_join_constraint_equalities<'a>( + constraint: &'a JoinConstraint, + left: TableBindingRef<'a>, + right: TableBindingRef<'a>, + left_schema: &'a TableSchema, + right_schema: &'a TableSchema, +) -> Option, QualifiedColumnRef<'a>)>> { + match constraint { + JoinConstraint::On(on) => simple_join_equalities(on), + JoinConstraint::Using(columns) if !columns.is_empty() => { + let mut equalities = Vec::with_capacity(columns.len()); + for column in columns { + equalities.push(( + QualifiedColumnRef { + table: Some(left.binding_name()), + column: column.as_str(), + }, + QualifiedColumnRef { + table: Some(right.binding_name()), + column: column.as_str(), + }, + )); + } + Some(equalities) + } + JoinConstraint::Using(_) => None, + JoinConstraint::Natural => { + let common_columns = simple_indexed_join_natural_columns(left_schema, right_schema); + if common_columns.is_empty() { + return None; + } + let mut equalities = Vec::with_capacity(common_columns.len()); + for column in common_columns { + equalities.push(( + QualifiedColumnRef { + table: Some(left.binding_name()), + column, + }, + QualifiedColumnRef { + table: Some(right.binding_name()), + column, + }, + )); + } + Some(equalities) + } + } +} + +pub(crate) fn simple_indexed_join_using_columns( + constraint: &JoinConstraint, + left_schema: &TableSchema, + right_schema: &TableSchema, +) -> Vec { + match constraint { + JoinConstraint::Using(columns) => columns.clone(), + JoinConstraint::Natural => simple_indexed_join_natural_columns(left_schema, right_schema) + .into_iter() + .map(str::to_string) + .collect(), + JoinConstraint::On(_) => Vec::new(), + } +} + +pub(crate) fn simple_indexed_join_natural_columns<'a>( + left_schema: &'a TableSchema, + right_schema: &'a TableSchema, +) -> Vec<&'a str> { + left_schema + .columns + .iter() + .filter(|left_column| { + right_schema + .columns + .iter() + .any(|right_column| identifiers_equal(&left_column.name, &right_column.name)) + }) + .map(|column| column.name.as_str()) + .collect() +} + +pub(crate) fn project_simple_projection_row( + stored_row: &StoredRow, + projection_indexes: &[usize], +) -> QueryRow { + project_simple_projection_values(&stored_row.values, projection_indexes) +} + +pub(crate) fn project_simple_projection_value_vec( + values: &[Value], + projection_indexes: &[usize], +) -> Vec { + let mut projected = Vec::with_capacity(projection_indexes.len()); + for index in projection_indexes { + projected.push(values[*index].clone()); + } + projected +} + +pub(crate) fn project_simple_projection_values( + values: &[Value], + projection_indexes: &[usize], +) -> QueryRow { + let mut projected = SmallVec::<[Value; 4]>::with_capacity(projection_indexes.len()); + for index in projection_indexes { + projected.push(values[*index].clone()); + } + QueryRow::from_small_values(projected) +} + +pub(crate) fn simple_expression_projection_plan<'a>( + table_schema: &'a TableSchema, + table_name: &str, + binding_name: &str, + projection: &'a [SelectItem], +) -> Option> { + let mut sources = Vec::new(); + let mut column_names = Vec::new(); + for (item_index, item) in projection.iter().enumerate() { + match item { + SelectItem::Expr { expr, alias } => { + if let Expr::Column { table, column } = expr { + let column_index = simple_expression_projection_column_index( + table_schema, + table_name, + binding_name, + table.as_deref(), + column, + )?; + sources.push(SimpleExpressionProjectionSource::Column(column_index)); + column_names.push(alias.clone().unwrap_or_else(|| column.clone())); + } else { + sources.push(SimpleExpressionProjectionSource::Expr(expr)); + column_names.push( + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, item_index + 1)), + ); + } + } + SelectItem::Wildcard => { + for (column_index, column) in table_schema.columns.iter().enumerate() { + sources.push(SimpleExpressionProjectionSource::Column(column_index)); + column_names.push(column.name.clone()); + } + } + SelectItem::QualifiedWildcard(qualified_name) => { + if !identifiers_equal(qualified_name, table_name) + && !identifiers_equal(qualified_name, binding_name) + { + return None; + } + for (column_index, column) in table_schema.columns.iter().enumerate() { + sources.push(SimpleExpressionProjectionSource::Column(column_index)); + column_names.push(column.name.clone()); + } + } + } + } + Some(SimpleExpressionProjectionPlan { + sources, + column_names, + }) +} + +pub(crate) fn simple_expression_projection_column_index( + table_schema: &TableSchema, + table_name: &str, + binding_name: &str, + qualifier: Option<&str>, + column_name: &str, +) -> Option { + if let Some(qualifier) = qualifier { + if !identifiers_equal(qualifier, table_name) && !identifiers_equal(qualifier, binding_name) + { + return None; + } + } + table_schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, column_name)) +} + +pub(crate) fn simple_grouped_having_bindings(column_count: usize) -> Vec { + (0..column_count) + .map(|index| ColumnBinding::visible(None, format!("__grouped_having_col_{index}"))) + .collect() +} + +pub(crate) fn simple_grouped_having_column_name( + select: &Select, + table_name: &str, + binding_name: &str, + column_names: &[String], + synthetic_names: &[String], + table: Option<&str>, + column: &str, +) -> Option { + if let Some(table) = table { + if !identifiers_equal(table, table_name) && !identifiers_equal(table, binding_name) { + return None; + } + return unique_grouped_having_group_index(select, column) + .map(|index| synthetic_names[index].clone()); + } + + let alias_index = unique_grouped_having_column_index(column_names, column); + let group_index = unique_grouped_having_group_index(select, column); + match (alias_index, group_index) { + (Some(alias_index), None) => Some(synthetic_names[alias_index].clone()), + (None, Some(group_index)) => Some(synthetic_names[group_index].clone()), + (Some(alias_index), Some(group_index)) if alias_index == group_index => { + Some(synthetic_names[alias_index].clone()) + } + _ => None, + } +} + +pub(crate) fn analyze_simple_grouped_numeric_aggregate_binding( + expr: &Expr, + projection_index: usize, + table_name: &str, + binding_name: &str, + table_binding: TableBindingRef<'_>, + table_schema: &TableSchema, +) -> Option { + let Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return None; + }; + if !order_by.is_empty() || *within_group { + return None; + } + if name.eq_ignore_ascii_case("count") { + if *distinct && *star { + return None; + } + if args.is_empty() && *star { + return Some(SimpleGroupedNumericAggregateBinding { + kind: SimpleGroupedNumericAggregateKind::CountRows, + projection_index, + source_column_name: None, + source_column_index: None, + source_expr: None, + }); + } + if *star || args.len() != 1 || !expr_references_only_binding(&args[0], table_binding) { + return None; + } + if let Expr::Column { table, column } = &args[0] { + if let Some(table) = table.as_deref() { + if !identifiers_equal(table, table_name) && !identifiers_equal(table, binding_name) + { + return None; + } + } + let column_index = table_schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, column))?; + return Some(SimpleGroupedNumericAggregateBinding { + kind: if *distinct { + SimpleGroupedNumericAggregateKind::CountDistinct + } else { + SimpleGroupedNumericAggregateKind::CountNonNull + }, + projection_index, + source_column_name: Some(column.clone()), + source_column_index: Some(column_index), + source_expr: None, + }); + } + return Some(SimpleGroupedNumericAggregateBinding { + kind: if *distinct { + SimpleGroupedNumericAggregateKind::CountDistinct + } else { + SimpleGroupedNumericAggregateKind::CountNonNull + }, + projection_index, + source_column_name: None, + source_column_index: None, + source_expr: Some(args[0].clone()), + }); + } + + let kind = if name.eq_ignore_ascii_case("sum") { + if *distinct { + SimpleGroupedNumericAggregateKind::SumDistinct + } else { + SimpleGroupedNumericAggregateKind::Sum + } + } else if name.eq_ignore_ascii_case("avg") { + if *distinct { + SimpleGroupedNumericAggregateKind::AvgDistinct + } else { + SimpleGroupedNumericAggregateKind::Avg + } + } else if name.eq_ignore_ascii_case("total") { + if *distinct { + SimpleGroupedNumericAggregateKind::TotalDistinct + } else { + SimpleGroupedNumericAggregateKind::Total + } + } else if name.eq_ignore_ascii_case("stddev") || name.eq_ignore_ascii_case("stddev_samp") { + if *distinct { + SimpleGroupedNumericAggregateKind::StddevSampDistinct + } else { + SimpleGroupedNumericAggregateKind::StddevSamp + } + } else if name.eq_ignore_ascii_case("stddev_pop") { + if *distinct { + SimpleGroupedNumericAggregateKind::StddevPopDistinct + } else { + SimpleGroupedNumericAggregateKind::StddevPop + } + } else if name.eq_ignore_ascii_case("variance") || name.eq_ignore_ascii_case("var_samp") { + if *distinct { + SimpleGroupedNumericAggregateKind::VarSampDistinct + } else { + SimpleGroupedNumericAggregateKind::VarSamp + } + } else if name.eq_ignore_ascii_case("var_pop") { + if *distinct { + SimpleGroupedNumericAggregateKind::VarPopDistinct + } else { + SimpleGroupedNumericAggregateKind::VarPop + } + } else if name.eq_ignore_ascii_case("bool_and") { + if *distinct { + SimpleGroupedNumericAggregateKind::BoolAndDistinct + } else { + SimpleGroupedNumericAggregateKind::BoolAnd + } + } else if name.eq_ignore_ascii_case("bool_or") { + if *distinct { + SimpleGroupedNumericAggregateKind::BoolOrDistinct + } else { + SimpleGroupedNumericAggregateKind::BoolOr + } + } else if name.eq_ignore_ascii_case("min") { + if *distinct { + return None; + } + SimpleGroupedNumericAggregateKind::Min + } else if name.eq_ignore_ascii_case("max") { + if *distinct { + return None; + } + SimpleGroupedNumericAggregateKind::Max + } else { + return None; + }; + if *star || args.len() != 1 { + return None; + } + if !expr_references_only_binding(&args[0], table_binding) { + return None; + } + + if matches!( + kind, + SimpleGroupedNumericAggregateKind::Min | SimpleGroupedNumericAggregateKind::Max + ) { + return Some(SimpleGroupedNumericAggregateBinding { + kind, + projection_index, + source_column_name: None, + source_column_index: None, + source_expr: Some(args[0].clone()), + }); + } + + if let Expr::Column { table, column } = &args[0] { + if let Some(table) = table.as_deref() { + if !identifiers_equal(table, table_name) && !identifiers_equal(table, binding_name) { + return None; + } + } + let column_index = table_schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, column))?; + Some(SimpleGroupedNumericAggregateBinding { + kind, + projection_index, + source_column_name: Some(column.clone()), + source_column_index: Some(column_index), + source_expr: None, + }) + } else { + Some(SimpleGroupedNumericAggregateBinding { + kind, + projection_index, + source_column_name: None, + source_column_index: None, + source_expr: Some(args[0].clone()), + }) + } +} + +pub(crate) fn simple_aggregate_source_column_index( + aggregate: &SimpleGroupedNumericAggregateBinding, + table_schema: &TableSchema, +) -> Option { + if let Some(column_index) = aggregate.source_column_index { + return Some(column_index); + } + let Some(Expr::Column { column, .. }) = aggregate.source_expr.as_ref() else { + return None; + }; + schema_column_index(table_schema, column) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn collect_simple_grouped_numeric_projection_aggregates( + expr: &Expr, + table_name: &str, + binding_name: &str, + table_binding: TableBindingRef<'_>, + table_schema: &TableSchema, + aggregate_bindings: &mut Vec, + saw_supported_aggregate: &mut bool, +) -> Option<()> { + match expr { + Expr::Literal(_) | Expr::Parameter(_) => Some(()), + Expr::Unary { expr, .. } + | Expr::Cast { expr, .. } + | Expr::IsNull { expr, .. } + | Expr::Collate { expr, .. } => collect_simple_grouped_numeric_projection_aggregates( + expr, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + ), + Expr::Binary { left, right, .. } => { + collect_simple_grouped_numeric_projection_aggregates( + left, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + collect_simple_grouped_numeric_projection_aggregates( + right, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + ) + } + Expr::Between { + expr, low, high, .. + } => { + collect_simple_grouped_numeric_projection_aggregates( + expr, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + collect_simple_grouped_numeric_projection_aggregates( + low, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + collect_simple_grouped_numeric_projection_aggregates( + high, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + ) + } + Expr::InList { expr, items, .. } => { + collect_simple_grouped_numeric_projection_aggregates( + expr, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + for item in items { + collect_simple_grouped_numeric_projection_aggregates( + item, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + } + Some(()) + } + Expr::Like { + expr, + pattern, + escape, + .. + } => { + collect_simple_grouped_numeric_projection_aggregates( + expr, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + collect_simple_grouped_numeric_projection_aggregates( + pattern, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + if let Some(escape) = escape { + collect_simple_grouped_numeric_projection_aggregates( + escape, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + } + Some(()) + } + Expr::Function { args, .. } | Expr::Row(args) => { + for arg in args { + collect_simple_grouped_numeric_projection_aggregates( + arg, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + } + Some(()) + } + Expr::Case { + operand, + branches, + else_expr, + } => { + if let Some(operand) = operand { + collect_simple_grouped_numeric_projection_aggregates( + operand, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + } + for (condition, value) in branches { + collect_simple_grouped_numeric_projection_aggregates( + condition, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + collect_simple_grouped_numeric_projection_aggregates( + value, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + } + if let Some(else_expr) = else_expr { + collect_simple_grouped_numeric_projection_aggregates( + else_expr, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + } + Some(()) + } + Expr::Aggregate { .. } => { + let binding = analyze_simple_grouped_numeric_aggregate_binding( + expr, + usize::MAX, + table_name, + binding_name, + table_binding, + table_schema, + )?; + if !matches!(binding.kind, SimpleGroupedNumericAggregateKind::CountRows) { + *saw_supported_aggregate = true; + } + if !aggregate_bindings.iter().any(|existing| { + existing.kind == binding.kind + && existing.source_column_name == binding.source_column_name + && existing.source_expr == binding.source_expr + }) { + aggregate_bindings.push(binding); + } + Some(()) + } + Expr::Column { .. } + | Expr::RowNumber { .. } + | Expr::WindowFunction { .. } + | Expr::InSubquery { .. } + | Expr::CompareSubquery { .. } + | Expr::ScalarSubquery(_) + | Expr::Exists(_) => None, + } +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn collect_simple_grouped_numeric_having_aggregates( + expr: &Expr, + table_name: &str, + binding_name: &str, + table_binding: TableBindingRef<'_>, + table_schema: &TableSchema, + aggregate_bindings: &mut Vec, + saw_supported_aggregate: &mut bool, +) -> Option<()> { + match expr { + Expr::Literal(_) | Expr::Parameter(_) | Expr::Column { .. } => Some(()), + Expr::Unary { expr, .. } + | Expr::Cast { expr, .. } + | Expr::IsNull { expr, .. } + | Expr::Collate { expr, .. } => collect_simple_grouped_numeric_having_aggregates( + expr, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + ), + Expr::Binary { left, right, .. } => { + collect_simple_grouped_numeric_having_aggregates( + left, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + collect_simple_grouped_numeric_having_aggregates( + right, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + ) + } + Expr::Between { + expr, low, high, .. + } => { + collect_simple_grouped_numeric_having_aggregates( + expr, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + collect_simple_grouped_numeric_having_aggregates( + low, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + collect_simple_grouped_numeric_having_aggregates( + high, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + ) + } + Expr::InList { expr, items, .. } => { + collect_simple_grouped_numeric_having_aggregates( + expr, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + for item in items { + collect_simple_grouped_numeric_having_aggregates( + item, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + } + Some(()) + } + Expr::Like { + expr, + pattern, + escape, + .. + } => { + collect_simple_grouped_numeric_having_aggregates( + expr, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + collect_simple_grouped_numeric_having_aggregates( + pattern, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + if let Some(escape) = escape { + collect_simple_grouped_numeric_having_aggregates( + escape, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + } + Some(()) + } + Expr::Function { args, .. } | Expr::Row(args) => { + for arg in args { + collect_simple_grouped_numeric_having_aggregates( + arg, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + } + Some(()) + } + Expr::Case { + operand, + branches, + else_expr, + } => { + if let Some(operand) = operand { + collect_simple_grouped_numeric_having_aggregates( + operand, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + } + for (condition, value) in branches { + collect_simple_grouped_numeric_having_aggregates( + condition, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + collect_simple_grouped_numeric_having_aggregates( + value, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + } + if let Some(else_expr) = else_expr { + collect_simple_grouped_numeric_having_aggregates( + else_expr, + table_name, + binding_name, + table_binding, + table_schema, + aggregate_bindings, + saw_supported_aggregate, + )?; + } + Some(()) + } + Expr::Aggregate { .. } => { + let binding = analyze_simple_grouped_numeric_aggregate_binding( + expr, + usize::MAX, + table_name, + binding_name, + table_binding, + table_schema, + )?; + if !matches!(binding.kind, SimpleGroupedNumericAggregateKind::CountRows) { + *saw_supported_aggregate = true; + } + if !aggregate_bindings.iter().any(|existing| { + existing.kind == binding.kind + && existing.source_column_name == binding.source_column_name + && existing.source_expr == binding.source_expr + }) { + aggregate_bindings.push(binding); + } + Some(()) + } + Expr::RowNumber { .. } + | Expr::WindowFunction { .. } + | Expr::InSubquery { .. } + | Expr::CompareSubquery { .. } + | Expr::ScalarSubquery(_) + | Expr::Exists(_) => None, + } +} + +pub(crate) fn simple_select_item_column_index( + dataset: &Dataset, + table: Option<&str>, + column: &str, +) -> Option { + let matches = dataset + .columns + .iter() + .enumerate() + .filter(|(_, binding)| { + if !identifiers_equal(&binding.name, column) { + return false; + } + match table { + Some(table_name) => binding + .table + .as_deref() + .is_some_and(|binding_table| identifiers_equal(binding_table, table_name)), + None => !binding.hidden, + } + }) + .map(|(index, _)| index) + .collect::>(); + match matches.as_slice() { + [index] => Some(*index), + _ => None, + } +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn simple_join_projection_plan( + items: &[SelectItem], + eval_dataset: &Dataset, + left_table_name: &str, + left_alias: &Option, + left_schema: &TableSchema, + right_table_name: &str, + right_alias: &Option, + right_schema: &TableSchema, + using_join_columns: &[String], +) -> Option<(Vec, Vec)> { + let left_binding = left_alias.as_deref().unwrap_or(left_table_name); + let right_binding = right_alias.as_deref().unwrap_or(right_table_name); + let mut projection_plan = Vec::new(); + let mut column_names = Vec::new(); + + for (index, item) in items.iter().enumerate() { + match item { + SelectItem::Wildcard => { + if !using_join_columns.is_empty() { + for using_column in using_join_columns { + projection_plan.push(SimpleJoinProjectionSource::Expr( + merged_single_column_using_expr( + left_binding, + right_binding, + using_column, + ), + )); + column_names.push(using_column.to_string()); + } + projection_plan.extend( + left_schema + .columns + .iter() + .enumerate() + .filter(|(_, column)| { + !using_join_columns.iter().any(|using_column| { + identifiers_equal(&column.name, using_column) + }) + }) + .map(|(column_index, _)| { + SimpleJoinProjectionSource::Left(column_index) + }), + ); + column_names.extend( + left_schema + .columns + .iter() + .filter(|column| { + !using_join_columns.iter().any(|using_column| { + identifiers_equal(&column.name, using_column) + }) + }) + .map(|column| column.name.clone()), + ); + projection_plan.extend( + right_schema + .columns + .iter() + .enumerate() + .filter(|(_, column)| { + !using_join_columns.iter().any(|using_column| { + identifiers_equal(&column.name, using_column) + }) + }) + .map(|(column_index, _)| { + SimpleJoinProjectionSource::Right(column_index) + }), + ); + column_names.extend( + right_schema + .columns + .iter() + .filter(|column| { + !using_join_columns.iter().any(|using_column| { + identifiers_equal(&column.name, using_column) + }) + }) + .map(|column| column.name.clone()), + ); + } else { + projection_plan.extend( + left_schema + .columns + .iter() + .enumerate() + .map(|(column_index, _)| { + SimpleJoinProjectionSource::Left(column_index) + }), + ); + column_names + .extend(left_schema.columns.iter().map(|column| column.name.clone())); + projection_plan.extend( + right_schema + .columns + .iter() + .enumerate() + .map(|(column_index, _)| { + SimpleJoinProjectionSource::Right(column_index) + }), + ); + column_names.extend( + right_schema + .columns + .iter() + .map(|column| column.name.clone()), + ); + } + } + SelectItem::QualifiedWildcard(table_name) => { + if identifiers_equal(table_name, left_table_name) + || identifiers_equal(table_name, left_binding) + { + projection_plan.extend( + left_schema + .columns + .iter() + .enumerate() + .map(|(column_index, _)| { + SimpleJoinProjectionSource::Left(column_index) + }), + ); + column_names + .extend(left_schema.columns.iter().map(|column| column.name.clone())); + } else if identifiers_equal(table_name, right_table_name) + || identifiers_equal(table_name, right_binding) + { + projection_plan.extend( + right_schema + .columns + .iter() + .enumerate() + .map(|(column_index, _)| { + SimpleJoinProjectionSource::Right(column_index) + }), + ); + column_names.extend( + right_schema + .columns + .iter() + .map(|column| column.name.clone()), + ); + } else { + return None; + } + } + SelectItem::Expr { expr, alias } => { + let source = if let Expr::Column { table, column } = expr { + let left_index = left_schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, column)); + let right_index = right_schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, column)); + match table.as_deref() { + Some(table_name) + if identifiers_equal(table_name, left_table_name) + || identifiers_equal(table_name, left_binding) => + { + Some(SimpleJoinProjectionSource::Left(left_index?)) + } + Some(table_name) + if identifiers_equal(table_name, right_table_name) + || identifiers_equal(table_name, right_binding) => + { + Some(SimpleJoinProjectionSource::Right(right_index?)) + } + Some(_) => None, + None => match (left_index, right_index) { + (Some(left_index), None) => { + Some(SimpleJoinProjectionSource::Left(left_index)) + } + (None, Some(right_index)) => { + Some(SimpleJoinProjectionSource::Right(right_index)) + } + (Some(_), Some(_)) + if using_join_columns.iter().any(|using_column| { + identifiers_equal(column, using_column) + }) => + { + Some(SimpleJoinProjectionSource::Expr( + merged_single_column_using_expr( + left_binding, + right_binding, + column, + ), + )) + } + _ => None, + }, + } + } else if expr_resolves_against_dataset(expr, eval_dataset) { + Some(SimpleJoinProjectionSource::Expr(expr.clone())) + } else { + None + }?; + projection_plan.push(source); + column_names.push( + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + ); + } + } + } + + Some((projection_plan, column_names)) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn simple_join_projection_order_by_plan( + query: &Query, + projection_items: &[SelectItem], + projection_plan: &[SimpleJoinProjectionSource], + column_names: &[String], + left_table_name: &str, + left_alias: &Option, + left_schema: &TableSchema, + right_table_name: &str, + right_alias: &Option, + right_schema: &TableSchema, +) -> Result>> { + if query.order_by.is_empty() { + return Ok(None); + } + let left_binding = TableBindingRef { + name: left_table_name, + alias: left_alias, + }; + let right_binding = TableBindingRef { + name: right_table_name, + alias: right_alias, + }; + let direct_order = query + .order_by + .iter() + .map(|entry| { + let Expr::Column { + table: order_table, + column: order_column, + } = &entry.expr + else { + return None; + }; + + let mut projection_index = None; + for (index, source) in projection_plan.iter().enumerate() { + let source_matches = match source { + SimpleJoinProjectionSource::Left(column_index) => { + identifiers_equal(&left_schema.columns[*column_index].name, order_column) + && order_table.as_deref().is_none_or(|qualifier| { + matches_table_binding(left_binding, Some(qualifier)) + }) + } + SimpleJoinProjectionSource::Right(column_index) => { + identifiers_equal(&right_schema.columns[*column_index].name, order_column) + && order_table.as_deref().is_none_or(|qualifier| { + matches_table_binding(right_binding, Some(qualifier)) + }) + } + SimpleJoinProjectionSource::Expr(_) => false, + }; + let alias_matches = order_table.is_none() + && column_names + .get(index) + .is_some_and(|candidate| candidate.eq_ignore_ascii_case(order_column)); + if !source_matches && !alias_matches { + continue; + } + if projection_index.replace(index).is_some() { + return None; + } + } + + projection_index.map(|projection_index| SimpleOrderByPlan { + projection_index, + descending: entry.descending, + collation: entry.collation.clone(), + }) + }) + .collect::>>(); + if direct_order.is_some() { + return Ok(direct_order); + } + + Ok(projection_order_by_plan(&query.order_by, projection_items)) +} + +pub(crate) fn simple_join_eval_row( + left_values: Option<&[Value]>, + left_width: usize, + right_values: Option<&[Value]>, + right_width: usize, +) -> Vec { + let mut values = Vec::with_capacity(left_width + right_width); + match left_values { + Some(left_values) => values.extend(left_values.iter().cloned()), + None => values.extend((0..left_width).map(|_| Value::Null)), + } + match right_values { + Some(right_values) => values.extend(right_values.iter().cloned()), + None => values.extend((0..right_width).map(|_| Value::Null)), + } + values +} + +pub(crate) fn simple_join_key_from_indexes( + row: &[Value], + indexes: &[usize], +) -> Result>> { + let join_values = indexes + .iter() + .map(|index| { + row.get(*index) + .ok_or_else(|| DbError::internal("join row is shorter than table schema")) + }) + .collect::>>()?; + if join_values + .iter() + .any(|join_value| matches!(join_value, Value::Null)) + { + return Ok(None); + } + Row::new(join_values.iter().cloned().cloned().collect()) + .encode() + .map(Some) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn simple_join_filter_matches( + runtime: &EngineRuntime, + filter: Option<&Expr>, + eval_dataset: &Dataset, + left_values: Option<&[Value]>, + left_width: usize, + right_values: Option<&[Value]>, + right_width: usize, + params: &[Value], +) -> Result { + let Some(filter) = filter else { + return Ok(true); + }; + let joined_values = simple_join_eval_row(left_values, left_width, right_values, right_width); + Ok(matches!( + runtime.eval_expr( + filter, + eval_dataset, + &joined_values, + params, + &BTreeMap::new(), + None, + )?, + Value::Bool(true) + )) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn project_simple_join_row( + runtime: &EngineRuntime, + projection_plan: &[SimpleJoinProjectionSource], + eval_dataset: &Dataset, + left_values: Option<&[Value]>, + left_width: usize, + right_values: Option<&[Value]>, + right_width: usize, + params: &[Value], +) -> Result> { + let mut projected = Vec::with_capacity(projection_plan.len()); + let mut joined_values = None::>; + for slot in projection_plan { + match slot { + SimpleJoinProjectionSource::Left(index) => { + let value = left_values + .and_then(|row| row.get(*index)) + .cloned() + .unwrap_or(Value::Null); + projected.push(value); + } + SimpleJoinProjectionSource::Right(index) => { + let value = right_values + .and_then(|row| row.get(*index)) + .cloned() + .unwrap_or(Value::Null); + projected.push(value); + } + SimpleJoinProjectionSource::Expr(expr) => { + let joined_values = joined_values.get_or_insert_with(|| { + simple_join_eval_row(left_values, left_width, right_values, right_width) + }); + projected.push(runtime.eval_expr( + expr, + eval_dataset, + joined_values, + params, + &BTreeMap::new(), + None, + )?); + } + } + } + Ok(projected) +} + +pub(crate) fn simple_trigram_lookup(filter: &Expr) -> Option> { + match filter { + Expr::Like { + expr, + pattern, + escape, + negated, + .. + } if !negated && escape.is_none() => match (&**expr, &**pattern) { + (Expr::Column { table, column }, pattern @ (Expr::Literal(_) | Expr::Parameter(_))) => { + Some(SimpleTrigramLookup { + table_qualifier: table.as_deref(), + column_name: column.as_str(), + pattern_expr: pattern, + has_additional_filter: false, + }) + } + _ => None, + }, + Expr::Binary { + left, + op: BinaryOp::And, + right, + } => { + if let Some(mut lookup) = simple_trigram_lookup(left) { + lookup.has_additional_filter = true; + Some(lookup) + } else { + simple_trigram_lookup(right).map(|mut lookup| { + lookup.has_additional_filter = true; + lookup + }) + } + } + _ => None, + } +} + +pub(crate) fn simple_fulltext_lookup(filter: &Expr) -> Option> { + match filter { + Expr::Function { name, args } + if name.eq_ignore_ascii_case("fulltext_match") && args.len() == 2 => + { + Some(SimpleFullTextLookup { + index_name_expr: &args[0], + query_expr: &args[1], + }) + } + Expr::Binary { + left, + op: BinaryOp::And, + right, + } => simple_fulltext_lookup(left).or_else(|| simple_fulltext_lookup(right)), + _ => None, + } +} + +pub(crate) fn simple_spatial_join_predicate<'a>( + expr: &'a Expr, + left_binding: TableBindingRef<'a>, + right_binding: TableBindingRef<'a>, +) -> Option> { + let Expr::Function { name, args } = expr else { + return None; + }; + let lower = name.to_ascii_lowercase(); + let (left, right, radius_expr) = if lower == "st_dwithin" { + let [left, right, radius] = args.as_slice() else { + return None; + }; + if expr_has_column_ref(radius) { + return None; + } + (left, right, Some(radius)) + } else if matches!( + lower.as_str(), + "st_intersects" | "st_contains" | "st_within" | "st_equals" + ) { + let [left, right] = args.as_slice() else { + return None; + }; + (left, right, None) + } else { + return None; + }; + let left_ref = qualified_column_ref_expr(left)?; + let right_ref = qualified_column_ref_expr(right)?; + let left_is_left = matches_table_binding(left_binding, left_ref.table); + let left_is_right = matches_table_binding(right_binding, left_ref.table); + let right_is_left = matches_table_binding(left_binding, right_ref.table); + let right_is_right = matches_table_binding(right_binding, right_ref.table); + if (left_is_left && right_is_right) || (left_is_right && right_is_left) { + Some(SimpleSpatialJoinPredicate { + left: left_ref, + right: right_ref, + radius_expr, + }) + } else { + None + } +} + +pub(crate) fn simple_spatial_lookup(filter: &Expr) -> Option> { + match filter { + Expr::Function { name, args } if name.eq_ignore_ascii_case("st_dwithin") => { + let [left, right, radius] = args.as_slice() else { + return None; + }; + simple_spatial_column_value_pair(left, right).map( + |(table_qualifier, column_name, value_expr)| SimpleSpatialLookup { + table_qualifier, + column_name, + value_expr, + radius_expr: Some(radius), + }, + ) + } + Expr::Function { name, args } + if matches!( + name.to_ascii_lowercase().as_str(), + "st_intersects" | "st_contains" | "st_within" | "st_equals" + ) => + { + let [left, right] = args.as_slice() else { + return None; + }; + simple_spatial_column_value_pair(left, right).map( + |(table_qualifier, column_name, value_expr)| SimpleSpatialLookup { + table_qualifier, + column_name, + value_expr, + radius_expr: None, + }, + ) + } + Expr::Binary { + left, + op: BinaryOp::And, + right, + } => simple_spatial_lookup(left).or_else(|| simple_spatial_lookup(right)), + _ => None, + } +} + +pub(crate) fn simple_spatial_column_value_pair<'a>( + left: &'a Expr, + right: &'a Expr, +) -> Option<(Option<&'a str>, &'a str, &'a Expr)> { + match (left, right) { + (Expr::Column { table, column }, value) if !expr_has_column_ref(value) => { + Some((table.as_deref(), column.as_str(), value)) + } + (value, Expr::Column { table, column }) if !expr_has_column_ref(value) => { + Some((table.as_deref(), column.as_str(), value)) + } + _ => None, + } +} + +impl EngineRuntime { + fn analyze_simple_count_query<'a>( + &'a self, + query: &'a Query, + ) -> Result>> { + if !query.ctes.is_empty() + || !query.order_by.is_empty() + || query.limit.is_some() + || query.offset.is_some() + { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if !select.group_by.is_empty() + || select.having.is_some() + || select.distinct + || !select.distinct_on.is_empty() + || select.from.len() != 1 + || select.projection.len() != 1 + { + return Ok(None); + } + let FromItem::Table { + name, + alias: table_alias, + } = &select.from[0] + else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + { + return Ok(None); + } + let Some(table) = self.table_schema(name) else { + return Ok(None); + }; + if !generated_columns_are_stored(table) { + return Ok(None); + } + + let SelectItem::Expr { expr, alias } = &select.projection[0] else { + return Ok(None); + }; + let Expr::Aggregate { + name: aggregate_name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return Ok(None); + }; + if !aggregate_name.eq_ignore_ascii_case("count") + || !args.is_empty() + || *distinct + || !*star + || !order_by.is_empty() + || *within_group + { + return Ok(None); + } + + Ok(Some(SimpleCountQueryPlan { + table_name: name, + table_ref: table_alias.as_deref().unwrap_or(name), + filter: select.filter.as_ref(), + column_name: alias.clone().unwrap_or_else(|| infer_expr_name(expr, 1)), + })) + } + pub(crate) fn try_execute_simple_count_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_simple_count_query(query)? else { + return Ok(None); + }; + + let row_count = if let Some(filter) = plan.filter { + let table = self + .table_schema(plan.table_name) + .ok_or_else(|| DbError::sql(format!("unknown table {}", plan.table_name)))?; + dml::matching_row_ids( + self, + plan.table_name, + plan.table_ref, + table, + Some(filter), + params, + )? + .len() + } else { + self.visible_table_row_source(plan.table_name).map_or_else( + || { + self.table_data(plan.table_name) + .map_or(0, TableData::row_count) + }, + |source| source.row_count(), + ) + }; + let row_count = i64::try_from(row_count).map_err(|_| { + DbError::sql(format!( + "table {} exceeds COUNT(*) row-count limits", + plan.table_name + )) + })?; + Ok(Some(QueryResult::with_rows( + vec![plan.column_name], + vec![QueryRow::new(vec![Value::Int64(row_count)])], + ))) + } + fn analyze_simple_min_max_query<'a>( + &'a self, + query: &'a Query, + ) -> Result>> { + if !query.ctes.is_empty() + || !query.order_by.is_empty() + || query.limit.is_some() + || query.offset.is_some() + { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.filter.is_some() + || !select.group_by.is_empty() + || select.having.is_some() + || select.distinct + || !select.distinct_on.is_empty() + || select.from.len() != 1 + || select.projection.len() != 1 + { + return Ok(None); + } + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + { + return Ok(None); + } + let Some(table) = self.table_schema(name) else { + return Ok(None); + }; + if !generated_columns_are_stored(table) { + return Ok(None); + } + + let SelectItem::Expr { + expr, + alias: output_alias, + } = &select.projection[0] + else { + return Ok(None); + }; + let Expr::Aggregate { + name: aggregate_name, + args, + distinct, + star, + order_by, + within_group, + } = expr + else { + return Ok(None); + }; + if *distinct || *star || !order_by.is_empty() || *within_group || args.len() != 1 { + return Ok(None); + } + let is_max = if aggregate_name.eq_ignore_ascii_case("max") { + true + } else if aggregate_name.eq_ignore_ascii_case("min") { + false + } else { + return Ok(None); + }; + + let binding_name = alias.as_deref().unwrap_or(name); + let Expr::Column { + table: aggregate_table, + column: aggregate_column, + } = &args[0] + else { + return Ok(None); + }; + if let Some(aggregate_table) = aggregate_table.as_deref() { + if !identifiers_equal(aggregate_table, name) + && !identifiers_equal(aggregate_table, binding_name) + { + return Ok(None); + } + } + let Some(column_index) = table + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, aggregate_column)) + else { + return Ok(None); + }; + + Ok(Some(SimpleMinMaxQueryPlan { + table_name: name, + column_index, + is_max, + column_name: output_alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, 1)), + })) + } + pub(crate) fn try_execute_simple_min_max_query( + &self, + query: &Query, + ) -> Result> { + let Some(plan) = self.analyze_simple_min_max_query(query)? else { + return Ok(None); + }; + let Some(row_source) = self.visible_table_row_source(plan.table_name) else { + return Ok(None); + }; + Ok(Some( + self.simple_min_max_result_from_source(row_source, &plan)?, + )) + } + fn simple_min_max_result_from_source( + &self, + row_source: VisibleTableRowSource<'_>, + plan: &SimpleMinMaxQueryPlan<'_>, + ) -> Result { + let mut best = Value::Null; + for stored_row in row_source.rows() { + let stored_row = stored_row?; + update_simple_min_max_value( + &mut best, + stored_row.values()[plan.column_index].clone(), + plan.is_max, + )?; + } + Ok(QueryResult::with_rows( + vec![plan.column_name.clone()], + vec![QueryRow::new(vec![best])], + )) + } + fn simple_min_max_result_from_persisted_state( + &self, + store: &S, + state: PersistedTableState, + plan: &SimpleMinMaxQueryPlan<'_>, + ) -> Result { + let mut best = Value::Null; + visit_persisted_table_rows(store, state, |_, values| { + update_simple_min_max_value(&mut best, values[plan.column_index].clone(), plan.is_max) + })?; + Ok(QueryResult::with_rows( + vec![plan.column_name.clone()], + vec![QueryRow::new(vec![best])], + )) + } + pub(crate) fn try_execute_simple_deferred_count_query( + &self, + query: &Query, + pager: &PagerHandle, + wal: &WalHandle, + snapshot_lsn: u64, + ) -> Result> { + let Some(plan) = self.analyze_simple_count_query(query)? else { + return Ok(None); + }; + if plan.filter.is_some() { + return Ok(None); + } + if self.visible_table_is_temporary(plan.table_name) + || self.visible_table_row_source(plan.table_name).is_some() + || !self.has_deferred_tables() + { + return Ok(None); + } + if !self + .deferred_table_names() + .any(|candidate| identifiers_equal(candidate, plan.table_name)) + { + return Ok(None); + } + let Some(state) = self.persisted_table_state(plan.table_name) else { + return Ok(None); + }; + let row_count = if state.pointer.is_table_paged_manifest() + && state.pointer.head_page_id != 0 + && state.pointer.logical_len != 0 + { + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + read_persisted_table_row_count(&store, state) + .ok() + .and_then(|count| i64::try_from(count).ok()) + .unwrap_or(0) + } else { + self.catalog + .table_stats + .iter() + .find(|(name, _)| identifiers_equal(name, plan.table_name)) + .map(|(_, stats)| stats.row_count) + .or_else(|| { + if state.row_count == 0 + && state.pointer.head_page_id != 0 + && state.pointer.logical_len != 0 + { + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + read_persisted_table_row_count(&store, state) + .ok() + .and_then(|count| i64::try_from(count).ok()) + } else { + i64::try_from(state.row_count).ok() + } + }) + .unwrap_or(0) + }; + Ok(Some(QueryResult::with_rows( + vec![plan.column_name], + vec![QueryRow::new(vec![Value::Int64(row_count)])], + ))) + } + pub(crate) fn try_execute_simple_deferred_min_max_query( + &self, + query: &Query, + pager: &PagerHandle, + wal: &WalHandle, + snapshot_lsn: u64, + ) -> Result> { + let Some(plan) = self.analyze_simple_min_max_query(query)? else { + return Ok(None); + }; + if self.visible_table_is_temporary(plan.table_name) + || self.visible_table_row_source(plan.table_name).is_some() + || !self.has_deferred_tables() + { + return Ok(None); + } + if !self + .deferred_table_names() + .any(|candidate| identifiers_equal(candidate, plan.table_name)) + { + return Ok(None); + } + let Some(state) = self.persisted_table_state(plan.table_name) else { + return Ok(None); + }; + if !state.pointer.is_table_paged_manifest() { + return Ok(None); + } + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + Ok(Some(self.simple_min_max_result_from_persisted_state( + &store, state, &plan, + )?)) + } + pub(crate) fn try_execute_simple_grouped_count_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_simple_grouped_count_query(query, params)? else { + return Ok(None); + }; + let Some(source) = self.visible_table_row_source(plan.table_name) else { + return Ok(None); + }; + Ok(Some(self.simple_grouped_count_result_from_source( + source, &plan, params, + )?)) + } + pub(crate) fn analyze_simple_grouped_count_query<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result>> { + if !query.ctes.is_empty() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || select.from.len() != 1 + || select.group_by.is_empty() + || select.projection.len() != select.group_by.len() + 1 + { + return Ok(None); + } + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + { + return Ok(None); + } + + let table_schema = match self.table_schema(name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + let binding_name = alias.as_deref().unwrap_or(name); + let table_binding = TableBindingRef { name, alias }; + for group_expr in &select.group_by { + if expr_contains_recursive_unsupported_feature(group_expr) + || !expr_references_only_binding(group_expr, table_binding) + { + return Ok(None); + } + } + let group_eval_bindings = table_schema + .columns + .iter() + .map(|column| { + ColumnBinding::visible(Some(binding_name.to_string()), column.name.clone()) + }) + .collect::>(); + + let filter_expr = match select.filter.as_ref() { + Some(filter) + if !expr_contains_recursive_unsupported_feature(filter) + && expr_references_only_binding(filter, table_binding) => + { + Some(filter.clone()) + } + Some(_) => return Ok(None), + None => None, + }; + + let mut column_names = Vec::with_capacity(select.projection.len()); + let mut group_projection_needs_rewrite = false; + for (projection_item, group_expr) in select + .projection + .iter() + .take(select.group_by.len()) + .zip(&select.group_by) + { + let SelectItem::Expr { + expr: projection_group_expr, + alias: projection_group_alias, + } = projection_item + else { + return Ok(None); + }; + if !grouped_projection_expr_matches_group_expr( + projection_group_expr, + group_expr, + table_binding, + ) { + group_projection_needs_rewrite = true; + } + column_names.push( + projection_group_alias.clone().unwrap_or_else(|| { + infer_expr_name(projection_group_expr, column_names.len() + 1) + }), + ); + } + + let SelectItem::Expr { + expr: count_expr, + alias: count_alias, + } = &select.projection[select.group_by.len()] + else { + return Ok(None); + }; + let count_projection_index = select.group_by.len(); + let count_binding = SimpleGroupedNumericAggregateBinding { + kind: SimpleGroupedNumericAggregateKind::CountRows, + projection_index: count_projection_index, + source_column_name: None, + source_column_index: None, + source_expr: None, + }; + let direct_count = matches!( + count_expr, + Expr::Aggregate { + name, + args, + distinct, + star, + order_by, + within_group, + } if name.eq_ignore_ascii_case("count") + && args.is_empty() + && !*distinct + && *star + && order_by.is_empty() + && !*within_group + ); + + column_names.push( + count_alias + .clone() + .unwrap_or_else(|| infer_expr_name(count_expr, select.group_by.len() + 1)), + ); + let needs_projection_rewrite = group_projection_needs_rewrite || !direct_count; + let (projection_exprs, raw_projection_bindings, having, having_bindings, order_by) = + if needs_projection_rewrite { + let raw_projection_bindings = + simple_grouped_projection_bindings(select.group_by.len(), 1); + let raw_names = raw_projection_bindings + .iter() + .map(|binding| binding.name.clone()) + .collect::>(); + let projection_exprs = select + .projection + .iter() + .map(|item| { + let SelectItem::Expr { expr, .. } = item else { + return None; + }; + rewrite_simple_grouped_output_expr( + expr, + select.group_by.as_slice(), + name, + binding_name, + table_binding, + &raw_names, + std::slice::from_ref(&count_binding), + ) + }) + .collect::>>(); + let Some(projection_exprs) = projection_exprs else { + return Ok(None); + }; + let having = match select.having.as_ref() { + Some(having) => rewrite_simple_grouped_output_expr( + having, + select.group_by.as_slice(), + name, + binding_name, + table_binding, + &raw_names, + std::slice::from_ref(&count_binding), + ), + None => None, + }; + if select.having.is_some() && having.is_none() { + return Ok(None); + } + let order_by = projection_order_by_plan(&query.order_by, &select.projection); + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + ( + Some(projection_exprs), + Some(raw_projection_bindings.clone()), + having, + raw_projection_bindings, + order_by, + ) + } else { + let having_bindings = simple_grouped_having_bindings(column_names.len()); + let having_names = having_bindings + .iter() + .map(|binding| binding.name.clone()) + .collect::>(); + let having = match select.having.as_ref() { + Some(having) => self.rewrite_simple_grouped_having_expr( + having, + select, + name, + binding_name, + &column_names, + &having_names, + count_projection_index, + None, + )?, + None => None, + }; + if select.having.is_some() && having.is_none() { + return Ok(None); + } + let order_by = self.simple_grouped_order_by_plan( + query, + select, + name, + binding_name, + &column_names, + )?; + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + (None, None, having, having_bindings, order_by) + }; + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + Ok(Some(SimpleGroupedCountPlan { + table_name: name, + group_exprs: &select.group_by, + group_eval_bindings, + filter_expr, + column_names, + projection_exprs, + raw_projection_bindings, + having, + having_bindings, + order_by, + limit, + offset, + })) + } + fn simple_grouped_count_result_from_source( + &self, + row_source: VisibleTableRowSource<'_>, + plan: &SimpleGroupedCountPlan<'_>, + params: &[Value], + ) -> Result { + if let Some(result) = + self.try_simple_grouped_count_result_from_runtime_index(Some(row_source), plan, params)? + { + return Ok(result); + } + + let mut groups = Vec::::new(); + let mut group_positions = BTreeMap::, usize>::new(); + let group_dataset = Dataset::with_rows(plan.group_eval_bindings.clone(), Vec::new()); + for stored_row in row_source.rows() { + let stored_row = stored_row?; + if let Some(filter_expr) = plan.filter_expr.as_ref() { + if !matches!( + self.eval_expr( + filter_expr, + &group_dataset, + stored_row.values(), + params, + &BTreeMap::new(), + None, + )?, + Value::Bool(true) + ) { + continue; + } + } + + let group_values = evaluate_simple_grouped_values( + self, + plan.group_exprs, + &group_dataset, + stored_row.values(), + params, + )?; + let group_key = row_identity(&group_values)?; + let group_index = if let Some(group_index) = group_positions.get(&group_key).copied() { + group_index + } else { + groups.push(SimpleGroupedCountAggregate::new(group_values)); + let group_index = groups.len() - 1; + group_positions.insert(group_key, group_index); + group_index + }; + groups[group_index].count += 1; + } + + render_simple_grouped_count_groups(self, groups, plan, params) + } + pub(crate) fn try_simple_grouped_count_result_from_runtime_index( + &self, + row_source: Option>, + plan: &SimpleGroupedCountPlan<'_>, + params: &[Value], + ) -> Result> { + if plan.group_exprs.len() != 1 + || plan.filter_expr.is_some() + || plan.projection_exprs.is_some() + || plan.having.is_some() + { + return Ok(None); + } + let Expr::Column { + table: group_table, + column: group_column, + } = &plan.group_exprs[0] + else { + return Ok(None); + }; + if group_table + .as_deref() + .is_some_and(|table| !identifiers_equal(table, plan.table_name)) + { + return Ok(None); + } + let Some(table_schema) = self.table_schema(plan.table_name) else { + return Ok(None); + }; + let Some(group_column_index) = schema_column_index(table_schema, group_column) else { + return Ok(None); + }; + let Some(index) = self.single_column_btree_index(plan.table_name, group_column) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { + return Ok(None); + }; + + let mut groups = Vec::new(); + match keys { + RuntimeBtreeKeys::UniqueInt64(entries, deleted) => { + groups.reserve(entries.len()); + for (key, row_id) in entries.iter() { + if deleted.contains(&row_id) { + continue; + } + groups.push(SimpleGroupedCountAggregate { + group_values: vec![Value::Int64(key)], + count: 1, + }); + } + } + RuntimeBtreeKeys::NonUniqueInt64(entries, deleted) => { + groups.reserve(entries.len()); + for (key, row_ids) in entries.iter() { + let count = row_ids + .iter() + .filter(|row_id| !deleted.contains(row_id)) + .count(); + if count == 0 { + continue; + } + groups.push(SimpleGroupedCountAggregate { + group_values: vec![Value::Int64(key)], + count: i64::try_from(count).map_err(|_| { + DbError::constraint( + "grouped COUNT index bucket exceeds INT64 row-count limits", + ) + })?, + }); + } + } + RuntimeBtreeKeys::UniqueUuid(entries, deleted) => { + groups.reserve(entries.len()); + for (key, row_id) in entries.iter() { + if deleted.contains(row_id) { + continue; + } + groups.push(SimpleGroupedCountAggregate { + group_values: vec![Value::Uuid(*key)], + count: 1, + }); + } + } + RuntimeBtreeKeys::NonUniqueUuid(entries, deleted) => { + groups.reserve(entries.len()); + for (key, row_ids) in entries.iter() { + let count = row_ids + .iter() + .filter(|row_id| !deleted.contains(row_id)) + .count(); + if count == 0 { + continue; + } + groups.push(SimpleGroupedCountAggregate { + group_values: vec![Value::Uuid(*key)], + count: i64::try_from(count).map_err(|_| { + DbError::constraint( + "grouped COUNT index bucket exceeds INT64 row-count limits", + ) + })?, + }); + } + } + RuntimeBtreeKeys::UniqueEncoded(entries, deleted) => { + groups.reserve(entries.len()); + for (key, row_id) in entries.iter() { + if deleted.contains(row_id) { + continue; + } + let Some(values) = Self::runtime_index_key_values_to_group_values( + key, + Some(*row_id), + row_source, + &[group_column_index], + )? + else { + continue; + }; + groups.push(SimpleGroupedCountAggregate { + group_values: values, + count: 1, + }); + } + } + RuntimeBtreeKeys::NonUniqueEncoded(entries, deleted) => { + groups.reserve(entries.len()); + for (key, row_ids) in entries.iter() { + let count = row_ids + .iter() + .filter(|row_id| !deleted.contains(row_id)) + .count(); + if count == 0 { + continue; + } + let Some(row_id) = row_ids + .iter() + .copied() + .find(|row_id| !deleted.contains(row_id)) + else { + continue; + }; + let Some(values) = Self::runtime_index_key_values_to_group_values( + key, + Some(row_id), + row_source, + &[group_column_index], + )? + else { + continue; + }; + groups.push(SimpleGroupedCountAggregate { + group_values: values, + count: i64::try_from(count).map_err(|_| { + DbError::constraint( + "grouped COUNT index bucket exceeds INT64 row-count limits", + ) + })?, + }); + } + } + } + + Ok(Some(render_simple_grouped_count_groups( + self, groups, plan, params, + )?)) + } + pub(crate) fn try_execute_simple_grouped_count_sql_from_runtime_index( + &self, + table_name: &str, + group_column: &str, + ) -> Result> { + if self.security_rules_active()? + || self + .visible_view(table_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(table_name) + { + return Ok(None); + } + let Some(table_schema) = self.table_schema(table_name) else { + return Ok(None); + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + let Some(group_column_index) = schema_column_index(table_schema, group_column) else { + return Ok(None); + }; + let Some(index) = self.single_column_btree_index(table_name, group_column) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { + return Ok(None); + }; + + let mut groups = Vec::new(); + match keys { + RuntimeBtreeKeys::UniqueInt64(entries, deleted) => { + groups.reserve(entries.len()); + for (key, row_id) in entries.iter() { + if deleted.contains(&row_id) { + continue; + } + groups.push(SimpleGroupedCountAggregate { + group_values: vec![Value::Int64(key)], + count: 1, + }); + } + } + RuntimeBtreeKeys::NonUniqueInt64(entries, deleted) => { + groups.reserve(entries.len()); + for (key, row_ids) in entries.iter() { + let count = row_ids + .iter() + .filter(|row_id| !deleted.contains(row_id)) + .count(); + if count == 0 { + continue; + } + groups.push(SimpleGroupedCountAggregate { + group_values: vec![Value::Int64(key)], + count: i64::try_from(count).map_err(|_| { + DbError::constraint( + "grouped COUNT index bucket exceeds INT64 row-count limits", + ) + })?, + }); + } + } + RuntimeBtreeKeys::UniqueUuid(entries, deleted) => { + groups.reserve(entries.len()); + for (key, row_id) in entries.iter() { + if deleted.contains(row_id) { + continue; + } + groups.push(SimpleGroupedCountAggregate { + group_values: vec![Value::Uuid(*key)], + count: 1, + }); + } + } + RuntimeBtreeKeys::NonUniqueUuid(entries, deleted) => { + groups.reserve(entries.len()); + for (key, row_ids) in entries.iter() { + let count = row_ids + .iter() + .filter(|row_id| !deleted.contains(row_id)) + .count(); + if count == 0 { + continue; + } + groups.push(SimpleGroupedCountAggregate { + group_values: vec![Value::Uuid(*key)], + count: i64::try_from(count).map_err(|_| { + DbError::constraint( + "grouped COUNT index bucket exceeds INT64 row-count limits", + ) + })?, + }); + } + } + RuntimeBtreeKeys::UniqueEncoded(entries, deleted) => { + groups.reserve(entries.len()); + for (key, row_id) in entries.iter() { + if deleted.contains(row_id) { + continue; + } + let Some(value) = Self::decode_runtime_index_group_key(key) else { + return Ok(None); + }; + groups.push(SimpleGroupedCountAggregate { + group_values: vec![value], + count: 1, + }); + } + } + RuntimeBtreeKeys::NonUniqueEncoded(entries, deleted) => { + groups.reserve(entries.len()); + for (key, row_ids) in entries.iter() { + let count = row_ids + .iter() + .filter(|row_id| !deleted.contains(row_id)) + .count(); + if count == 0 { + continue; + } + let Some(value) = Self::decode_runtime_index_group_key(key) else { + return Ok(None); + }; + groups.push(SimpleGroupedCountAggregate { + group_values: vec![value], + count: i64::try_from(count).map_err(|_| { + DbError::constraint( + "grouped COUNT index bucket exceeds INT64 row-count limits", + ) + })?, + }); + } + } + } + + let mut rows = groups + .into_iter() + .map(SimpleGroupedCountAggregate::into_row) + .collect::>(); + sort_query_rows_by_projection_order( + Some(self), + &mut rows, + &[SimpleOrderByPlan { + projection_index: 0, + descending: false, + collation: None, + }], + )?; + Ok(Some(QueryResult::with_rows( + vec![ + table_schema.columns[group_column_index].name.clone(), + "col2".to_string(), + ], + rows, + ))) + } + fn simple_grouped_count_result_from_persisted_state( + &self, + store: &S, + state: PersistedTableState, + plan: &SimpleGroupedCountPlan<'_>, + params: &[Value], + ) -> Result { + let mut groups = Vec::::new(); + let mut group_positions = BTreeMap::, usize>::new(); + let group_dataset = Dataset::with_rows(plan.group_eval_bindings.clone(), Vec::new()); + visit_persisted_table_rows(store, state, |_, values| { + if let Some(filter_expr) = plan.filter_expr.as_ref() { + if !matches!( + self.eval_expr( + filter_expr, + &group_dataset, + values, + params, + &BTreeMap::new(), + None, + )?, + Value::Bool(true) + ) { + return Ok(()); + } + } + + let group_values = evaluate_simple_grouped_values( + self, + plan.group_exprs, + &group_dataset, + values, + params, + )?; + let group_key = row_identity(&group_values)?; + let group_index = if let Some(group_index) = group_positions.get(&group_key).copied() { + group_index + } else { + groups.push(SimpleGroupedCountAggregate::new(group_values)); + let group_index = groups.len() - 1; + group_positions.insert(group_key, group_index); + group_index + }; + groups[group_index].count += 1; + Ok(()) + })?; + + render_simple_grouped_count_groups(self, groups, plan, params) + } + pub(crate) fn try_execute_simple_grouped_numeric_aggregate_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_simple_grouped_numeric_aggregate_query(query, params)? else { + return Ok(None); + }; + let Some(source) = self.visible_table_row_source(plan.table_name) else { + return Ok(None); + }; + Ok(Some( + self.simple_grouped_numeric_aggregate_result_from_source(source, &plan, params)?, + )) + } + fn analyze_simple_grouped_numeric_aggregate_query<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result>> { + if !query.ctes.is_empty() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || select.from.len() != 1 + || select.projection.len() <= select.group_by.len() + { + return Ok(None); + } + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + { + return Ok(None); + } + + let table_schema = match self.table_schema(name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + let binding_name = alias.as_deref().unwrap_or(name); + let table_binding = TableBindingRef { name, alias }; + for group_expr in &select.group_by { + if expr_contains_recursive_unsupported_feature(group_expr) + || !expr_references_only_binding(group_expr, table_binding) + { + return Ok(None); + } + } + let group_eval_bindings = table_schema + .columns + .iter() + .map(|column| { + ColumnBinding::visible(Some(binding_name.to_string()), column.name.clone()) + }) + .collect::>(); + + let filter_expr = match select.filter.as_ref() { + Some(filter) + if !expr_contains_recursive_unsupported_feature(filter) + && expr_references_only_binding(filter, table_binding) => + { + Some(filter.clone()) + } + Some(_) => return Ok(None), + None => None, + }; + + let mut column_names = Vec::with_capacity(select.projection.len()); + let mut group_projection_needs_rewrite = false; + for (projection_item, group_expr) in select + .projection + .iter() + .take(select.group_by.len()) + .zip(&select.group_by) + { + let SelectItem::Expr { + expr: projection_group_expr, + alias: projection_group_alias, + } = projection_item + else { + return Ok(None); + }; + if !grouped_projection_expr_matches_group_expr( + projection_group_expr, + group_expr, + table_binding, + ) { + group_projection_needs_rewrite = true; + } + column_names.push( + projection_group_alias.clone().unwrap_or_else(|| { + infer_expr_name(projection_group_expr, column_names.len() + 1) + }), + ); + } + + let mut aggregate_bindings = Vec::new(); + let mut saw_supported_aggregate = false; + let mut projection_exprs: Option> = None; + for (projection_index, projection_item) in select + .projection + .iter() + .enumerate() + .skip(select.group_by.len()) + { + let SelectItem::Expr { + expr: projection_expr, + alias, + } = projection_item + else { + return Ok(None); + }; + let binding = if let Some(binding) = analyze_simple_grouped_numeric_aggregate_binding( + projection_expr, + projection_index, + name, + binding_name, + table_binding, + table_schema, + ) { + if !matches!(binding.kind, SimpleGroupedNumericAggregateKind::CountRows) { + saw_supported_aggregate = true; + } + Some(binding) + } else { + if collect_simple_grouped_numeric_projection_aggregates( + projection_expr, + name, + binding_name, + table_binding, + table_schema, + &mut aggregate_bindings, + &mut saw_supported_aggregate, + ) + .is_none() + { + return Ok(None); + } + projection_exprs.get_or_insert_with(Vec::new); + None + }; + column_names.push( + alias + .clone() + .unwrap_or_else(|| infer_expr_name(projection_expr, projection_index + 1)), + ); + if let Some(binding) = binding { + aggregate_bindings.push(binding); + } + } + let mut having_requires_projection_rewrite = false; + if let Some(having) = select.having.as_ref() { + let aggregate_count_before = aggregate_bindings.len(); + if collect_simple_grouped_numeric_having_aggregates( + having, + name, + binding_name, + table_binding, + table_schema, + &mut aggregate_bindings, + &mut saw_supported_aggregate, + ) + .is_none() + { + return Ok(None); + } + having_requires_projection_rewrite = aggregate_bindings.len() != aggregate_count_before; + } + if aggregate_bindings.is_empty() { + return Ok(None); + } + let (projection_exprs, raw_projection_bindings, having, having_bindings, order_by) = + if group_projection_needs_rewrite + || projection_exprs.is_some() + || having_requires_projection_rewrite + { + let raw_projection_bindings = simple_grouped_projection_bindings( + select.group_by.len(), + aggregate_bindings.len(), + ); + let raw_names = raw_projection_bindings + .iter() + .map(|binding| binding.name.clone()) + .collect::>(); + let mut rewritten_projection_exprs = Vec::with_capacity(select.projection.len()); + for projection_item in &select.projection { + let SelectItem::Expr { expr, .. } = projection_item else { + return Ok(None); + }; + let Some(rewritten) = rewrite_simple_grouped_output_expr( + expr, + select.group_by.as_slice(), + name, + binding_name, + table_binding, + &raw_names, + &aggregate_bindings, + ) else { + return Ok(None); + }; + rewritten_projection_exprs.push(rewritten); + } + let having = match select.having.as_ref() { + Some(having) => rewrite_simple_grouped_output_expr( + having, + select.group_by.as_slice(), + name, + binding_name, + table_binding, + &raw_names, + &aggregate_bindings, + ), + None => None, + }; + if select.having.is_some() && having.is_none() { + return Ok(None); + } + let order_by = projection_order_by_plan(&query.order_by, &select.projection); + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + ( + Some(rewritten_projection_exprs), + Some(raw_projection_bindings.clone()), + having, + raw_projection_bindings, + order_by, + ) + } else { + let having_bindings = simple_grouped_having_bindings(column_names.len()); + let having_names = having_bindings + .iter() + .map(|binding| binding.name.clone()) + .collect::>(); + let having = match select.having.as_ref() { + Some(having) => self.rewrite_simple_grouped_having_expr_with_bindings( + having, + select, + name, + binding_name, + &column_names, + &having_names, + &aggregate_bindings, + )?, + None => None, + }; + if select.having.is_some() && having.is_none() { + return Ok(None); + } + let order_by = self.simple_grouped_order_by_plan( + query, + select, + name, + binding_name, + &column_names, + )?; + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + (None, None, having, having_bindings, order_by) + }; + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + Ok(Some(SimpleGroupedNumericAggregatePlan { + table_name: name, + group_exprs: &select.group_by, + group_eval_bindings, + filter_expr, + column_names, + aggregate_bindings, + projection_exprs, + raw_projection_bindings, + having, + having_bindings, + order_by, + limit, + offset, + })) + } + fn simple_grouped_numeric_aggregate_result_from_source( + &self, + row_source: VisibleTableRowSource<'_>, + plan: &SimpleGroupedNumericAggregatePlan<'_>, + params: &[Value], + ) -> Result { + if let Some(result) = + self.try_simple_scalar_int64_numeric_aggregate_from_source(row_source, plan)? + { + return Ok(result); + } + if let Some(result) = + self.try_simple_scalar_filtered_numeric_aggregate_from_source(row_source, plan, params)? + { + return Ok(result); + } + + let mut groups = Vec::::new(); + let mut group_positions = BTreeMap::, usize>::new(); + let group_dataset = Dataset::with_rows(plan.group_eval_bindings.clone(), Vec::new()); + if plan.group_exprs.is_empty() { + groups.push(SimpleGroupedNumericAggregate::new( + Vec::new(), + plan.aggregate_bindings.len(), + )); + group_positions.insert(row_identity(&[])?, 0); + } + for stored_row in row_source.rows() { + let stored_row = stored_row?; + if let Some(filter_expr) = plan.filter_expr.as_ref() { + if !matches!( + self.eval_expr( + filter_expr, + &group_dataset, + stored_row.values(), + params, + &BTreeMap::new(), + None, + )?, + Value::Bool(true) + ) { + continue; + } + } + + let group_values = evaluate_simple_grouped_values( + self, + plan.group_exprs, + &group_dataset, + stored_row.values(), + params, + )?; + let group_key = row_identity(&group_values)?; + let group_index = if let Some(group_index) = group_positions.get(&group_key).copied() { + group_index + } else { + groups.push(SimpleGroupedNumericAggregate::new( + group_values, + plan.aggregate_bindings.len(), + )); + let group_index = groups.len() - 1; + group_positions.insert(group_key, group_index); + group_index + }; + groups[group_index].count += 1; + for (aggregate_index, aggregate) in plan.aggregate_bindings.iter().enumerate() { + match aggregate.kind { + SimpleGroupedNumericAggregateKind::CountNonNull => { + if let Some(source_column_index) = aggregate.source_column_index { + groups[group_index].count_non_null( + aggregate_index, + &stored_row.values()[source_column_index], + ); + } else if let Some(source_expr) = aggregate.source_expr.as_ref() { + let value = self.eval_expr( + source_expr, + &group_dataset, + stored_row.values(), + params, + &BTreeMap::new(), + None, + )?; + groups[group_index].count_non_null(aggregate_index, &value); + } + } + SimpleGroupedNumericAggregateKind::CountDistinct => { + if let Some(source_column_index) = aggregate.source_column_index { + groups[group_index].count_distinct( + aggregate_index, + &stored_row.values()[source_column_index], + )?; + } else if let Some(source_expr) = aggregate.source_expr.as_ref() { + let value = self.eval_expr( + source_expr, + &group_dataset, + stored_row.values(), + params, + &BTreeMap::new(), + None, + )?; + groups[group_index].count_distinct(aggregate_index, &value)?; + } + } + SimpleGroupedNumericAggregateKind::Sum + | SimpleGroupedNumericAggregateKind::SumDistinct + | SimpleGroupedNumericAggregateKind::Avg + | SimpleGroupedNumericAggregateKind::AvgDistinct + | SimpleGroupedNumericAggregateKind::Total + | SimpleGroupedNumericAggregateKind::TotalDistinct => { + if let Some(source_column_index) = aggregate.source_column_index { + if matches!( + aggregate.kind, + SimpleGroupedNumericAggregateKind::SumDistinct + | SimpleGroupedNumericAggregateKind::AvgDistinct + | SimpleGroupedNumericAggregateKind::TotalDistinct + ) { + groups[group_index].add_numeric_distinct( + aggregate_index, + &stored_row.values()[source_column_index], + )?; + } else { + groups[group_index].add_numeric( + aggregate_index, + &stored_row.values()[source_column_index], + )?; + } + } else if let Some(source_expr) = aggregate.source_expr.as_ref() { + let value = self.eval_expr( + source_expr, + &group_dataset, + stored_row.values(), + params, + &BTreeMap::new(), + None, + )?; + if matches!( + aggregate.kind, + SimpleGroupedNumericAggregateKind::SumDistinct + | SimpleGroupedNumericAggregateKind::AvgDistinct + | SimpleGroupedNumericAggregateKind::TotalDistinct + ) { + groups[group_index] + .add_numeric_distinct(aggregate_index, &value)?; + } else { + groups[group_index].add_numeric(aggregate_index, &value)?; + } + } + } + SimpleGroupedNumericAggregateKind::StddevSamp + | SimpleGroupedNumericAggregateKind::StddevSampDistinct + | SimpleGroupedNumericAggregateKind::StddevPop + | SimpleGroupedNumericAggregateKind::StddevPopDistinct + | SimpleGroupedNumericAggregateKind::VarSamp + | SimpleGroupedNumericAggregateKind::VarSampDistinct + | SimpleGroupedNumericAggregateKind::VarPop + | SimpleGroupedNumericAggregateKind::VarPopDistinct => { + if let Some(source_column_index) = aggregate.source_column_index { + if aggregate.kind.uses_distinct() { + groups[group_index].add_variance_distinct( + aggregate_index, + &stored_row.values()[source_column_index], + )?; + } else { + groups[group_index].add_variance( + aggregate_index, + &stored_row.values()[source_column_index], + )?; + } + } else if let Some(source_expr) = aggregate.source_expr.as_ref() { + let value = self.eval_expr( + source_expr, + &group_dataset, + stored_row.values(), + params, + &BTreeMap::new(), + None, + )?; + if aggregate.kind.uses_distinct() { + groups[group_index] + .add_variance_distinct(aggregate_index, &value)?; + } else { + groups[group_index].add_variance(aggregate_index, &value)?; + } + } + } + SimpleGroupedNumericAggregateKind::BoolAnd + | SimpleGroupedNumericAggregateKind::BoolAndDistinct + | SimpleGroupedNumericAggregateKind::BoolOr + | SimpleGroupedNumericAggregateKind::BoolOrDistinct => { + if let Some(source_column_index) = aggregate.source_column_index { + if aggregate.kind.uses_distinct() { + groups[group_index].add_bool_distinct( + aggregate_index, + &stored_row.values()[source_column_index], + )?; + } else { + groups[group_index].add_bool( + aggregate_index, + &stored_row.values()[source_column_index], + )?; + } + } else if let Some(source_expr) = aggregate.source_expr.as_ref() { + let value = self.eval_expr( + source_expr, + &group_dataset, + stored_row.values(), + params, + &BTreeMap::new(), + None, + )?; + if aggregate.kind.uses_distinct() { + groups[group_index].add_bool_distinct(aggregate_index, &value)?; + } else { + groups[group_index].add_bool(aggregate_index, &value)?; + } + } + } + SimpleGroupedNumericAggregateKind::Min + | SimpleGroupedNumericAggregateKind::Max => { + let Some(source_expr) = aggregate.source_expr.as_ref() else { + continue; + }; + let value = self.eval_expr( + source_expr, + &group_dataset, + stored_row.values(), + params, + &BTreeMap::new(), + None, + )?; + update_simple_min_max_value( + &mut groups[group_index].extreme_values[aggregate_index], + value, + matches!(aggregate.kind, SimpleGroupedNumericAggregateKind::Max), + )?; + } + SimpleGroupedNumericAggregateKind::CountRows => {} + } + } + } + + render_simple_grouped_numeric_aggregate_groups(self, groups, plan, params) + } + fn try_simple_scalar_int64_numeric_aggregate_from_source( + &self, + row_source: VisibleTableRowSource<'_>, + plan: &SimpleGroupedNumericAggregatePlan<'_>, + ) -> Result> { + let Some(column_index) = self.simple_scalar_int64_aggregate_column(plan) else { + return Ok(None); + }; + let mut stats = SimpleScalarInt64AggregateStats::default(); + row_source.visit_int64_column_values(column_index, |_, value| { + stats.add(value); + Ok(()) + })?; + Ok(Some(QueryResult::with_rows( + plan.column_names.clone(), + vec![QueryRow::new(stats.into_values(&plan.aggregate_bindings))], + ))) + } + fn simple_scalar_int64_aggregate_column( + &self, + plan: &SimpleGroupedNumericAggregatePlan<'_>, + ) -> Option { + if !plan.group_exprs.is_empty() + || plan.filter_expr.is_some() + || plan.projection_exprs.is_some() + || plan.having.is_some() + || plan.order_by.is_some() + || plan.limit.is_some() + || plan.offset != 0 + { + return None; + } + let table_schema = self.table_schema(plan.table_name)?; + let mut scalar_column_index = None; + for aggregate in &plan.aggregate_bindings { + let column_index = match aggregate.kind { + SimpleGroupedNumericAggregateKind::CountRows => continue, + SimpleGroupedNumericAggregateKind::CountNonNull + | SimpleGroupedNumericAggregateKind::Sum + | SimpleGroupedNumericAggregateKind::Avg + | SimpleGroupedNumericAggregateKind::Min + | SimpleGroupedNumericAggregateKind::Max => { + simple_aggregate_source_column_index(aggregate, table_schema)? + } + _ => return None, + }; + if table_schema.columns.get(column_index)?.column_type != ColumnType::Int64 { + return None; + } + if scalar_column_index + .replace(column_index) + .is_some_and(|existing| existing != column_index) + { + return None; + } + } + scalar_column_index + } + fn try_simple_scalar_filtered_numeric_aggregate_from_source( + &self, + row_source: VisibleTableRowSource<'_>, + plan: &SimpleGroupedNumericAggregatePlan<'_>, + params: &[Value], + ) -> Result> { + if !plan.group_exprs.is_empty() + || plan.projection_exprs.is_some() + || plan.having.is_some() + || plan.order_by.is_some() + || plan.limit.is_some() + || plan.offset != 0 + { + return Ok(None); + } + let Some(filter_expr) = plan.filter_expr.as_ref() else { + return Ok(None); + }; + let Some((_filter_table, filter_column, filter_value_expr)) = + simple_btree_lookup(filter_expr) + else { + return Ok(None); + }; + let Some(table_schema) = self.table_schema(plan.table_name) else { + return Ok(None); + }; + let Some(filter_column_index) = schema_column_index(table_schema, filter_column) else { + return Ok(None); + }; + if plan.aggregate_bindings.iter().any(|aggregate| { + !matches!( + aggregate.kind, + SimpleGroupedNumericAggregateKind::CountRows + | SimpleGroupedNumericAggregateKind::Sum + ) || (matches!(aggregate.kind, SimpleGroupedNumericAggregateKind::Sum) + && aggregate.source_column_index.is_none()) + }) { + return Ok(None); + } + + let filter_value = self.eval_expr( + filter_value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?; + let mut row_count = 0_i64; + let mut numeric_states = vec![ + SimpleGroupedNumericState { + numeric_count: 0, + total_int: 0, + total_float: 0.0, + saw_float: false, + saw_value: false, + }; + plan.aggregate_bindings.len() + ]; + + for stored_row in row_source.rows() { + let stored_row = stored_row?; + if compare_values(&stored_row.values()[filter_column_index], &filter_value)? + != std::cmp::Ordering::Equal + { + continue; + } + row_count += 1; + for (aggregate_index, aggregate) in plan.aggregate_bindings.iter().enumerate() { + if matches!(aggregate.kind, SimpleGroupedNumericAggregateKind::Sum) { + let source_column_index = aggregate.source_column_index.ok_or_else(|| { + DbError::internal("simple scalar SUM missing source column") + })?; + numeric_states[aggregate_index] + .add(&stored_row.values()[source_column_index])?; + } + } + } + + let values = plan + .aggregate_bindings + .iter() + .enumerate() + .map(|(aggregate_index, aggregate)| match aggregate.kind { + SimpleGroupedNumericAggregateKind::CountRows => Value::Int64(row_count), + SimpleGroupedNumericAggregateKind::Sum => { + numeric_states[aggregate_index].value(aggregate.kind) + } + _ => Value::Null, + }) + .collect::>(); + Ok(Some(QueryResult::with_rows( + plan.column_names.clone(), + vec![QueryRow::new(values)], + ))) + } + fn try_simple_scalar_filtered_numeric_aggregate_from_persisted_state( + &self, + store: &S, + state: PersistedTableState, + plan: &SimpleGroupedNumericAggregatePlan<'_>, + params: &[Value], + ) -> Result> { + if !plan.group_exprs.is_empty() + || plan.projection_exprs.is_some() + || plan.having.is_some() + || plan.order_by.is_some() + || plan.limit.is_some() + || plan.offset != 0 + { + return Ok(None); + } + let Some(filter_expr) = plan.filter_expr.as_ref() else { + return Ok(None); + }; + let Some((_filter_table, filter_column, filter_value_expr)) = + simple_btree_lookup(filter_expr) + else { + return Ok(None); + }; + let Some(table_schema) = self.table_schema(plan.table_name) else { + return Ok(None); + }; + let Some(filter_column_index) = schema_column_index(table_schema, filter_column) else { + return Ok(None); + }; + if plan.aggregate_bindings.iter().any(|aggregate| { + !matches!( + aggregate.kind, + SimpleGroupedNumericAggregateKind::CountRows + | SimpleGroupedNumericAggregateKind::Sum + ) || (matches!(aggregate.kind, SimpleGroupedNumericAggregateKind::Sum) + && aggregate.source_column_index.is_none()) + }) { + return Ok(None); + } + + let filter_value = self.eval_expr( + filter_value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?; + let mut row_count = 0_i64; + let mut numeric_states = vec![ + SimpleGroupedNumericState { + numeric_count: 0, + total_int: 0, + total_float: 0.0, + saw_float: false, + saw_value: false, + }; + plan.aggregate_bindings.len() + ]; + + let mut projection_indexes = Vec::with_capacity(plan.aggregate_bindings.len() + 1); + projection_indexes.push(filter_column_index); + let filter_projection_index = 0; + let mut aggregate_projection_indexes = Vec::with_capacity(plan.aggregate_bindings.len()); + for aggregate in &plan.aggregate_bindings { + let Some(source_column_index) = aggregate.source_column_index else { + aggregate_projection_indexes.push(None); + continue; + }; + let projection_index = + push_projection_index(&mut projection_indexes, source_column_index); + aggregate_projection_indexes.push(Some(projection_index)); + } + + visit_persisted_table_projected_values(store, state, &projection_indexes, |_, values| { + if compare_values(&values[filter_projection_index], &filter_value)? + != std::cmp::Ordering::Equal + { + return Ok(()); + } + row_count += 1; + for (aggregate_index, aggregate) in plan.aggregate_bindings.iter().enumerate() { + if matches!(aggregate.kind, SimpleGroupedNumericAggregateKind::Sum) { + let source_projection_index = aggregate_projection_indexes[aggregate_index] + .ok_or_else(|| { + DbError::internal("simple scalar SUM missing source column") + })?; + numeric_states[aggregate_index].add(&values[source_projection_index])?; + } + } + Ok(()) + })?; + + let values = plan + .aggregate_bindings + .iter() + .enumerate() + .map(|(aggregate_index, aggregate)| match aggregate.kind { + SimpleGroupedNumericAggregateKind::CountRows => Value::Int64(row_count), + SimpleGroupedNumericAggregateKind::Sum => { + numeric_states[aggregate_index].value(aggregate.kind) + } + _ => Value::Null, + }) + .collect::>(); + Ok(Some(QueryResult::with_rows( + plan.column_names.clone(), + vec![QueryRow::new(values)], + ))) + } + fn simple_grouped_numeric_aggregate_result_from_persisted_state( + &self, + store: &S, + state: PersistedTableState, + plan: &SimpleGroupedNumericAggregatePlan<'_>, + params: &[Value], + ) -> Result { + if let Some(result) = + self.try_simple_scalar_int64_numeric_aggregate_from_persisted_state(store, state, plan)? + { + return Ok(result); + } + if let Some(result) = self + .try_simple_scalar_filtered_numeric_aggregate_from_persisted_state( + store, state, plan, params, + )? + { + return Ok(result); + } + + let mut groups = Vec::::new(); + let mut group_positions = BTreeMap::, usize>::new(); + let group_dataset = Dataset::with_rows(plan.group_eval_bindings.clone(), Vec::new()); + if plan.group_exprs.is_empty() { + groups.push(SimpleGroupedNumericAggregate::new( + Vec::new(), + plan.aggregate_bindings.len(), + )); + group_positions.insert(row_identity(&[])?, 0); + } + visit_persisted_table_rows(store, state, |_, values| { + if let Some(filter_expr) = plan.filter_expr.as_ref() { + if !matches!( + self.eval_expr( + filter_expr, + &group_dataset, + values, + params, + &BTreeMap::new(), + None, + )?, + Value::Bool(true) + ) { + return Ok(()); + } + } + + let group_values = evaluate_simple_grouped_values( + self, + plan.group_exprs, + &group_dataset, + values, + params, + )?; + let group_key = row_identity(&group_values)?; + let group_index = if let Some(group_index) = group_positions.get(&group_key).copied() { + group_index + } else { + groups.push(SimpleGroupedNumericAggregate::new( + group_values, + plan.aggregate_bindings.len(), + )); + let group_index = groups.len() - 1; + group_positions.insert(group_key, group_index); + group_index + }; + groups[group_index].count += 1; + for (aggregate_index, aggregate) in plan.aggregate_bindings.iter().enumerate() { + match aggregate.kind { + SimpleGroupedNumericAggregateKind::CountNonNull => { + if let Some(source_column_index) = aggregate.source_column_index { + groups[group_index] + .count_non_null(aggregate_index, &values[source_column_index]); + } else if let Some(source_expr) = aggregate.source_expr.as_ref() { + let value = self.eval_expr( + source_expr, + &group_dataset, + values, + params, + &BTreeMap::new(), + None, + )?; + groups[group_index].count_non_null(aggregate_index, &value); + } + } + SimpleGroupedNumericAggregateKind::CountDistinct => { + if let Some(source_column_index) = aggregate.source_column_index { + groups[group_index] + .count_distinct(aggregate_index, &values[source_column_index])?; + } else if let Some(source_expr) = aggregate.source_expr.as_ref() { + let value = self.eval_expr( + source_expr, + &group_dataset, + values, + params, + &BTreeMap::new(), + None, + )?; + groups[group_index].count_distinct(aggregate_index, &value)?; + } + } + SimpleGroupedNumericAggregateKind::Sum + | SimpleGroupedNumericAggregateKind::SumDistinct + | SimpleGroupedNumericAggregateKind::Avg + | SimpleGroupedNumericAggregateKind::AvgDistinct + | SimpleGroupedNumericAggregateKind::Total + | SimpleGroupedNumericAggregateKind::TotalDistinct => { + if let Some(source_column_index) = aggregate.source_column_index { + if matches!( + aggregate.kind, + SimpleGroupedNumericAggregateKind::SumDistinct + | SimpleGroupedNumericAggregateKind::AvgDistinct + | SimpleGroupedNumericAggregateKind::TotalDistinct + ) { + groups[group_index].add_numeric_distinct( + aggregate_index, + &values[source_column_index], + )?; + } else { + groups[group_index] + .add_numeric(aggregate_index, &values[source_column_index])?; + } + } else if let Some(source_expr) = aggregate.source_expr.as_ref() { + let value = self.eval_expr( + source_expr, + &group_dataset, + values, + params, + &BTreeMap::new(), + None, + )?; + if matches!( + aggregate.kind, + SimpleGroupedNumericAggregateKind::SumDistinct + | SimpleGroupedNumericAggregateKind::AvgDistinct + | SimpleGroupedNumericAggregateKind::TotalDistinct + ) { + groups[group_index] + .add_numeric_distinct(aggregate_index, &value)?; + } else { + groups[group_index].add_numeric(aggregate_index, &value)?; + } + } + } + SimpleGroupedNumericAggregateKind::StddevSamp + | SimpleGroupedNumericAggregateKind::StddevSampDistinct + | SimpleGroupedNumericAggregateKind::StddevPop + | SimpleGroupedNumericAggregateKind::StddevPopDistinct + | SimpleGroupedNumericAggregateKind::VarSamp + | SimpleGroupedNumericAggregateKind::VarSampDistinct + | SimpleGroupedNumericAggregateKind::VarPop + | SimpleGroupedNumericAggregateKind::VarPopDistinct => { + if let Some(source_column_index) = aggregate.source_column_index { + if aggregate.kind.uses_distinct() { + groups[group_index].add_variance_distinct( + aggregate_index, + &values[source_column_index], + )?; + } else { + groups[group_index] + .add_variance(aggregate_index, &values[source_column_index])?; + } + } else if let Some(source_expr) = aggregate.source_expr.as_ref() { + let value = self.eval_expr( + source_expr, + &group_dataset, + values, + params, + &BTreeMap::new(), + None, + )?; + if aggregate.kind.uses_distinct() { + groups[group_index] + .add_variance_distinct(aggregate_index, &value)?; + } else { + groups[group_index].add_variance(aggregate_index, &value)?; + } + } + } + SimpleGroupedNumericAggregateKind::BoolAnd + | SimpleGroupedNumericAggregateKind::BoolAndDistinct + | SimpleGroupedNumericAggregateKind::BoolOr + | SimpleGroupedNumericAggregateKind::BoolOrDistinct => { + if let Some(source_column_index) = aggregate.source_column_index { + if aggregate.kind.uses_distinct() { + groups[group_index].add_bool_distinct( + aggregate_index, + &values[source_column_index], + )?; + } else { + groups[group_index] + .add_bool(aggregate_index, &values[source_column_index])?; + } + } else if let Some(source_expr) = aggregate.source_expr.as_ref() { + let value = self.eval_expr( + source_expr, + &group_dataset, + values, + params, + &BTreeMap::new(), + None, + )?; + if aggregate.kind.uses_distinct() { + groups[group_index].add_bool_distinct(aggregate_index, &value)?; + } else { + groups[group_index].add_bool(aggregate_index, &value)?; + } + } + } + SimpleGroupedNumericAggregateKind::Min + | SimpleGroupedNumericAggregateKind::Max => { + let Some(source_expr) = aggregate.source_expr.as_ref() else { + continue; + }; + let value = self.eval_expr( + source_expr, + &group_dataset, + values, + params, + &BTreeMap::new(), + None, + )?; + update_simple_min_max_value( + &mut groups[group_index].extreme_values[aggregate_index], + value, + matches!(aggregate.kind, SimpleGroupedNumericAggregateKind::Max), + )?; + } + SimpleGroupedNumericAggregateKind::CountRows => {} + } + } + Ok(()) + })?; + + render_simple_grouped_numeric_aggregate_groups(self, groups, plan, params) + } + fn try_simple_scalar_int64_numeric_aggregate_from_persisted_state( + &self, + store: &S, + state: PersistedTableState, + plan: &SimpleGroupedNumericAggregatePlan<'_>, + ) -> Result> { + let Some(column_index) = self.simple_scalar_int64_aggregate_column(plan) else { + return Ok(None); + }; + let mut stats = SimpleScalarInt64AggregateStats::default(); + visit_persisted_table_int64_column(store, state, column_index, |_, value| { + stats.add(value); + Ok(()) + })?; + Ok(Some(QueryResult::with_rows( + plan.column_names.clone(), + vec![QueryRow::new(stats.into_values(&plan.aggregate_bindings))], + ))) + } + fn try_execute_simple_deferred_paged_grouped_count_query( + &self, + query: &Query, + params: &[Value], + pager: &PagerHandle, + wal: &WalHandle, + snapshot_lsn: u64, + ) -> Result> { + let Some(plan) = self.analyze_simple_grouped_count_query(query, params)? else { + return Ok(None); + }; + if self.visible_table_is_temporary(plan.table_name) + || self.visible_table_row_source(plan.table_name).is_some() + { + return Ok(None); + } + if let Some(result) = + self.try_simple_grouped_count_result_from_runtime_index(None, &plan, params)? + { + return Ok(Some(result)); + } + let Some(state) = self.persisted_table_state(plan.table_name) else { + return Ok(None); + }; + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + Ok(Some( + self.simple_grouped_count_result_from_persisted_state(&store, state, &plan, params)?, + )) + } + pub(crate) fn try_execute_simple_deferred_paged_grouped_numeric_aggregate_query( + &self, + query: &Query, + params: &[Value], + pager: &PagerHandle, + wal: &WalHandle, + snapshot_lsn: u64, + ) -> Result> { + let Some(plan) = self.analyze_simple_grouped_numeric_aggregate_query(query, params)? else { + return Ok(None); + }; + if self.visible_table_is_temporary(plan.table_name) + || self.visible_table_row_source(plan.table_name).is_some() + { + return Ok(None); + } + let Some(state) = self.persisted_table_state(plan.table_name) else { + return Ok(None); + }; + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + Ok(Some( + self.simple_grouped_numeric_aggregate_result_from_persisted_state( + &store, state, &plan, params, + )?, + )) + } + pub(crate) fn try_execute_simple_view_projection_limit_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + if query.recursive || !query.ctes.is_empty() { + return Ok(None); + } + let Some(limit_expr) = query.limit.as_ref() else { + return Ok(None); + }; + let ctes = BTreeMap::new(); + let limit_value = match simple_int64_constant_expr_value(limit_expr, params)? { + Some(value) => value, + None => self.eval_constant_i64(limit_expr, params, &ctes)?, + }; + let limit = usize::try_from(limit_value.max(0)).unwrap_or(usize::MAX); + let offset = query + .offset + .as_ref() + .map(|expr| { + simple_int64_constant_expr_value(expr, params)? + .map(Ok) + .unwrap_or_else(|| self.eval_constant_i64(expr, params, &ctes)) + }) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || select.filter.is_some() + || !select.group_by.is_empty() + || select.having.is_some() + || projection_has_aggregate_items(&select.projection) + || select.from.len() != 1 + { + return Ok(None); + } + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + let Some(view) = self.visible_view(name, NameResolutionScope::Session) else { + return Ok(None); + }; + let view_binding = alias.as_deref().unwrap_or(name.as_str()); + let view_query = self.cached_view_query(view)?; + if view_query.recursive + || !view_query.ctes.is_empty() + || !view_query.order_by.is_empty() + || view_query.limit.is_some() + || view_query.offset.is_some() + { + return Ok(None); + } + let QueryBody::Select(view_select) = &view_query.body else { + return Ok(None); + }; + if view_select.distinct + || !view_select.distinct_on.is_empty() + || !view_select.group_by.is_empty() + || view_select.having.is_some() + || projection_has_aggregate_items(&view_select.projection) + { + return Ok(None); + } + if !query.order_by.is_empty() { + return if view.temporary { + self.try_execute_ordered_view_projection_limit_select( + select, + view_select, + &view.name, + &view.column_names, + view_binding, + &query.order_by, + limit, + offset, + params, + ) + } else { + let persistent_runtime = self.persistent_resolution_runtime(); + persistent_runtime.try_execute_ordered_view_projection_limit_select( + select, + view_select, + &view.name, + &view.column_names, + view_binding, + &query.order_by, + limit, + offset, + params, + ) + }; + } + if view_select.filter.is_some() { + return Ok(None); + } + + let mut pushed_projection = Vec::with_capacity(select.projection.len()); + for (index, item) in select.projection.iter().enumerate() { + let SelectItem::Expr { expr, alias } = item else { + return Ok(None); + }; + let Expr::Column { table, column } = expr else { + return Ok(None); + }; + if table + .as_deref() + .is_some_and(|qualifier| !identifiers_equal(qualifier, view_binding)) + { + return Ok(None); + } + let Some(view_expr) = + view_projection_expr_for_output_column(&view_select.projection, column) + else { + return Ok(None); + }; + pushed_projection.push(SelectItem::Expr { + expr: view_expr, + alias: Some( + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, index + 1)), + ), + }); + } + + if view.temporary { + self.try_execute_indexed_join_limit_projection_select( + view_select, + &pushed_projection, + limit, + offset, + ) + } else { + let persistent_runtime = self.persistent_resolution_runtime(); + persistent_runtime.try_execute_indexed_join_limit_projection_select( + view_select, + &pushed_projection, + limit, + offset, + ) + } + } + #[allow(clippy::too_many_arguments)] + fn try_execute_ordered_view_projection_limit_select( + &self, + outer_select: &Select, + view_select: &Select, + view_name: &str, + view_column_names: &[String], + view_binding: &str, + order_by: &[OrderBy], + limit: usize, + offset: usize, + params: &[Value], + ) -> Result> { + if order_by.len() != 1 || order_by[0].collation.is_some() { + return Ok(None); + } + let Some(pushed_projection) = pushed_view_projection_for_outer_projection( + &outer_select.projection, + view_select, + view_name, + view_binding, + view_column_names, + ) else { + return Ok(None); + }; + + let mut join_select = view_select.clone(); + join_select.filter = None; + let Some(plan) = self.analyze_indexed_join_limit_projection_select( + &join_select, + &pushed_projection, + limit, + offset, + )? + else { + return Ok(None); + }; + + let Expr::Column { + table: order_table, + column: order_column, + } = &order_by[0].expr + else { + return Ok(None); + }; + if order_table.as_deref().is_some_and(|qualifier| { + !identifiers_equal(qualifier, view_binding) && !identifiers_equal(qualifier, view_name) + }) { + return Ok(None); + } + let Some(order_expr) = view_projection_expr_for_output_column_with_names( + &view_select.projection, + view_column_names, + order_column, + ) else { + return Ok(None); + }; + let Some((order_table_index, order_column_index)) = + indexed_join_limit_projection_column(&order_expr, &plan.tables, self) + else { + return Ok(None); + }; + if order_table_index != 0 { + return Ok(None); + } + + let root_table = plan.tables[0]; + let root_binding = root_table.alias.as_deref().unwrap_or(root_table.name); + let Some(root_schema) = self.table_schema(root_table.name) else { + return Ok(None); + }; + let Some(order_column_schema) = root_schema.columns.get(order_column_index) else { + return Ok(None); + }; + + let root_filter_columns = if let Some(filter) = view_select.filter.as_ref() { + let Some(root_columns) = indexed_join_table_eval_columns(&plan.tables[..1], self) + else { + return Ok(None); + }; + let Some(join_columns) = indexed_join_table_eval_columns(&plan.tables, self) else { + return Ok(None); + }; + let root_dataset = Dataset::with_rows(root_columns.clone(), Vec::new()); + let join_dataset = Dataset::with_rows(join_columns, Vec::new()); + if !expr_resolves_against_dataset(filter, &root_dataset) + || !expr_resolves_against_dataset(filter, &join_dataset) + { + return Ok(None); + } + Some(root_columns) + } else { + None + }; + + let Some(index) = self.ordered_view_root_btree_index( + root_table.name, + &order_column_schema.name, + view_select.filter.as_ref(), + root_binding, + )? + else { + return Ok(None); + }; + + self.execute_ordered_indexed_join_limit_projection_plan( + &plan, + view_select.filter.as_ref(), + root_filter_columns, + &index.name, + order_by[0].descending, + params, + ) + .map(Some) + } + pub(crate) fn try_execute_simple_indexed_join_projection_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + if !query.ctes.is_empty() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if !select.group_by.is_empty() + || select.having.is_some() + || projection_has_aggregate_items(&select.projection) + || select.from.len() != 1 + { + return Ok(None); + } + if !select.distinct_on.is_empty() { + return Ok(None); + } + let FromItem::Join { + left, + right, + kind, + constraint, + } = &select.from[0] + else { + return Ok(None); + }; + if !matches!( + kind, + JoinKind::Inner | JoinKind::Left | JoinKind::Right | JoinKind::Full + ) { + return Ok(None); + } + let is_left_outer = matches!(kind, JoinKind::Left | JoinKind::Full); + let is_right_outer = matches!(kind, JoinKind::Right | JoinKind::Full); + let (left_name, left_alias) = match &**left { + FromItem::Table { name, alias } => (name, alias), + _ => return Ok(None), + }; + let (right_name, right_alias) = match &**right { + FromItem::Table { name, alias } => (name, alias), + _ => return Ok(None), + }; + if self + .visible_view(left_name, NameResolutionScope::Session) + .is_some() + || self + .visible_view(right_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(left_name) + || self.visible_table_is_temporary(right_name) + { + return Ok(None); + } + + let left_binding = TableBindingRef { + name: left_name, + alias: left_alias, + }; + let right_binding = TableBindingRef { + name: right_name, + alias: right_alias, + }; + let left_schema = match self.table_schema(left_name) { + Some(table) => table, + None => return Ok(None), + }; + let right_schema = match self.table_schema(right_name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(left_schema) || !generated_columns_are_stored(right_schema) + { + return Ok(None); + } + let Some(join_equalities) = simple_indexed_join_constraint_equalities( + constraint, + left_binding, + right_binding, + left_schema, + right_schema, + ) else { + return Ok(None); + }; + let Some((left_join_columns, right_join_columns)) = + orient_join_equalities(&join_equalities, left_binding, right_binding) + else { + return Ok(None); + }; + let using_join_columns = + simple_indexed_join_using_columns(constraint, left_schema, right_schema); + + let left_source = match self.visible_table_row_source(left_name) { + Some(source) => source, + None => return Ok(None), + }; + let right_source = match self.visible_table_row_source(right_name) { + Some(source) => source, + None => return Ok(None), + }; + let join_eval_bindings = simple_join_projection_eval_bindings( + left_name, + left_alias, + left_schema, + right_name, + right_alias, + right_schema, + ); + let join_eval_dataset = Dataset::with_rows(join_eval_bindings, Vec::new()); + let default_source_is_left = if matches!(kind, JoinKind::Full) { + true + } else { + !is_right_outer + }; + let (source_is_left, source_filter, post_join_filter) = if let Some(filter) = + select.filter.as_ref() + { + if let Some((filter_table, filter_column, value_expr)) = simple_btree_lookup(filter) { + if matches_table_binding(left_binding, filter_table) && !is_right_outer { + (true, Some((filter_column, value_expr)), None) + } else if !is_left_outer && matches_table_binding(right_binding, filter_table) { + (false, Some((filter_column, value_expr)), None) + } else if !expr_contains_recursive_unsupported_feature(filter) + && expr_resolves_against_dataset(filter, &join_eval_dataset) + { + (default_source_is_left, None, Some(filter.clone())) + } else { + return Ok(None); + } + } else if !expr_contains_recursive_unsupported_feature(filter) + && expr_resolves_against_dataset(filter, &join_eval_dataset) + { + (default_source_is_left, None, Some(filter.clone())) + } else { + return Ok(None); + } + } else { + (default_source_is_left, None, None) + }; + let Some((projection_plan, column_names)) = simple_join_projection_plan( + &select.projection, + &join_eval_dataset, + left_name, + left_alias, + left_schema, + right_name, + right_alias, + right_schema, + &using_join_columns, + ) else { + return Ok(None); + }; + let order_by = simple_join_projection_order_by_plan( + query, + &select.projection, + &projection_plan, + &column_names, + left_name, + left_alias, + left_schema, + right_name, + right_alias, + right_schema, + )?; + + let ( + source_table, + source_schema, + source_join_columns, + source_source, + probe_table, + probe_schema, + probe_join_columns, + probe_source, + ) = if source_is_left { + ( + left_name, + left_schema, + left_join_columns, + left_source, + right_name, + right_schema, + right_join_columns, + right_source, + ) + } else { + ( + right_name, + right_schema, + right_join_columns, + right_source, + left_name, + left_schema, + left_join_columns, + left_source, + ) + }; + let mut source_join_indexes = Vec::with_capacity(source_join_columns.len()); + for source_join_column in &source_join_columns { + let source_join_index = source_schema + .columns + .iter() + .position(|column| identifiers_equal(&column.name, source_join_column)) + .ok_or_else(|| DbError::sql(format!("unknown column {source_join_column}")))?; + source_join_indexes.push(source_join_index); + } + let source_row_ids = if let Some((filter_column, value_expr)) = source_filter { + let filter_value = self.eval_expr( + value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?; + if crate::exec::dml::row_id_alias_column_name(source_schema) + .is_some_and(|name| identifiers_equal(name, filter_column)) + { + Some(match filter_value { + Value::Int64(row_id) => RuntimeRowIdSet::Single(row_id), + _ => RuntimeRowIdSet::Empty, + }) + } else { + let Some(filter_index) = self.catalog.indexes.values().find(|index| { + identifiers_equal(&index.table_name, source_table) + && index.fresh + && index.kind == IndexKind::Btree + && index.predicate_sql.is_none() + && index.columns.len() == 1 + && index.columns[0] + .column_name + .as_deref() + .is_some_and(|index_column| { + identifiers_equal(index_column, filter_column) + }) + && index.columns[0].expression_sql.is_none() + }) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&filter_index.name) else { + return Ok(None); + }; + Some(keys.row_ids_for_value_set(&filter_value)?) + } + } else { + None + }; + + let is_probe_rowid_alias = probe_join_columns.len() == 1 + && crate::exec::dml::row_id_alias_column_name(probe_schema) + .is_some_and(|name| identifiers_equal(name, probe_join_columns[0])); + let (probe_index, ordered_source_join_indexes) = if is_probe_rowid_alias { + (None, source_join_indexes) + } else { + match self.catalog.indexes.values().find_map(|index| { + if !identifiers_equal(&index.table_name, probe_table) + || !index.fresh + || index.kind != IndexKind::Btree + || index.predicate_sql.is_some() + || index.columns.len() != probe_join_columns.len() + { + return None; + } + let mut ordered_source_join_indexes = Vec::with_capacity(index.columns.len()); + for index_column in &index.columns { + if index_column.expression_sql.is_some() { + return None; + } + let index_column_name = index_column.column_name.as_deref()?; + let join_position = probe_join_columns.iter().position(|join_column| { + identifiers_equal(join_column, index_column_name) + })?; + ordered_source_join_indexes.push(source_join_indexes[join_position]); + } + Some((index, ordered_source_join_indexes)) + }) { + Some((probe_index, ordered_source_join_indexes)) => { + (Some(probe_index), ordered_source_join_indexes) + } + None => (None, source_join_indexes), + } + }; + let keys = if let Some(index) = probe_index { + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { + return Ok(None); + }; + Some(keys) + } else { + None + }; + let probe_join_indexes = if probe_index.is_none() && !is_probe_rowid_alias { + let mut probe_join_indexes = Vec::with_capacity(probe_join_columns.len()); + for probe_join_column in &probe_join_columns { + let Some(probe_join_index) = probe_schema + .columns + .iter() + .position(|column| identifiers_equal(&column.name, probe_join_column)) + else { + return Ok(None); + }; + probe_join_indexes.push(probe_join_index); + } + Some(probe_join_indexes) + } else { + None + }; + + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let early_stop_limit = if order_by.is_none() && !select.distinct { + limit.map(|limit| limit.saturating_add(offset)) + } else { + None + }; + + let use_probe_row_position_map = probe_source + .row_count() + .saturating_mul(source_source.row_count()) + > 8_192; + let probe_row_positions = if use_probe_row_position_map { + let mut positions = Int64Map::::default(); + for (position, row) in probe_source.rows().enumerate() { + positions.insert(row?.row_id(), position); + } + Some(positions) + } else { + None + }; + let probe_hash_rows = if let Some(probe_join_indexes) = probe_join_indexes.as_ref() { + let mut hashed = SimpleJoinHashRows::new(); + for probe_row in probe_source.rows() { + let probe_row = probe_row?; + let Some(join_key) = + simple_join_key_from_indexes(probe_row.values(), probe_join_indexes)? + else { + continue; + }; + hashed + .entry(join_key) + .or_default() + .push((probe_row.row_id(), probe_row.values().to_vec())); + } + Some(hashed) + } else { + None + }; + + let mut rows = Vec::new(); + let mut stop = false; + let left_width = left_schema.columns.len(); + let right_width = right_schema.columns.len(); + let mut matched_probe_row_ids = + matches!(kind, JoinKind::Full).then(Int64Map::<()>::default); + if let Some(source_row_ids) = source_row_ids { + match source_row_ids { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(source_row_id) => { + if let Some(source_row) = source_source.row_by_id(source_row_id)? { + let _ = self.process_simple_indexed_join_source_row( + *kind, + source_is_left, + source_row.values(), + &ordered_source_join_indexes, + probe_source, + probe_row_positions.as_ref(), + keys, + probe_hash_rows.as_ref(), + matched_probe_row_ids.as_mut(), + post_join_filter.as_ref(), + &projection_plan, + &join_eval_dataset, + left_width, + right_width, + params, + early_stop_limit, + &mut rows, + )?; + } + } + RuntimeRowIdSet::Contiguous { start, len } => { + for source_row_id in contiguous_row_ids(start, len) { + if stop { + break; + } + let Some(source_row) = source_source.row_by_id(source_row_id)? else { + continue; + }; + stop = self.process_simple_indexed_join_source_row( + *kind, + source_is_left, + source_row.values(), + &ordered_source_join_indexes, + probe_source, + probe_row_positions.as_ref(), + keys, + probe_hash_rows.as_ref(), + matched_probe_row_ids.as_mut(), + post_join_filter.as_ref(), + &projection_plan, + &join_eval_dataset, + left_width, + right_width, + params, + early_stop_limit, + &mut rows, + )?; + } + } + RuntimeRowIdSet::Many(source_row_ids) => { + for source_row_id in source_row_ids { + if stop { + break; + } + let Some(source_row) = source_source.row_by_id(*source_row_id)? else { + continue; + }; + stop = self.process_simple_indexed_join_source_row( + *kind, + source_is_left, + source_row.values(), + &ordered_source_join_indexes, + probe_source, + probe_row_positions.as_ref(), + keys, + probe_hash_rows.as_ref(), + matched_probe_row_ids.as_mut(), + post_join_filter.as_ref(), + &projection_plan, + &join_eval_dataset, + left_width, + right_width, + params, + early_stop_limit, + &mut rows, + )?; + } + } + RuntimeRowIdSet::Owned(source_row_ids) => { + for source_row_id in source_row_ids { + if stop { + break; + } + let Some(source_row) = source_source.row_by_id(source_row_id)? else { + continue; + }; + stop = self.process_simple_indexed_join_source_row( + *kind, + source_is_left, + source_row.values(), + &ordered_source_join_indexes, + probe_source, + probe_row_positions.as_ref(), + keys, + probe_hash_rows.as_ref(), + matched_probe_row_ids.as_mut(), + post_join_filter.as_ref(), + &projection_plan, + &join_eval_dataset, + left_width, + right_width, + params, + early_stop_limit, + &mut rows, + )?; + } + } + } + } else { + for source_row in source_source.rows() { + let source_row = source_row?; + stop = self.process_simple_indexed_join_source_row( + *kind, + source_is_left, + source_row.values(), + &ordered_source_join_indexes, + probe_source, + probe_row_positions.as_ref(), + keys, + probe_hash_rows.as_ref(), + matched_probe_row_ids.as_mut(), + post_join_filter.as_ref(), + &projection_plan, + &join_eval_dataset, + left_width, + right_width, + params, + early_stop_limit, + &mut rows, + )?; + if stop { + break; + } + } + } + if matches!(kind, JoinKind::Full) && source_is_left && !stop { + if let Some(matched_probe_row_ids) = matched_probe_row_ids.as_ref() { + for probe_row in probe_source.rows() { + let probe_row = probe_row?; + if matched_probe_row_ids.contains_key(&probe_row.row_id()) { + continue; + } + if !simple_join_filter_matches( + self, + post_join_filter.as_ref(), + &join_eval_dataset, + None, + left_width, + Some(probe_row.values()), + right_width, + params, + )? { + continue; + } + rows.push(project_simple_join_row( + self, + &projection_plan, + &join_eval_dataset, + None, + left_width, + Some(probe_row.values()), + right_width, + params, + )?); + if early_stop_limit.is_some_and(|limit| rows.len() >= limit) { + break; + } + } + } + } + let mut rows = rows.into_iter().map(QueryRow::new).collect::>(); + if select.distinct { + rows = dedup_query_rows(rows)?; + } + Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + column_names, + order_by.as_deref(), + limit, + offset, + )?)) + } + #[allow(clippy::too_many_arguments)] + fn process_simple_indexed_join_source_row( + &self, + kind: JoinKind, + source_is_left: bool, + source_values: &[Value], + source_join_indexes: &[usize], + probe_source: VisibleTableRowSource<'_>, + probe_row_positions: Option<&Int64Map>, + probe_keys: Option<&RuntimeBtreeKeys>, + probe_hash_rows: Option<&SimpleJoinHashRows>, + mut matched_probe_row_ids: Option<&mut Int64Map<()>>, + post_join_filter: Option<&Expr>, + projection_plan: &[SimpleJoinProjectionSource], + join_eval_dataset: &Dataset, + left_width: usize, + right_width: usize, + params: &[Value], + early_stop_limit: Option, + rows: &mut Vec>, + ) -> Result { + let join_values = source_join_indexes + .iter() + .map(|source_join_index| { + source_values + .get(*source_join_index) + .ok_or_else(|| DbError::internal("join row is shorter than table schema")) + }) + .collect::>>()?; + if join_values + .iter() + .any(|join_value| matches!(join_value, Value::Null)) + { + if matches!(kind, JoinKind::Inner) { + return Ok(false); + } + if !simple_join_filter_matches( + self, + post_join_filter, + join_eval_dataset, + if source_is_left { + Some(source_values) + } else { + None + }, + left_width, + if source_is_left { + None + } else { + Some(source_values) + }, + right_width, + params, + )? { + return Ok(false); + } + rows.push(project_simple_join_row( + self, + projection_plan, + join_eval_dataset, + if source_is_left { + Some(source_values) + } else { + None + }, + left_width, + if source_is_left { + None + } else { + Some(source_values) + }, + right_width, + params, + )?); + return Ok(early_stop_limit.is_some_and(|limit| rows.len() >= limit)); + } + + let join_key = Row::new(join_values.iter().cloned().cloned().collect()).encode()?; + let probe_row_ids = if let Some(keys) = probe_keys { + if join_values.len() == 1 { + keys.row_ids_for_value_set(join_values[0])? + } else { + keys.row_id_set_for_key(&RuntimeBtreeKey::Encoded(RuntimeEncodedKey::from_vec( + Row::new(join_values.into_iter().cloned().collect()).encode()?, + ))) + } + } else if join_values.len() == 1 { + let join_value = join_values[0]; + if let Value::Int64(val) = join_value { + RuntimeRowIdSet::Single(*val) + } else { + RuntimeRowIdSet::Empty + } + } else { + RuntimeRowIdSet::Empty + }; + let rows_before = rows.len(); + let mut should_stop = false; + let mut row_error = None; + + if let Some(probe_hash_rows) = probe_hash_rows { + if let Some(matching_probe_rows) = probe_hash_rows.get(&join_key) { + for (row_id, probe_row) in matching_probe_rows { + match simple_join_filter_matches( + self, + post_join_filter, + join_eval_dataset, + if source_is_left { + Some(source_values) + } else { + Some(probe_row.as_slice()) + }, + left_width, + if source_is_left { + Some(probe_row.as_slice()) + } else { + Some(source_values) + }, + right_width, + params, + ) { + Ok(true) => {} + Ok(false) => continue, + Err(error) => { + row_error = Some(error); + break; + } + } + match project_simple_join_row( + self, + projection_plan, + join_eval_dataset, + if source_is_left { + Some(source_values) + } else { + Some(probe_row.as_slice()) + }, + left_width, + if source_is_left { + Some(probe_row.as_slice()) + } else { + Some(source_values) + }, + right_width, + params, + ) { + Ok(projected) => { + if let Some(matched_probe_row_ids) = matched_probe_row_ids.as_mut() { + matched_probe_row_ids.insert(*row_id, ()); + } + rows.push(projected); + } + Err(error) => { + row_error = Some(error); + break; + } + } + if early_stop_limit.is_some_and(|limit| rows.len() >= limit) { + should_stop = true; + break; + } + } + } + if let Some(error) = row_error { + return Err(error); + } + if !should_stop && !matches!(kind, JoinKind::Inner) && rows.len() == rows_before { + if !simple_join_filter_matches( + self, + post_join_filter, + join_eval_dataset, + if source_is_left { + Some(source_values) + } else { + None + }, + left_width, + if source_is_left { + None + } else { + Some(source_values) + }, + right_width, + params, + )? { + return Ok(false); + } + rows.push(project_simple_join_row( + self, + projection_plan, + join_eval_dataset, + if source_is_left { + Some(source_values) + } else { + None + }, + left_width, + if source_is_left { + None + } else { + Some(source_values) + }, + right_width, + params, + )?); + should_stop = early_stop_limit.is_some_and(|limit| rows.len() >= limit); + } + return Ok(should_stop); + } + + probe_row_ids.for_each(|row_id| { + if should_stop || row_error.is_some() { + return; + } + let probe_row = if let Some(positions) = probe_row_positions { + let Some(probe_position) = positions.get(&row_id).copied() else { + return; + }; + match probe_source.row_at_position(probe_position) { + Ok(Some(probe_row)) => probe_row.values().to_vec(), + Ok(None) => return, + Err(error) => { + row_error = Some(error); + return; + } + } + } else { + match probe_source.row_by_id(row_id) { + Ok(Some(probe_row)) => probe_row.values().to_vec(), + Ok(None) => return, + Err(error) => { + row_error = Some(error); + return; + } + } + }; + + match simple_join_filter_matches( + self, + post_join_filter, + join_eval_dataset, + if source_is_left { + Some(source_values) + } else { + Some(&probe_row) + }, + left_width, + if source_is_left { + Some(&probe_row) + } else { + Some(source_values) + }, + right_width, + params, + ) { + Ok(true) => {} + Ok(false) => return, + Err(error) => { + row_error = Some(error); + return; + } + } + match project_simple_join_row( + self, + projection_plan, + join_eval_dataset, + if source_is_left { + Some(source_values) + } else { + Some(&probe_row) + }, + left_width, + if source_is_left { + Some(&probe_row) + } else { + Some(source_values) + }, + right_width, + params, + ) { + Ok(projected) => { + if let Some(matched_probe_row_ids) = matched_probe_row_ids.as_mut() { + matched_probe_row_ids.insert(row_id, ()); + } + rows.push(projected) + } + Err(error) => { + row_error = Some(error); + return; + } + } + if early_stop_limit.is_some_and(|limit| rows.len() >= limit) { + should_stop = true; + } + }); + if let Some(error) = row_error { + return Err(error); + } + if !should_stop && !matches!(kind, JoinKind::Inner) && rows.len() == rows_before { + if !simple_join_filter_matches( + self, + post_join_filter, + join_eval_dataset, + if source_is_left { + Some(source_values) + } else { + None + }, + left_width, + if source_is_left { + None + } else { + Some(source_values) + }, + right_width, + params, + )? { + return Ok(false); + } + rows.push(project_simple_join_row( + self, + projection_plan, + join_eval_dataset, + if source_is_left { + Some(source_values) + } else { + None + }, + left_width, + if source_is_left { + None + } else { + Some(source_values) + }, + right_width, + params, + )?); + should_stop = early_stop_limit.is_some_and(|limit| rows.len() >= limit); + } + Ok(should_stop) + } + pub(crate) fn single_column_btree_keys( + &self, + table_name: &str, + column_name: &str, + ) -> Option<&RuntimeBtreeKeys> { + let index = self.catalog.indexes.values().find(|index| { + identifiers_equal(&index.table_name, table_name) + && index.fresh + && index.kind == IndexKind::Btree + && index.predicate_sql.is_none() + && index.columns.len() == 1 + && index.columns[0] + .column_name + .as_deref() + .is_some_and(|index_column| identifiers_equal(index_column, column_name)) + && index.columns[0].expression_sql.is_none() + })?; + let RuntimeIndex::Btree { keys, .. } = self.index(&index.name)? else { + return None; + }; + Some(keys) + } + pub(crate) fn try_execute_simple_table_projection_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + if !query.ctes.is_empty() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.filter.is_some() + || !select.group_by.is_empty() + || select.having.is_some() + || select.distinct + || select.from.len() != 1 + { + return Ok(None); + } + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + { + return Ok(None); + } + + let table_schema = match self.table_schema(name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + let Some((projection_indexes, column_names)) = + self.simple_projection_plan(select, name, alias, table_schema) + else { + return Ok(None); + }; + let order_by = self.simple_projection_order_by_plan( + query, + table_schema, + name, + alias.as_deref().unwrap_or(name), + &projection_indexes, + )?; + let row_id_order = if query.order_by.len() == 1 { + if let Expr::Column { + table: order_table, + column: order_column, + } = &query.order_by[0].expr + { + if order_table.as_deref().is_some_and(|qualifier| { + !matches_table_binding(TableBindingRef { name, alias }, Some(qualifier)) + }) { + None + } else if let Some(filter_column_index) = + schema_column_index(table_schema, order_column) + { + if table_schema + .primary_key_columns + .iter() + .any(|column| identifiers_equal(column, order_column)) + && table_schema.columns[filter_column_index].column_type + == crate::catalog::ColumnType::Int64 + { + Some((order_column.as_str(), query.order_by[0].descending)) + } else { + None + } + } else { + None + } + } else { + None + } + } else { + None + }; + + if !query.order_by.is_empty() && order_by.is_none() && row_id_order.is_none() { + return Ok(None); + } + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let _row_source = self.visible_table_row_source(name); + let Some(row_source) = _row_source else { + return Ok(None); + }; + if let Some((filter_column, descending)) = row_id_order { + if limit != Some(0) { + if let Some(row_ids) = self.ordered_runtime_btree_row_ids( + name, + filter_column, + limit, + offset, + descending, + )? { + let mut rows = Vec::with_capacity(row_ids.len().min(64)); + for row_id in row_ids { + if let Some(row) = + row_source.projected_query_row_by_id(row_id, &projection_indexes)? + { + rows.push(row); + } + } + return Ok(Some(QueryResult::with_rows(column_names, rows))); + } + let mut ordered_row_ids = Vec::with_capacity(row_source.row_count()); + for stored_row in row_source.rows() { + ordered_row_ids.push(stored_row?.row_id()); + } + ordered_row_ids.sort_unstable(); + if descending { + ordered_row_ids.reverse(); + } + let take = limit.unwrap_or(usize::MAX); + let mut rows = Vec::with_capacity(take.min(ordered_row_ids.len())); + for row_id in ordered_row_ids.into_iter().skip(offset).take(take) { + if let Some(row) = + row_source.projected_query_row_by_id(row_id, &projection_indexes)? + { + rows.push(row); + } + } + return Ok(Some(QueryResult::with_rows(column_names, rows))); + } + } + + Ok(Some(self.simple_projection_result_from_source( + row_source, + &projection_indexes, + column_names, + order_by, + limit, + offset, + )?)) + } + pub(crate) fn try_execute_simple_filtered_projection_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + if !query.ctes.is_empty() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if !select.group_by.is_empty() + || select.having.is_some() + || !select.distinct_on.is_empty() + || select.from.len() != 1 + { + return Ok(None); + } + if select_requires_grouped_evaluation(self, select)? { + return Ok(None); + } + if select.distinct + && (!query.order_by.is_empty() || query.limit.is_some() || query.offset.is_some()) + { + return Ok(None); + } + let Some(filter) = select.filter.as_ref() else { + return Ok(None); + }; + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + { + return Ok(None); + } + + let table_schema = match self.table_schema(name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + let Some((projection_indexes, column_names)) = + self.simple_projection_plan(select, name, alias, table_schema) + else { + return Ok(None); + }; + let binding_name = alias.as_deref().unwrap_or(name); + let order_by = self.simple_projection_order_by_plan( + query, + table_schema, + name, + binding_name, + &projection_indexes, + )?; + let order_by_requires_rowid_range = !query.order_by.is_empty() && order_by.is_none(); + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let _row_source = self.visible_table_row_source(name); + if !select.distinct { + if let Some(row_source) = _row_source { + if !order_by_requires_rowid_range { + if let Some(result) = self.try_simple_filtered_projection_exact_index_result( + row_source, + name, + table_schema, + filter, + &projection_indexes, + column_names.clone(), + order_by.as_deref(), + params, + limit, + offset, + )? { + return Ok(Some(result)); + } + } + } + } + let Some(row_source) = _row_source else { + return Ok(None); + }; + + if !select.distinct + && query.order_by.is_empty() + && limit.is_none() + && offset == 0 + && residual_like_filter_can_use_direct_scan(filter) + { + if let Some((filter_table, filter_column, literal)) = + simple_contains_like_projection_filter(filter) + { + if filter_table.is_none_or(|table_name| { + identifiers_equal(table_name, name) + || identifiers_equal(table_name, binding_name) + }) { + let filter_column_index = table_schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, filter_column)) + .ok_or_else(|| { + DbError::internal(format!( + "simple LIKE projection column {filter_column} missing from {name}" + )) + })?; + return Ok(Some( + self.simple_contains_like_projection_result_from_source( + row_source, + filter_column_index, + literal, + &projection_indexes, + column_names, + )?, + )); + } + } + } + + let Some(range_filter) = simple_range_projection_filter(filter) else { + return Ok(None); + }; + let filter_table = range_filter.table; + let filter_column = range_filter.column; + let lower_bound = range_filter.lower; + let upper_bound = range_filter.upper; + if let Some(table_name) = filter_table { + if !identifiers_equal(table_name, name) && !identifiers_equal(table_name, binding_name) + { + return Ok(None); + } + } + let filter_column_index = table_schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, filter_column)) + .ok_or_else(|| { + DbError::internal(format!( + "simple filtered projection column {filter_column} missing from {name}" + )) + })?; + + let lower_bound = lower_bound + .map(|bound| { + Ok(SimpleRangeBoundValue { + inclusive: bound.inclusive, + value: self.eval_expr( + bound.value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?, + }) + }) + .transpose()?; + let upper_bound = upper_bound + .map(|bound| { + Ok(SimpleRangeBoundValue { + inclusive: bound.inclusive, + value: self.eval_expr( + bound.value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?, + }) + }) + .transpose()?; + + let residual_plans = self.build_simple_residual_plans( + table_schema, + name, + binding_name, + &range_filter.residual, + params, + )?; + // If a residual predicate references an unknown/external table the + // builder silently stops; bail to the generic executor in that case. + if residual_plans.len() != range_filter.residual.len() { + return Ok(None); + } + if !select.distinct && residual_plans.is_empty() { + if let Some(result) = self.try_simple_rowid_range_projection_result( + row_source, + table_schema, + TableBindingRef { name, alias }, + filter_column, + lower_bound.as_ref(), + upper_bound.as_ref(), + &projection_indexes, + column_names.clone(), + &query.order_by, + limit, + offset, + )? { + return Ok(Some(result)); + } + } + if order_by_requires_rowid_range { + return Ok(None); + } + if order_by.is_none() { + if let Some(result) = self.try_simple_filtered_projection_range_index_result( + row_source, + name, + table_schema, + filter_column_index, + filter_column, + lower_bound.as_ref(), + upper_bound.as_ref(), + &residual_plans, + &projection_indexes, + column_names.clone(), + limit, + offset, + )? { + return Ok(Some(result)); + } + } + if let Some(result) = self.try_simple_filtered_projection_ordered_index_result( + row_source, + name, + table_schema, + filter_column_index, + lower_bound.as_ref(), + upper_bound.as_ref(), + &residual_plans, + &projection_indexes, + column_names.clone(), + order_by.as_deref(), + limit, + offset, + )? { + return Ok(Some(result)); + } + Ok(Some(self.simple_filtered_projection_result_from_source( + row_source, + filter_column_index, + lower_bound.as_ref(), + upper_bound.as_ref(), + &residual_plans, + &projection_indexes, + column_names, + order_by, + limit, + offset, + )?)) + } + #[allow(clippy::too_many_arguments)] + fn try_simple_rowid_range_projection_result( + &self, + row_source: VisibleTableRowSource<'_>, + table_schema: &TableSchema, + table_binding: TableBindingRef<'_>, + filter_column: &str, + lower_bound: Option<&SimpleRangeBoundValue>, + upper_bound: Option<&SimpleRangeBoundValue>, + projection_indexes: &[usize], + column_names: Vec, + order_by: &[crate::sql::ast::OrderBy], + limit: Option, + offset: usize, + ) -> Result> { + let lower_only_limited = lower_bound.is_some() && upper_bound.is_none() && limit.is_some(); + let bounded_range = lower_bound.is_some() && upper_bound.is_some(); + if !bounded_range && !lower_only_limited { + return Ok(None); + } + if !order_by.is_empty() { + if order_by.len() != 1 || order_by[0].descending { + return Ok(None); + } + let Expr::Column { + table: order_table, + column: order_column, + } = &order_by[0].expr + else { + return Ok(None); + }; + if !identifiers_equal(order_column, filter_column) + || order_table + .as_deref() + .is_some_and(|qualifier| !matches_table_binding(table_binding, Some(qualifier))) + { + return Ok(None); + } + } + if !table_schema + .primary_key_columns + .iter() + .any(|column| identifiers_equal(column, filter_column)) + { + return Ok(None); + } + let Some(filter_column_index) = schema_column_index(table_schema, filter_column) else { + return Ok(None); + }; + if table_schema.columns[filter_column_index].column_type + != crate::catalog::ColumnType::Int64 + { + return Ok(None); + } + let Some(start) = simple_int64_range_start(lower_bound) else { + return Ok(None); + }; + let Some(end_exclusive) = simple_int64_range_end_exclusive(upper_bound) else { + return Ok(None); + }; + if end_exclusive <= start || limit == Some(0) { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + } + + let take = limit.unwrap_or(usize::MAX); + if let Some(rows) = row_source.projected_query_rows_in_id_range( + start, + end_exclusive, + take, + offset, + projection_indexes, + ) { + return Ok(Some(QueryResult::with_rows(column_names, rows))); + } + let max_probe_steps = if upper_bound.is_none() { + Some( + row_source + .row_count() + .saturating_add(offset) + .saturating_add(take), + ) + } else { + None + }; + let mut skipped = 0usize; + let mut rows = Vec::with_capacity(take.min(64)); + let mut row_id = start; + let mut probe_steps = 0usize; + while row_id < end_exclusive && rows.len() < take { + if max_probe_steps.is_some_and(|max_probe_steps| probe_steps >= max_probe_steps) { + return Ok(None); + } + probe_steps = probe_steps.saturating_add(1); + if let Some(row) = row_source.projected_query_row_by_id(row_id, projection_indexes)? { + if skipped < offset { + skipped += 1; + } else { + rows.push(row); + } + } + let Some(next_row_id) = row_id.checked_add(1) else { + break; + }; + row_id = next_row_id; + } + Ok(Some(QueryResult::with_rows(column_names, rows))) + } + #[allow(clippy::too_many_arguments)] + fn try_simple_deferred_rowid_range_projection_result( + &self, + store: &S, + state: PersistedTableState, + table_schema: &TableSchema, + table_binding: TableBindingRef<'_>, + filter_column: &str, + lower_bound: Option<&SimpleRangeBoundValue>, + upper_bound: Option<&SimpleRangeBoundValue>, + projection_indexes: &[usize], + column_names: Vec, + order_by: &[crate::sql::ast::OrderBy], + limit: Option, + offset: usize, + use_persistent_pk_index: bool, + paged_locator_cache: Option<&DeferredPagedRowLocatorCache>, + ) -> Result> { + let lower_only_limited = lower_bound.is_some() && upper_bound.is_none() && limit.is_some(); + let bounded_range = lower_bound.is_some() && upper_bound.is_some(); + if !bounded_range && !lower_only_limited { + return Ok(None); + } + if !order_by.is_empty() { + if order_by.len() != 1 || order_by[0].descending { + return Ok(None); + } + let Expr::Column { + table: order_table, + column: order_column, + } = &order_by[0].expr + else { + return Ok(None); + }; + if !identifiers_equal(order_column, filter_column) + || order_table + .as_deref() + .is_some_and(|qualifier| !matches_table_binding(table_binding, Some(qualifier))) + { + return Ok(None); + } + } + if !table_schema + .primary_key_columns + .iter() + .any(|column| identifiers_equal(column, filter_column)) + { + return Ok(None); + } + let Some(filter_column_index) = schema_column_index(table_schema, filter_column) else { + return Ok(None); + }; + if table_schema.columns[filter_column_index].column_type + != crate::catalog::ColumnType::Int64 + { + return Ok(None); + } + + let has_matching_locator_cache = + paged_locator_cache.is_some_and(|cache| cache.matches_state(state)); + let has_persistent_pk_locator = + use_persistent_pk_index && table_schema.pk_index_root.is_some(); + let has_compressed_lookup = state.pointer.is_compressed(); + if !has_matching_locator_cache && !has_persistent_pk_locator && !has_compressed_lookup { + return Ok(None); + } + + let Some(start) = simple_int64_range_start(lower_bound) else { + return Ok(None); + }; + let Some(end_exclusive) = simple_int64_range_end_exclusive(upper_bound) else { + return Ok(None); + }; + if end_exclusive <= start || limit == Some(0) { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + } + + let take = limit.unwrap_or(usize::MAX); + let max_probe_steps = if upper_bound.is_none() { + Some(state.row_count.saturating_add(offset).saturating_add(take)) + } else { + None + }; + let mut skipped = 0usize; + let mut rows = Vec::with_capacity(take.min(64)); + let mut row_id = start; + let mut probe_steps = 0usize; + while row_id < end_exclusive && rows.len() < take { + if max_probe_steps.is_some_and(|max_probe_steps| probe_steps >= max_probe_steps) { + return Ok(None); + } + probe_steps = probe_steps.saturating_add(1); + if let Some(values) = read_deferred_projected_values_by_id( + store, + state, + table_schema, + row_id, + use_persistent_pk_index, + paged_locator_cache, + projection_indexes, + )? { + if skipped < offset { + skipped += 1; + } else { + rows.push(QueryRow::new(values)); + } + } + let Some(next_row_id) = row_id.checked_add(1) else { + break; + }; + row_id = next_row_id; + } + Ok(Some(QueryResult::with_rows(column_names, rows))) + } + pub(crate) fn try_execute_simple_distinct_projection_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + if !query.ctes.is_empty() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.filter.is_some() + || !select.group_by.is_empty() + || select.having.is_some() + || !select.distinct + || !select.distinct_on.is_empty() + || select.from.len() != 1 + { + return Ok(None); + } + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + { + return Ok(None); + } + + let table_schema = match self.table_schema(name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + let Some((projection_indexes, column_names)) = + self.simple_projection_plan(select, name, alias, table_schema) + else { + return Ok(None); + }; + let order_by = self.simple_projection_order_by_plan( + query, + table_schema, + name, + alias.as_deref().unwrap_or(name), + &projection_indexes, + )?; + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let _row_source = self.visible_table_row_source(name); + let Some(row_source) = _row_source else { + return Ok(None); + }; + Ok(Some(self.simple_distinct_projection_result_from_source( + row_source, + &projection_indexes, + column_names, + order_by, + limit, + offset, + )?)) + } + pub(crate) fn try_execute_simple_distinct_filtered_projection_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + if !query.ctes.is_empty() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if !select.group_by.is_empty() + || select.having.is_some() + || !select.distinct + || !select.distinct_on.is_empty() + || select.from.len() != 1 + { + return Ok(None); + } + let Some(filter) = select.filter.as_ref() else { + return Ok(None); + }; + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + { + return Ok(None); + } + + let table_schema = match self.table_schema(name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + let Some((projection_indexes, column_names)) = + self.simple_projection_plan(select, name, alias, table_schema) + else { + return Ok(None); + }; + let binding_name = alias.as_deref().unwrap_or(name); + let Some(range_filter) = simple_range_projection_filter(filter) else { + return Ok(None); + }; + let filter_table = range_filter.table; + let filter_column = range_filter.column; + let lower_bound = range_filter.lower; + let upper_bound = range_filter.upper; + if let Some(table_name) = filter_table { + if !identifiers_equal(table_name, name) && !identifiers_equal(table_name, binding_name) + { + return Ok(None); + } + } + let filter_column_index = table_schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, filter_column)) + .ok_or_else(|| { + DbError::internal(format!( + "simple filtered distinct projection column {filter_column} missing from {name}" + )) + })?; + let lower_bound = lower_bound + .map(|bound| { + Ok(SimpleRangeBoundValue { + inclusive: bound.inclusive, + value: self.eval_expr( + bound.value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?, + }) + }) + .transpose()?; + let upper_bound = upper_bound + .map(|bound| { + Ok(SimpleRangeBoundValue { + inclusive: bound.inclusive, + value: self.eval_expr( + bound.value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?, + }) + }) + .transpose()?; + if !range_filter.residual.is_empty() { + // The distinct filtered fast path does not yet evaluate residual + // predicates; bail to the generic executor to preserve correctness. + return Ok(None); + } + let order_by = self.simple_projection_order_by_plan( + query, + table_schema, + name, + binding_name, + &projection_indexes, + )?; + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let _row_source = self.visible_table_row_source(name); + let Some(row_source) = _row_source else { + return Ok(None); + }; + Ok(Some( + self.simple_distinct_filtered_projection_result_from_source( + row_source, + filter_column_index, + lower_bound.as_ref(), + upper_bound.as_ref(), + &projection_indexes, + column_names, + order_by, + limit, + offset, + )?, + )) + } + pub(crate) fn try_execute_simple_union_range_projection_query( + &self, + query: &Query, + params: &[Value], + ctes: &BTreeMap, + ) -> Result> { + if !query.ctes.is_empty() || query.order_by.len() != 1 { + return Ok(None); + } + + let QueryBody::SetOperation { + op: crate::sql::ast::SetOperation::Union, + all: false, + left, + right, + } = &query.body + else { + return Ok(None); + }; + + let analyze_side = |body: &QueryBody| -> Result> { + let QueryBody::Select(select) = body else { + return Ok(None); + }; + if select.filter.is_none() + || !select.group_by.is_empty() + || select.having.is_some() + || select.distinct + || !select.distinct_on.is_empty() + || select.from.len() != 1 + { + return Ok(None); + } + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if ctes.contains_key(name) + || self + .visible_view(name, NameResolutionScope::Session) + .is_some() + { + return Ok(None); + } + let Some(table_schema) = self.table_schema(name) else { + return Ok(None); + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + let Some((projection_indexes, column_names)) = + self.simple_projection_plan(select, name, alias, table_schema) + else { + return Ok(None); + }; + if projection_indexes.len() != 1 { + return Ok(None); + } + let Some(filter) = select.filter.as_ref() else { + return Ok(None); + }; + let Some(range_filter) = simple_range_projection_filter(filter) else { + return Ok(None); + }; + if !range_filter.residual.is_empty() { + return Ok(None); + } + let binding_name = alias.as_deref().unwrap_or(name); + if let Some(filter_table) = range_filter.table { + if !identifiers_equal(filter_table, name) + && !identifiers_equal(filter_table, binding_name) + { + return Ok(None); + } + } + let Some(filter_column_index) = table_schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, range_filter.column)) + else { + return Ok(None); + }; + if projection_indexes[0] != filter_column_index { + return Ok(None); + } + let lower_bound = range_filter + .lower + .map(|bound| { + Ok(SimpleRangeBoundValue { + inclusive: bound.inclusive, + value: self.eval_expr( + bound.value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?, + }) + }) + .transpose()?; + let upper_bound = range_filter + .upper + .map(|bound| { + Ok(SimpleRangeBoundValue { + inclusive: bound.inclusive, + value: self.eval_expr( + bound.value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?, + }) + }) + .transpose()?; + if !simple_range_bounds_match_column_type( + table_schema.columns[filter_column_index].column_type, + lower_bound.as_ref(), + upper_bound.as_ref(), + ) { + return Ok(None); + } + Ok(Some(SimpleUnionRangeProjectionSide { + table_name: name.clone(), + alias: alias.clone(), + projection_indexes, + column_names, + filter_column_index, + lower_bound, + upper_bound, + })) + }; + + let Some(left_side) = analyze_side(left)? else { + return Ok(None); + }; + let Some(right_side) = analyze_side(right)? else { + return Ok(None); + }; + if !identifiers_equal(&left_side.table_name, &right_side.table_name) + || left_side.projection_indexes != right_side.projection_indexes + || left_side.filter_column_index != right_side.filter_column_index + { + return Ok(None); + } + + let order_by = &query.order_by[0]; + if order_by.collation.is_some() || order_by.descending { + return Ok(None); + } + let Expr::Column { + table: order_table, + column: order_column, + } = &order_by.expr + else { + return Ok(None); + }; + if let Some(order_table) = order_table.as_deref() { + if !identifiers_equal(order_table, &left_side.table_name) + && !left_side + .alias + .as_deref() + .is_some_and(|alias| identifiers_equal(order_table, alias)) + { + return Ok(None); + } + } + if !identifiers_equal(order_column, &left_side.column_names[0]) { + return Ok(None); + } + + let Some(index) = + self.single_column_btree_index(&left_side.table_name, &left_side.column_names[0]) + else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { + return Ok(None); + }; + let Some(left_start) = simple_int64_range_start(left_side.lower_bound.as_ref()) else { + return Ok(None); + }; + let Some(left_end_exclusive) = + simple_int64_range_end_exclusive(left_side.upper_bound.as_ref()) + else { + return Ok(None); + }; + let Some(right_start) = simple_int64_range_start(right_side.lower_bound.as_ref()) else { + return Ok(None); + }; + let Some(right_end_exclusive) = + simple_int64_range_end_exclusive(right_side.upper_bound.as_ref()) + else { + return Ok(None); + }; + + let mut distinct_values = BTreeSet::new(); + let mut supported = true; + let mut collect_range = |range_start: i64, range_end_exclusive: i64| { + if range_start >= range_end_exclusive { + return; + } + match keys { + RuntimeBtreeKeys::UniqueInt64(entries, deleted) => { + for (value, row_id) in entries.iter() { + if deleted.contains(&row_id) { + continue; + } + if value >= range_start && value < range_end_exclusive { + distinct_values.insert(value); + } + } + } + RuntimeBtreeKeys::NonUniqueInt64(entries, deleted) => { + for (value, row_ids) in entries.iter() { + if row_ids.iter().any(|row_id| !deleted.contains(&row_id)) + && value >= range_start + && value < range_end_exclusive + { + distinct_values.insert(value); + } + } + } + _ => supported = false, + } + }; + collect_range(left_start, left_end_exclusive); + collect_range(right_start, right_end_exclusive); + if !supported { + return Ok(None); + } + + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let rows = distinct_values + .into_iter() + .skip(offset) + .take(limit.unwrap_or(usize::MAX)) + .map(|value| vec![Value::Int64(value)]) + .collect(); + let columns = left_side + .column_names + .into_iter() + .map(|name| ColumnBinding::visible(None, name)) + .collect(); + Ok(Some(Dataset::with_rows(columns, rows))) + } + pub(crate) fn try_execute_simple_expression_projection_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + if !query.ctes.is_empty() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if !select.group_by.is_empty() + || select.having.is_some() + || !select.distinct_on.is_empty() + || select.from.len() != 1 + { + return Ok(None); + } + if select_requires_grouped_evaluation(self, select)? { + return Ok(None); + } + if select.distinct + && (!query.order_by.is_empty() || query.limit.is_some() || query.offset.is_some()) + { + return Ok(None); + } + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(name) + { + return Ok(None); + } + let table_schema = match self.table_schema(name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + if select + .projection + .iter() + .any(select_item_contains_window_or_subquery) + || select + .filter + .as_ref() + .is_some_and(expr_contains_recursive_unsupported_feature) + || query + .order_by + .iter() + .any(|order| expr_contains_recursive_unsupported_feature(&order.expr)) + { + return Ok(None); + } + if select + .projection + .iter() + .any(select_item_contains_fulltext_function) + || select + .filter + .as_ref() + .is_some_and(expr_contains_fulltext_function) + || query + .order_by + .iter() + .any(|order| expr_contains_fulltext_function(&order.expr)) + { + return Ok(None); + } + let has_expression_projection = select.projection.iter().any(|item| match item { + SelectItem::Expr { expr, .. } => !matches!(expr, Expr::Column { .. }), + SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => true, + }); + if !has_expression_projection + && select.filter.is_none() + && query.order_by.is_empty() + && query.limit.is_none() + && query.offset.is_none() + { + return Ok(None); + } + + let binding_name = alias.as_deref().unwrap_or(name); + let Some(projection_plan) = + simple_expression_projection_plan(table_schema, name, binding_name, &select.projection) + else { + return Ok(None); + }; + let ctes = BTreeMap::new(); + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &ctes)) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &ctes)) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let Some(row_source) = self.visible_table_row_source(name) else { + return Ok(None); + }; + if let Some(row_ids) = select + .filter + .as_ref() + .map(|filter| { + self.trigram_candidate_row_ids_for_filter(name, alias, filter, params, &ctes) + }) + .transpose()? + .flatten() + { + return Ok(Some( + self.simple_expression_projection_result_from_row_ids( + row_source, + table_schema, + binding_name, + &select.projection, + &projection_plan, + select.filter.as_ref(), + select.distinct, + &query.order_by, + params, + limit, + offset, + &row_ids, + )?, + )); + } + Ok(Some(self.simple_expression_projection_result_from_source( + row_source, + table_schema, + binding_name, + &select.projection, + &projection_plan, + select.filter.as_ref(), + select.distinct, + &query.order_by, + params, + limit, + offset, + )?)) + } + fn simple_projection_result_from_source( + &self, + row_source: VisibleTableRowSource<'_>, + projection_indexes: &[usize], + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, + ) -> Result { + let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); + let mut rows = Vec::with_capacity( + bounded_row_count + .unwrap_or(row_source.row_count()) + .min(row_source.row_count()), + ); + if order_by.is_none() { + let mut skipped = 0usize; + for stored_row in row_source.rows() { + let stored_row = stored_row?; + if skipped < offset { + skipped = skipped.saturating_add(1); + continue; + } + if limit.is_some_and(|limit| rows.len() >= limit) { + break; + } + rows.push(project_simple_projection_values( + stored_row.values(), + projection_indexes, + )); + } + return Ok(QueryResult::with_rows(column_names, rows)); + } + + for stored_row in row_source.rows() { + let stored_row = stored_row?; + rows.push(project_simple_projection_values( + stored_row.values(), + projection_indexes, + )); + } + apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + column_names, + order_by.as_deref(), + limit, + offset, + ) + } + fn simple_distinct_projection_result_from_source( + &self, + row_source: VisibleTableRowSource<'_>, + projection_indexes: &[usize], + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, + ) -> Result { + let mut rows = Vec::new(); + let mut seen = BTreeSet::new(); + for stored_row in row_source.rows() { + let stored_row = stored_row?; + let projected = + project_simple_projection_values(stored_row.values(), projection_indexes); + if seen.insert(row_identity(projected.values())?) { + rows.push(projected); + } + } + apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + column_names, + order_by.as_deref(), + limit, + offset, + ) + } + #[allow(clippy::too_many_arguments)] + fn simple_projection_result_from_persisted_state( + &self, + store: &S, + state: PersistedTableState, + projection_indexes: &[usize], + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, + ) -> Result { + let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); + let mut rows = Vec::with_capacity( + bounded_row_count + .unwrap_or(state.row_count) + .min(state.row_count), + ); + if order_by.is_none() { + let mut skipped = 0usize; + visit_persisted_table_rows_until(store, state, |_, values| { + if skipped < offset { + skipped = skipped.saturating_add(1); + return Ok(false); + } + if limit.is_some_and(|limit| rows.len() >= limit) { + return Ok(true); + } + rows.push(project_simple_projection_values(values, projection_indexes)); + Ok(limit.is_some_and(|limit| rows.len() >= limit)) + })?; + return Ok(QueryResult::with_rows(column_names, rows)); + } + visit_persisted_table_rows(store, state, |_, values| { + rows.push(project_simple_projection_values(values, projection_indexes)); + Ok(()) + })?; + apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + column_names, + order_by.as_deref(), + limit, + offset, + ) + } + #[allow(clippy::too_many_arguments)] + fn simple_distinct_projection_result_from_persisted_state( + &self, + store: &S, + state: PersistedTableState, + projection_indexes: &[usize], + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, + ) -> Result { + let mut rows = Vec::new(); + let mut seen = BTreeSet::new(); + visit_persisted_table_rows(store, state, |_, values| { + let projected = project_simple_projection_values(values, projection_indexes); + if seen.insert(row_identity(projected.values())?) { + rows.push(projected); + } + Ok(()) + })?; + apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + column_names, + order_by.as_deref(), + limit, + offset, + ) + } + #[allow(clippy::too_many_arguments)] + fn simple_expression_projection_result_from_persisted_state( + &self, + store: &S, + state: PersistedTableState, + table_schema: &TableSchema, + binding_name: &str, + projection: &[SelectItem], + projection_plan: &SimpleExpressionProjectionPlan<'_>, + filter: Option<&Expr>, + distinct: bool, + order_by: &[crate::sql::ast::OrderBy], + params: &[Value], + limit: Option, + offset: usize, + ) -> Result { + let dataset = Dataset::with_rows( + table_bindings_with_hidden_row_id(table_schema, binding_name), + Vec::new(), + ); + let projection_order_by = projection_order_by_plan(order_by, projection); + let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); + let mut rows = Vec::with_capacity(bounded_row_count.unwrap_or(state.row_count)); + let mut seen = BTreeSet::new(); + visit_persisted_table_rows(store, state, |row_id, values| { + let mut eval_values = values.to_vec(); + eval_values.push(Value::Int64(row_id)); + if let Some(filter) = filter { + if !matches!( + self.eval_expr( + filter, + &dataset, + &eval_values, + params, + &BTreeMap::new(), + None, + )?, + Value::Bool(true) + ) { + return Ok(()); + } + } + let output = self.project_simple_expression_row( + projection_plan, + &dataset, + &eval_values, + params, + )?; + if distinct && !seen.insert(row_identity(&output)?) { + return Ok(()); + } + + let mut order_values = Vec::with_capacity(order_by.len()); + if let Some(order_by_plan) = projection_order_by.as_deref() { + for order in order_by_plan { + order_values.push(output[order.projection_index].clone()); + } + } else { + for order in order_by { + order_values.push(self.eval_expr( + &order.expr, + &dataset, + &eval_values, + params, + &BTreeMap::new(), + None, + )?); + } + } + let row = (QueryRow::new(output), order_values); + if let Some(bounded_row_count) = bounded_row_count { + if order_by.is_empty() { + if rows.len() < bounded_row_count { + rows.push(row); + } + } else { + push_bounded_ordered_query_row( + Some(self), + &mut rows, + row, + order_by, + bounded_row_count, + )?; + } + } else { + rows.push(row); + } + Ok(()) + })?; + + if !order_by.is_empty() { + sort_query_rows_by_order_values(Some(self), &mut rows, order_by)?; + } + + let rows = rows + .into_iter() + .skip(offset) + .take(limit.unwrap_or(usize::MAX)) + .map(|(row, _)| row) + .collect(); + Ok(QueryResult::with_rows( + projection_plan.column_names.clone(), + rows, + )) + } + #[allow(clippy::too_many_arguments)] + fn simple_expression_projection_result_from_deferred_row_ids( + &self, + store: &S, + state: PersistedTableState, + table_schema: &TableSchema, + binding_name: &str, + projection: &[SelectItem], + projection_plan: &SimpleExpressionProjectionPlan<'_>, + filter: Option<&Expr>, + distinct: bool, + order_by: &[crate::sql::ast::OrderBy], + params: &[Value], + limit: Option, + offset: usize, + row_ids: &[i64], + use_persistent_pk_index: bool, + paged_locator_cache: Option<&DeferredPagedRowLocatorCache>, + ) -> Result { + let dataset = Dataset::with_rows( + table_bindings_with_hidden_row_id(table_schema, binding_name), + Vec::new(), + ); + let projection_order_by = projection_order_by_plan(order_by, projection); + let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); + let mut rows = Vec::with_capacity(bounded_row_count.unwrap_or(row_ids.len())); + let mut seen = BTreeSet::new(); + for row_id in row_ids { + let Some(stored_row) = read_deferred_stored_row_by_id( + store, + state, + table_schema, + *row_id, + use_persistent_pk_index, + paged_locator_cache, + )? + else { + continue; + }; + let mut eval_values = stored_row.values; + eval_values.push(Value::Int64(stored_row.row_id)); + if let Some(filter) = filter { + if !matches!( + self.eval_expr( + filter, + &dataset, + &eval_values, + params, + &BTreeMap::new(), + None, + )?, + Value::Bool(true) + ) { + continue; + } + } + let output = self.project_simple_expression_row( + projection_plan, + &dataset, + &eval_values, + params, + )?; + if distinct && !seen.insert(row_identity(&output)?) { + continue; + } + + let mut order_values = Vec::with_capacity(order_by.len()); + if let Some(order_by_plan) = projection_order_by.as_deref() { + for order in order_by_plan { + order_values.push(output[order.projection_index].clone()); + } + } else { + for order in order_by { + order_values.push(self.eval_expr( + &order.expr, + &dataset, + &eval_values, + params, + &BTreeMap::new(), + None, + )?); + } + } + let row = (QueryRow::new(output), order_values); + if let Some(bounded_row_count) = bounded_row_count { + if order_by.is_empty() { + if rows.len() < bounded_row_count { + rows.push(row); + } else { + break; + } + } else { + push_bounded_ordered_query_row( + Some(self), + &mut rows, + row, + order_by, + bounded_row_count, + )?; + } + } else { + rows.push(row); + } + } + + if !order_by.is_empty() { + sort_query_rows_by_order_values(Some(self), &mut rows, order_by)?; + } + + let rows = rows + .into_iter() + .skip(offset) + .take(limit.unwrap_or(usize::MAX)) + .map(|(row, _)| row) + .collect(); + Ok(QueryResult::with_rows( + projection_plan.column_names.clone(), + rows, + )) + } + #[allow(clippy::too_many_arguments)] + fn simple_expression_projection_result_from_source( + &self, + row_source: VisibleTableRowSource<'_>, + table_schema: &TableSchema, + binding_name: &str, + projection: &[SelectItem], + projection_plan: &SimpleExpressionProjectionPlan<'_>, + filter: Option<&Expr>, + distinct: bool, + order_by: &[crate::sql::ast::OrderBy], + params: &[Value], + limit: Option, + offset: usize, + ) -> Result { + let dataset = Dataset::with_rows( + table_bindings_with_hidden_row_id(table_schema, binding_name), + Vec::new(), + ); + let projection_order_by = projection_order_by_plan(order_by, projection); + let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); + let mut rows = Vec::with_capacity(bounded_row_count.unwrap_or(row_source.row_count())); + let mut seen = BTreeSet::new(); + for stored_row in row_source.rows() { + let stored_row = stored_row?; + let mut values = stored_row.values().to_vec(); + values.push(Value::Int64(stored_row.row_id())); + if let Some(filter) = filter { + if !matches!( + self.eval_expr(filter, &dataset, &values, params, &BTreeMap::new(), None)?, + Value::Bool(true) + ) { + continue; + } + } + + let output = + self.project_simple_expression_row(projection_plan, &dataset, &values, params)?; + if distinct && !seen.insert(row_identity(&output)?) { + continue; + } + + let mut order_values = Vec::with_capacity(order_by.len()); + if let Some(order_by_plan) = projection_order_by.as_deref() { + for order in order_by_plan { + order_values.push(output[order.projection_index].clone()); + } + } else { + for order in order_by { + order_values.push(self.eval_expr( + &order.expr, + &dataset, + &values, + params, + &BTreeMap::new(), + None, + )?); + } + } + let row = (QueryRow::new(output), order_values); + if let Some(bounded_row_count) = bounded_row_count { + if order_by.is_empty() { + if rows.len() < bounded_row_count { + rows.push(row); + } else { + break; + } + } else { + push_bounded_ordered_query_row( + Some(self), + &mut rows, + row, + order_by, + bounded_row_count, + )?; + } + } else { + rows.push(row); + } + } + + if !order_by.is_empty() { + sort_query_rows_by_order_values(Some(self), &mut rows, order_by)?; + } + + let rows = rows + .into_iter() + .skip(offset) + .take(limit.unwrap_or(usize::MAX)) + .map(|(row, _)| row) + .collect(); + Ok(QueryResult::with_rows( + projection_plan.column_names.clone(), + rows, + )) + } + #[allow(clippy::too_many_arguments)] + fn simple_expression_projection_result_from_row_ids( + &self, + row_source: VisibleTableRowSource<'_>, + table_schema: &TableSchema, + binding_name: &str, + projection: &[SelectItem], + projection_plan: &SimpleExpressionProjectionPlan<'_>, + filter: Option<&Expr>, + distinct: bool, + order_by: &[crate::sql::ast::OrderBy], + params: &[Value], + limit: Option, + offset: usize, + row_ids: &[i64], + ) -> Result { + let dataset = Dataset::with_rows( + table_bindings_with_hidden_row_id(table_schema, binding_name), + Vec::new(), + ); + let projection_order_by = projection_order_by_plan(order_by, projection); + let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); + let mut rows = Vec::with_capacity(bounded_row_count.unwrap_or(row_ids.len())); + let mut seen = BTreeSet::new(); + for row_id in row_ids { + let Some(stored_row) = row_source.row_by_id(*row_id)? else { + continue; + }; + let mut values = stored_row.values().to_vec(); + values.push(Value::Int64(stored_row.row_id())); + if let Some(filter) = filter { + if !matches!( + self.eval_expr(filter, &dataset, &values, params, &BTreeMap::new(), None)?, + Value::Bool(true) + ) { + continue; + } + } + + let output = + self.project_simple_expression_row(projection_plan, &dataset, &values, params)?; + if distinct && !seen.insert(row_identity(&output)?) { + continue; + } + + let mut order_values = Vec::with_capacity(order_by.len()); + if let Some(order_by_plan) = projection_order_by.as_deref() { + for order in order_by_plan { + order_values.push(output[order.projection_index].clone()); + } + } else { + for order in order_by { + order_values.push(self.eval_expr( + &order.expr, + &dataset, + &values, + params, + &BTreeMap::new(), + None, + )?); + } + } + let row = (QueryRow::new(output), order_values); + if let Some(bounded_row_count) = bounded_row_count { + if order_by.is_empty() { + if rows.len() < bounded_row_count { + rows.push(row); + } else { + break; + } + } else { + push_bounded_ordered_query_row( + Some(self), + &mut rows, + row, + order_by, + bounded_row_count, + )?; + } + } else { + rows.push(row); + } + } + + if !order_by.is_empty() { + sort_query_rows_by_order_values(Some(self), &mut rows, order_by)?; + } + + let rows = rows + .into_iter() + .skip(offset) + .take(limit.unwrap_or(usize::MAX)) + .map(|(row, _)| row) + .collect(); + Ok(QueryResult::with_rows( + projection_plan.column_names.clone(), + rows, + )) + } + fn project_simple_expression_row( + &self, + projection_plan: &SimpleExpressionProjectionPlan<'_>, + dataset: &Dataset, + values: &[Value], + params: &[Value], + ) -> Result> { + let mut output = Vec::with_capacity(projection_plan.sources.len()); + for source in &projection_plan.sources { + match source { + SimpleExpressionProjectionSource::Column(index) => { + let value = values.get(*index).ok_or_else(|| { + DbError::internal("expression projection column index exceeds row width") + })?; + output.push(value.clone()); + } + SimpleExpressionProjectionSource::Expr(expr) => { + output.push(self.eval_expr( + expr, + dataset, + values, + params, + &BTreeMap::new(), + None, + )?); + } + } + } + Ok(output) + } + #[allow(clippy::too_many_arguments)] + fn try_simple_filtered_projection_exact_index_result( + &self, + row_source: VisibleTableRowSource<'_>, + table_name: &str, + table_schema: &TableSchema, + filter: &Expr, + projection_indexes: &[usize], + column_names: Vec, + order_by: Option<&[SimpleOrderByPlan]>, + params: &[Value], + limit: Option, + offset: usize, + ) -> Result> { + if limit == Some(0) { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + } + let Some((filter_table, filter_column, value_expr)) = simple_btree_lookup(filter) else { + return Ok(None); + }; + if let Some(filter_table) = filter_table { + if !identifiers_equal(filter_table, table_name) { + return Ok(None); + } + } + + let value = self.eval_expr( + value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?; + if matches!(value, Value::Null) { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + } + + let mut rows = Vec::new(); + if row_id_alias_column_name(table_schema) + .is_some_and(|column_name| identifiers_equal(column_name, filter_column)) + { + if let Value::Int64(row_id) = value { + if let Some(stored_row) = row_source.row_by_id(row_id)? { + rows.push(project_simple_projection_values( + stored_row.values(), + projection_indexes, + )); + } + } + return Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + column_names, + order_by, + limit, + offset, + )?)); + } + + let Some(index) = self.single_column_btree_index(table_name, filter_column) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { + return Ok(None); + }; + match keys.row_ids_for_value_set(&value)? { + RuntimeRowIdSet::Empty => {} + RuntimeRowIdSet::Single(row_id) => { + if let Some(stored_row) = row_source.row_by_id(row_id)? { + rows.push(project_simple_projection_values( + stored_row.values(), + projection_indexes, + )); + } + } + RuntimeRowIdSet::Contiguous { start, len } => { + rows.reserve(len); + for row_id in contiguous_row_ids(start, len) { + if let Some(stored_row) = row_source.row_by_id(row_id)? { + rows.push(project_simple_projection_values( + stored_row.values(), + projection_indexes, + )); + } + } + } + RuntimeRowIdSet::Many(row_ids) => { + rows.reserve(row_ids.len()); + for row_id in row_ids { + if let Some(stored_row) = row_source.row_by_id(*row_id)? { + rows.push(project_simple_projection_values( + stored_row.values(), + projection_indexes, + )); + } + } + } + RuntimeRowIdSet::Owned(row_ids) => { + rows.reserve(row_ids.len()); + for row_id in row_ids { + if let Some(stored_row) = row_source.row_by_id(row_id)? { + rows.push(project_simple_projection_values( + stored_row.values(), + projection_indexes, + )); + } + } + } + } + Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + column_names, + order_by, + limit, + offset, + )?)) + } + #[allow(clippy::too_many_arguments)] + fn try_simple_filtered_projection_range_index_result( + &self, + row_source: VisibleTableRowSource<'_>, + table_name: &str, + table_schema: &TableSchema, + filter_column_index: usize, + filter_column_name: &str, + lower_bound: Option<&SimpleRangeBoundValue>, + upper_bound: Option<&SimpleRangeBoundValue>, + residual_plans: &[SimpleResidualPlan], + projection_indexes: &[usize], + column_names: Vec, + limit: Option, + offset: usize, + ) -> Result> { + if limit == Some(0) { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + } + let Some(filter_column) = table_schema.columns.get(filter_column_index) else { + return Ok(None); + }; + if !simple_range_bounds_match_column_type( + filter_column.column_type, + lower_bound, + upper_bound, + ) { + return Ok(None); + } + let Some(index) = self.single_column_btree_index(table_name, filter_column_name) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { + return Ok(None); + }; + + let lower_key = lower_bound + .map(|bound| encode_runtime_index_key(&bound.value).map(|key| (key, bound.inclusive))) + .transpose()?; + let upper_key = upper_bound + .map(|bound| encode_runtime_index_key(&bound.value).map(|key| (key, bound.inclusive))) + .transpose()?; + let lower_range: Bound<&[u8]> = match lower_key.as_ref() { + Some((key, true)) => Bound::Included(key.as_slice()), + Some((key, false)) => Bound::Excluded(key.as_slice()), + None => Bound::Unbounded, + }; + let upper_range: Bound<&[u8]> = match upper_key.as_ref() { + Some((key, true)) => Bound::Included(key.as_slice()), + Some((key, false)) => Bound::Excluded(key.as_slice()), + None => Bound::Unbounded, + }; + + let mut candidate_row_ids = Vec::new(); + match keys { + RuntimeBtreeKeys::UniqueEncoded(entries, deleted) => { + candidate_row_ids.extend( + entries + .range::<[u8], _>((lower_range, upper_range)) + .filter_map(|(_, row_id)| (!deleted.contains(row_id)).then_some(*row_id)), + ); + } + RuntimeBtreeKeys::NonUniqueEncoded(entries, deleted) => { + for row_ids in entries + .range::<[u8], _>((lower_range, upper_range)) + .map(|(_, row_ids)| row_ids) + { + candidate_row_ids.extend( + row_ids + .iter() + .copied() + .filter(|row_id| !deleted.contains(row_id)), + ); + } + } + RuntimeBtreeKeys::UniqueInt64(..) + | RuntimeBtreeKeys::NonUniqueInt64(..) + | RuntimeBtreeKeys::UniqueUuid(..) + | RuntimeBtreeKeys::NonUniqueUuid(..) => return Ok(None), + } + if candidate_row_ids.len().saturating_mul(2) > row_source.row_count() { + return Ok(None); + } + candidate_row_ids.sort_unstable(); + + let take = limit.unwrap_or(usize::MAX); + let mut skipped = 0usize; + let mut rows = Vec::with_capacity(take.min(candidate_row_ids.len()).min(128)); + for row_id in candidate_row_ids { + let Some(stored_row) = row_source.row_by_id(row_id)? else { + continue; + }; + let values = stored_row.values(); + let candidate = &values[filter_column_index]; + if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? + || !simple_residual_matches_all(values, residual_plans)? + { + continue; + } + if skipped < offset { + skipped = skipped.saturating_add(1); + continue; + } + rows.push(project_simple_projection_values(values, projection_indexes)); + if rows.len() >= take { + break; + } + } + + Ok(Some(QueryResult::with_rows(column_names, rows))) + } + #[allow(clippy::too_many_arguments)] + fn try_simple_filtered_projection_ordered_index_result( + &self, + row_source: VisibleTableRowSource<'_>, + table_name: &str, + table_schema: &TableSchema, + filter_column_index: usize, + lower_bound: Option<&SimpleRangeBoundValue>, + upper_bound: Option<&SimpleRangeBoundValue>, + residual_plans: &[SimpleResidualPlan], + projection_indexes: &[usize], + column_names: Vec, + order_by: Option<&[SimpleOrderByPlan]>, + limit: Option, + offset: usize, + ) -> Result> { + let Some([order_by]) = order_by else { + return Ok(None); + }; + if order_by.collation.is_some() { + return Ok(None); + } + if limit == Some(0) { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + } + let Some(order_column_index) = projection_indexes.get(order_by.projection_index).copied() + else { + return Ok(None); + }; + let Some(order_column) = table_schema.columns.get(order_column_index) else { + return Ok(None); + }; + let Some(index) = self.single_column_btree_index(table_name, &order_column.name) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { + return Ok(None); + }; + let take = limit.unwrap_or(usize::MAX); + let mut skipped = 0usize; + let mut rows = Vec::with_capacity(take.min(64)); + + let mut push_matching_row = |row_id| -> Result { + let Some(stored_row) = row_source.row_by_id(row_id)? else { + return Ok(false); + }; + let values = stored_row.values(); + let candidate = &values[filter_column_index]; + if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? + || !simple_residual_matches_all(values, residual_plans)? + { + return Ok(false); + } + if skipped < offset { + skipped = skipped.saturating_add(1); + return Ok(false); + } + rows.push(project_simple_projection_values(values, projection_indexes)); + Ok(rows.len() >= take) + }; + + match keys { + RuntimeBtreeKeys::UniqueEncoded(entries, deleted) => { + if order_by.descending { + for row_id in entries.values().rev() { + if deleted.contains(row_id) { + continue; + } + if push_matching_row(*row_id)? { + break; + } + } + } else { + for row_id in entries.values() { + if deleted.contains(row_id) { + continue; + } + if push_matching_row(*row_id)? { + break; + } + } + } + } + RuntimeBtreeKeys::NonUniqueEncoded(entries, deleted) => { + if order_by.descending { + let mut done = false; + for row_ids in entries.values().rev() { + for row_id in row_ids { + if deleted.contains(row_id) { + continue; + } + if push_matching_row(*row_id)? { + done = true; + break; + } + } + if done { + break; + } + } + } else { + let mut done = false; + for row_ids in entries.values() { + for row_id in row_ids { + if deleted.contains(row_id) { + continue; + } + if push_matching_row(*row_id)? { + done = true; + break; + } + } + if done { + break; + } + } + } + } + RuntimeBtreeKeys::UniqueUuid(entries, deleted) => { + if order_by.descending { + for row_id in entries.values().rev() { + if deleted.contains(row_id) { + continue; + } + if push_matching_row(*row_id)? { + break; + } + } + } else { + for row_id in entries.values() { + if deleted.contains(row_id) { + continue; + } + if push_matching_row(*row_id)? { + break; + } + } + } + } + RuntimeBtreeKeys::NonUniqueUuid(entries, deleted) => { + if order_by.descending { + let mut done = false; + for row_ids in entries.values().rev() { + for row_id in row_ids { + if deleted.contains(row_id) { + continue; + } + if push_matching_row(*row_id)? { + done = true; + break; + } + } + if done { + break; + } + } + } else { + let mut done = false; + for row_ids in entries.values() { + for row_id in row_ids { + if deleted.contains(row_id) { + continue; + } + if push_matching_row(*row_id)? { + done = true; + break; + } + } + if done { + break; + } + } + } + } + RuntimeBtreeKeys::UniqueInt64(..) | RuntimeBtreeKeys::NonUniqueInt64(..) => { + return Ok(None) + } + } + + Ok(Some(QueryResult::with_rows(column_names, rows))) + } + #[allow(clippy::too_many_arguments)] + fn simple_filtered_projection_result_from_source( + &self, + row_source: VisibleTableRowSource<'_>, + filter_column_index: usize, + lower_bound: Option<&SimpleRangeBoundValue>, + upper_bound: Option<&SimpleRangeBoundValue>, + residual_plans: &[SimpleResidualPlan], + projection_indexes: &[usize], + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, + ) -> Result { + let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); + let mut rows = Vec::with_capacity( + bounded_row_count + .unwrap_or(row_source.row_count()) + .min(row_source.row_count()), + ); + if order_by.is_none() { + let mut skipped = 0usize; + for stored_row in row_source.rows() { + let stored_row = stored_row?; + let values = stored_row.values(); + let candidate = &values[filter_column_index]; + if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? { + continue; + } + if !simple_residual_matches_all(values, residual_plans)? { + continue; + } + if skipped < offset { + skipped = skipped.saturating_add(1); + continue; + } + if limit.is_some_and(|limit| rows.len() >= limit) { + break; + } + rows.push(project_simple_projection_values(values, projection_indexes)); + } + return Ok(QueryResult::with_rows(column_names, rows)); + } + + for stored_row in row_source.rows() { + let stored_row = stored_row?; + let values = stored_row.values(); + let candidate = &values[filter_column_index]; + if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? { + continue; + } + if !simple_residual_matches_all(values, residual_plans)? { + continue; + } + let row = project_simple_projection_values(values, projection_indexes); + if let (Some(order_by), Some(bounded_row_count)) = ( + order_by.as_deref(), + bounded_row_count.filter(|bounded| { + *bounded > 0 && row_source.row_count() > bounded.saturating_mul(4) + }), + ) { + push_bounded_projection_ordered_query_row( + Some(self), + &mut rows, + row, + order_by, + bounded_row_count, + )?; + } else { + rows.push(row); + } + } + apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + column_names, + order_by.as_deref(), + limit, + offset, + ) + } + fn simple_contains_like_projection_result_from_source( + &self, + row_source: VisibleTableRowSource<'_>, + filter_column_index: usize, + literal: &str, + projection_indexes: &[usize], + column_names: Vec, + ) -> Result { + let mut rows = Vec::new(); + for stored_row in row_source.rows() { + let stored_row = stored_row?; + let values = stored_row.values(); + let Some(Value::Text(candidate)) = values.get(filter_column_index) else { + continue; + }; + if candidate.contains(literal) { + rows.push(project_simple_projection_values(values, projection_indexes)); + } + } + Ok(QueryResult::with_rows(column_names, rows)) + } + #[allow(clippy::too_many_arguments)] + fn simple_distinct_filtered_projection_result_from_source( + &self, + row_source: VisibleTableRowSource<'_>, + filter_column_index: usize, + lower_bound: Option<&SimpleRangeBoundValue>, + upper_bound: Option<&SimpleRangeBoundValue>, + projection_indexes: &[usize], + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, + ) -> Result { + let mut rows = Vec::new(); + let mut seen = BTreeSet::new(); + for stored_row in row_source.rows() { + let stored_row = stored_row?; + let candidate = &stored_row.values()[filter_column_index]; + if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? { + continue; + } + let projected = + project_simple_projection_values(stored_row.values(), projection_indexes); + if seen.insert(row_identity(projected.values())?) { + rows.push(projected); + } + } + apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + column_names, + order_by.as_deref(), + limit, + offset, + ) + } + #[allow(clippy::too_many_arguments)] + fn simple_filtered_projection_result_from_persisted_state( + &self, + store: &S, + state: PersistedTableState, + filter_column_index: usize, + lower_bound: Option<&SimpleRangeBoundValue>, + upper_bound: Option<&SimpleRangeBoundValue>, + residual_plans: &[SimpleResidualPlan], + projection_indexes: &[usize], + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, + ) -> Result { + let bounded_row_count = limit.map(|limit| limit.saturating_add(offset)); + let mut rows = Vec::with_capacity( + bounded_row_count + .unwrap_or(state.row_count) + .min(state.row_count), + ); + if order_by.is_none() { + let mut skipped = 0usize; + visit_persisted_table_rows_until(store, state, |_, values| { + let candidate = &values[filter_column_index]; + if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? { + return Ok(false); + } + if !simple_residual_matches_all(values, residual_plans)? { + return Ok(false); + } + if skipped < offset { + skipped = skipped.saturating_add(1); + return Ok(false); + } + if limit.is_some_and(|limit| rows.len() >= limit) { + return Ok(true); + } + rows.push(project_simple_projection_values(values, projection_indexes)); + Ok(limit.is_some_and(|limit| rows.len() >= limit)) + })?; + return Ok(QueryResult::with_rows(column_names, rows)); + } + visit_persisted_table_rows(store, state, |_, values| { + let candidate = &values[filter_column_index]; + if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? { + return Ok(()); + } + if !simple_residual_matches_all(values, residual_plans)? { + return Ok(()); + } + let row = project_simple_projection_values(values, projection_indexes); + if let (Some(order_by), Some(bounded_row_count)) = ( + order_by.as_deref(), + bounded_row_count + .filter(|bounded| *bounded > 0 && state.row_count > bounded.saturating_mul(4)), + ) { + push_bounded_projection_ordered_query_row( + Some(self), + &mut rows, + row, + order_by, + bounded_row_count, + )?; + } else { + rows.push(row); + } + Ok(()) + })?; + apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + column_names, + order_by.as_deref(), + limit, + offset, + ) + } + fn build_simple_residual_plans( + &self, + table_schema: &TableSchema, + table_name: &str, + binding_name: &str, + residual: &[SimpleResidualFilterTerm<'_>], + params: &[Value], + ) -> Result> { + let mut plans = Vec::with_capacity(residual.len()); + for term in residual { + if let Some(term_table) = term.table { + if !identifiers_equal(term_table, table_name) + && !identifiers_equal(term_table, binding_name) + { + return Ok(plans); + } + } + let column_index = table_schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, term.column)) + .ok_or_else(|| { + DbError::internal(format!( + "simple filtered projection residual column {} missing from {table_name}", + term.column + )) + })?; + let value = self.eval_expr( + term.value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?; + plans.push(SimpleResidualPlan { + column_index, + op: term.op, + value, + }); + } + Ok(plans) + } + #[allow(clippy::too_many_arguments)] + fn simple_distinct_filtered_projection_result_from_persisted_state( + &self, + store: &S, + state: PersistedTableState, + filter_column_index: usize, + lower_bound: Option<&SimpleRangeBoundValue>, + upper_bound: Option<&SimpleRangeBoundValue>, + projection_indexes: &[usize], + column_names: Vec, + order_by: Option>, + limit: Option, + offset: usize, + ) -> Result { + let mut rows = Vec::new(); + let mut seen = BTreeSet::new(); + visit_persisted_table_rows(store, state, |_, values| { + let candidate = &values[filter_column_index]; + if !simple_range_bound_matches(candidate, lower_bound, upper_bound)? { + return Ok(()); + } + let projected = project_simple_projection_values(values, projection_indexes); + if seen.insert(row_identity(projected.values())?) { + rows.push(projected); + } + Ok(()) + })?; + apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + column_names, + order_by.as_deref(), + limit, + offset, + ) + } + pub(crate) fn try_execute_simple_indexed_projection_query( + &self, + query: &Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_simple_indexed_projection_query(query, params)? else { + return Ok(None); + }; + let row_source = self.visible_table_row_source(plan.table_name); + if plan.limit == Some(0) { + return Ok(Some(QueryResult::with_rows(plan.column_names, Vec::new()))); + } + + if plan.extra_lookup_terms.is_empty() + && row_id_alias_column_name(plan.table_schema) + .is_some_and(|column_name| identifiers_equal(column_name, plan.filter_column)) + { + let mut rows = Vec::new(); + if let Some(row_id) = value_as_int64(&plan.lookup_value) { + if let Some(stored_row) = row_source + .map(|source| source.row_by_id(row_id)) + .transpose()? + .flatten() + { + rows.push(project_simple_projection_values( + stored_row.values(), + &plan.projection_indexes, + )); + } + } + return Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + plan.column_names, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + )?)); + } + + let Some(index) = self.btree_index_for_simple_indexed_projection_plan(&plan) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { keys, covering }) = self.index(&index.name) else { + return Ok(None); + }; + let covering_offsets = if row_source.is_some_and(|source| !source.has_tombstoned_rows()) { + covering.as_ref().and_then(|covering| { + covering_projection_offsets(covering, plan.table_schema, &plan.projection_indexes) + }) + } else { + None + }; + let row_id_order = indexed_projection_row_id_order(&plan); + let row_ids = row_ids_for_simple_indexed_projection_lookup(keys, &plan)?; + + let scan_limit = if let Some((_, limit_with_offset)) = row_id_order { + limit_with_offset + } else if plan.order_by.is_none() && plan.offset == 0 { + plan.limit.unwrap_or(usize::MAX) + } else { + usize::MAX + }; + let mut rows = Vec::with_capacity(row_ids.len().min(scan_limit)); + let mut row_lookup_error = None; + if let Some((descending, _)) = row_id_order { + let ordered_row_ids = row_ids.into_sorted_vec(descending); + let limit = plan.limit.unwrap_or(usize::MAX); + for row_id in ordered_row_ids.into_iter().skip(plan.offset).take(limit) { + if row_lookup_error.is_some() || rows.len() >= scan_limit { + break; + } + if let (Some(covering), Some(offsets)) = + (covering.as_ref(), covering_offsets.as_ref()) + { + if let Some(row) = covering.project_row(row_id, offsets) { + rows.push(row); + continue; + } + } + let stored_row = match row_source + .map(|source| source.row_by_id(row_id)) + .transpose() + { + Ok(Some(Some(stored_row))) => stored_row, + Ok(Some(None)) | Ok(None) => continue, + Err(error) => { + row_lookup_error = Some(error); + break; + } + }; + rows.push(project_simple_projection_values( + stored_row.values(), + &plan.projection_indexes, + )); + } + if let Some(error) = row_lookup_error { + return Err(error); + } + return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); + } else { + row_ids.for_each(|row_id| { + if row_lookup_error.is_some() || rows.len() >= scan_limit { + return; + } + if let (Some(covering), Some(offsets)) = + (covering.as_ref(), covering_offsets.as_ref()) + { + if let Some(row) = covering.project_row(row_id, offsets) { + rows.push(row); + return; + } + } + let stored_row = match row_source + .map(|source| source.row_by_id(row_id)) + .transpose() + { + Ok(Some(Some(stored_row))) => stored_row, + Ok(Some(None)) | Ok(None) => return, + Err(error) => { + row_lookup_error = Some(error); + return; + } + }; + rows.push(project_simple_projection_values( + stored_row.values(), + &plan.projection_indexes, + )); + }); + } + if let Some(error) = row_lookup_error { + return Err(error); + } + Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + plan.column_names, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + )?)) + } + pub(crate) fn try_execute_simple_deferred_indexed_projection_query( + &self, + query: &Query, + params: &[Value], + pager: &PagerHandle, + wal: &WalHandle, + snapshot_lsn: u64, + use_persistent_pk_index: bool, + ) -> Result> { + let Some(plan) = self.analyze_simple_indexed_projection_query(query, params)? else { + return Ok(None); + }; + if let Some(row_source) = self.visible_table_row_source(plan.table_name) { + if plan.limit == Some(0) { + return Ok(Some(QueryResult::with_rows(plan.column_names, Vec::new()))); + } + if plan.extra_lookup_terms.is_empty() + && row_id_alias_column_name(plan.table_schema) + .is_some_and(|column_name| identifiers_equal(column_name, plan.filter_column)) + { + let mut rows = Vec::new(); + if let Some(row_id) = value_as_int64(&plan.lookup_value) { + if let Some(stored_row) = row_source.row_by_id(row_id)? { + rows.push(project_simple_projection_values( + stored_row.values(), + &plan.projection_indexes, + )); + } + } + return Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + plan.column_names, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + )?)); + } + + let Some(index) = self.btree_index_for_simple_indexed_projection_plan(&plan) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { keys, covering }) = self.index(&index.name) else { + return Ok(None); + }; + let row_id_order = indexed_projection_row_id_order(&plan); + let covering_offsets = if !row_source.has_tombstoned_rows() { + covering.as_ref().and_then(|covering| { + covering_projection_offsets( + covering, + plan.table_schema, + &plan.projection_indexes, + ) + }) + } else { + None + }; + let row_ids = row_ids_for_simple_indexed_projection_lookup(keys, &plan)?; + let scan_limit = + if row_id_order.is_none() && plan.order_by.is_none() && plan.offset == 0 { + plan.limit.unwrap_or(usize::MAX) + } else if let Some((_, limit_with_offset)) = row_id_order { + limit_with_offset + } else { + usize::MAX + }; + let mut rows = Vec::with_capacity(row_ids.len().min(scan_limit)); + let mut row_lookup_error = None; + if let Some((descending, _)) = row_id_order { + let ordered_row_ids = row_ids.into_sorted_vec(descending); + let limit = plan.limit.unwrap_or(usize::MAX); + for row_id in ordered_row_ids.into_iter().skip(plan.offset).take(limit) { + if row_lookup_error.is_some() || rows.len() >= scan_limit { + break; + } + if let (Some(covering), Some(offsets)) = + (covering.as_ref(), covering_offsets.as_ref()) + { + if let Some(row) = covering.project_row(row_id, offsets) { + rows.push(row); + continue; + } + } + match row_source.row_by_id(row_id) { + Ok(Some(stored_row)) => rows.push(project_simple_projection_values( + stored_row.values(), + &plan.projection_indexes, + )), + Ok(None) => {} + Err(error) => row_lookup_error = Some(error), + } + } + if let Some(error) = row_lookup_error { + return Err(error); + } + return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); + } + row_ids.for_each(|row_id| { + if row_lookup_error.is_some() || rows.len() >= scan_limit { + return; + } + if let (Some(covering), Some(offsets)) = + (covering.as_ref(), covering_offsets.as_ref()) + { + if let Some(row) = covering.project_row(row_id, offsets) { + rows.push(row); + return; + } + } + match row_source.row_by_id(row_id) { + Ok(Some(stored_row)) => rows.push(project_simple_projection_values( + stored_row.values(), + &plan.projection_indexes, + )), + Ok(None) => {} + Err(error) => row_lookup_error = Some(error), + } + }); + if let Some(error) = row_lookup_error { + return Err(error); + } + return Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + plan.column_names, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + )?)); + } + if !self.has_deferred_tables() { + return Ok(None); + } + if plan.limit == Some(0) { + return Ok(Some(QueryResult::with_rows(plan.column_names, Vec::new()))); + } + if !self + .deferred_table_names() + .any(|candidate| identifiers_equal(candidate, plan.table_name)) + { + return Ok(None); + } + let Some(state) = self.persisted_table_state(plan.table_name) else { + return Ok(None); + }; + let paged_locator_cache = self + .catalog + .table(plan.table_name) + .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) + .map(|cache| cache.as_ref()); + + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + + let mut rows = Vec::new(); + if plan.extra_lookup_terms.is_empty() + && row_id_alias_column_name(plan.table_schema) + .is_some_and(|column_name| identifiers_equal(column_name, plan.filter_column)) + { + if let Some(row_id) = value_as_int64(&plan.lookup_value) { + if let Some(stored_row) = read_deferred_stored_row_by_id( + &store, + state, + plan.table_schema, + row_id, + use_persistent_pk_index, + paged_locator_cache, + )? { + rows.push(project_simple_projection_row( + &stored_row, + &plan.projection_indexes, + )); + } + } + return Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + plan.column_names, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + )?)); + } + + let Some(index) = self.btree_index_for_simple_indexed_projection_plan(&plan) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { keys, covering }) = self.index(&index.name) else { + return Ok(None); + }; + let covering_offsets = covering.as_ref().and_then(|covering| { + covering_projection_offsets(covering, plan.table_schema, &plan.projection_indexes) + }); + let row_id_order = indexed_projection_row_id_order(&plan); + let row_ids = row_ids_for_simple_indexed_projection_lookup(keys, &plan)?; + let scan_limit = if let Some((_, limit_with_offset)) = row_id_order { + limit_with_offset + } else if plan.order_by.is_none() && plan.offset == 0 { + plan.limit.unwrap_or(usize::MAX) + } else { + usize::MAX + }; + rows.reserve(row_ids.len().min(scan_limit)); + let mut row_lookup_error = None; + if let Some((descending, _)) = row_id_order { + let ordered_row_ids = row_ids.into_sorted_vec(descending); + let limit = plan.limit.unwrap_or(usize::MAX); + for row_id in ordered_row_ids.into_iter().skip(plan.offset).take(limit) { + if row_lookup_error.is_some() || rows.len() >= scan_limit { + break; + } + if let (Some(covering), Some(offsets)) = + (covering.as_ref(), covering_offsets.as_ref()) + { + if let Some(row) = covering.project_row(row_id, offsets) { + rows.push(row); + continue; + } + } + match read_deferred_stored_row_by_id( + &store, + state, + plan.table_schema, + row_id, + use_persistent_pk_index, + paged_locator_cache, + ) { + Ok(Some(stored_row)) => rows.push(project_simple_projection_row( + &stored_row, + &plan.projection_indexes, + )), + Ok(None) => {} + Err(error) => { + row_lookup_error = Some(error); + break; + } + } + } + if let Some(error) = row_lookup_error { + return Err(error); + } + return Ok(Some(QueryResult::with_rows(plan.column_names, rows))); + } else { + row_ids.for_each(|row_id| { + if row_lookup_error.is_some() || rows.len() >= scan_limit { + return; + } + if let (Some(covering), Some(offsets)) = + (covering.as_ref(), covering_offsets.as_ref()) + { + if let Some(row) = covering.project_row(row_id, offsets) { + rows.push(row); + return; + } + } + match read_deferred_stored_row_by_id( + &store, + state, + plan.table_schema, + row_id, + use_persistent_pk_index, + paged_locator_cache, + ) { + Ok(Some(stored_row)) => rows.push(project_simple_projection_row( + &stored_row, + &plan.projection_indexes, + )), + Ok(None) => {} + Err(error) => row_lookup_error = Some(error), + } + }); + } + if let Some(error) = row_lookup_error { + return Err(error); + } + Ok(Some(apply_simple_projection_postprocessing_with_order( + Some(self), + rows, + plan.column_names, + plan.order_by.as_deref(), + plan.limit, + plan.offset, + )?)) + } + pub(crate) fn execute_simple_row_id_projection_at_snapshot( + &self, + request: SimpleRowIdProjectionRequest<'_>, + ) -> Result> { + if let Some(view) = self.visible_view(request.table_name, NameResolutionScope::Session) { + return self.execute_simple_view_row_id_projection_at_snapshot(&request, view); + } + if self.visible_table_is_temporary(request.table_name) { + return Ok(None); + } + let Some(table_schema) = self.table_schema(request.table_name) else { + return Ok(None); + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + if !row_id_alias_column_name(table_schema) + .is_some_and(|column_name| identifiers_equal(column_name, request.filter_column)) + { + return Ok(None); + } + let mut projection_indexes = Vec::with_capacity(request.projection_columns.len()); + let mut column_names = Vec::with_capacity(request.projection_columns.len()); + for projection_column in request.projection_columns { + let Some(index) = table_schema + .columns + .iter() + .position(|column| identifiers_equal(&column.name, projection_column)) + else { + return Ok(None); + }; + projection_indexes.push(index); + column_names.push((*projection_column).to_string()); + } + + self.execute_validated_simple_row_id_projection_at_snapshot( + ValidatedSimpleRowIdProjectionRequest { + table_schema, + projection_indexes: &projection_indexes, + column_names: Arc::from(column_names), + lookup_row_id: request.lookup_row_id, + pager: request.pager, + wal: request.wal, + snapshot_lsn: request.snapshot_lsn, + use_persistent_pk_index: request.use_persistent_pk_index, + }, + ) + } + pub(crate) fn try_execute_resident_simple_row_id_projection( + &self, + table_name: &str, + projection_columns: &[&str], + filter_column: &str, + lookup_row_id: i64, + ) -> Result> { + if let Some(view) = self.visible_view(table_name, NameResolutionScope::Session) { + if view.temporary { + return Ok(None); + } + // The observed-current caller has already established that this + // runtime represents a stable committed snapshot. When every + // base table needed by the view is resident, execute the same + // validated indexed join without acquiring a reader slot or + // constructing a snapshot page store. A missing resident source + // returns `None`, preserving the snapshot-backed fallback. + let store = page::InMemoryPageStore::default(); + return self.execute_simple_view_row_id_projection_from_store( + projection_columns, + filter_column, + lookup_row_id, + view, + &store, + false, + true, + ); + } + if self.visible_table_is_temporary(table_name) { + return Ok(None); + } + let Some(table_schema) = self.table_schema(table_name) else { + return Ok(None); + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + if !row_id_alias_column_name(table_schema) + .is_some_and(|column_name| identifiers_equal(column_name, filter_column)) + { + return Ok(None); + } + let mut projection_indexes = Vec::with_capacity(projection_columns.len()); + let mut column_names = Vec::with_capacity(projection_columns.len()); + for projection_column in projection_columns { + let Some(index) = table_schema + .columns + .iter() + .position(|column| identifiers_equal(&column.name, projection_column)) + else { + return Ok(None); + }; + projection_indexes.push(index); + column_names.push((*projection_column).to_string()); + } + self.try_execute_validated_resident_simple_row_id_projection( + table_schema, + &projection_indexes, + Arc::from(column_names), + lookup_row_id, + ) + } + fn execute_simple_view_row_id_projection_at_snapshot( + &self, + request: &SimpleRowIdProjectionRequest<'_>, + view: &ViewSchema, + ) -> Result> { + let store = SnapshotPageStore { + pager: request.pager, + wal: request.wal, + snapshot_lsn: request.snapshot_lsn, + }; + self.execute_simple_view_row_id_projection_from_store( + request.projection_columns, + request.filter_column, + request.lookup_row_id, + view, + &store, + request.use_persistent_pk_index, + false, + ) + } + #[allow(clippy::too_many_arguments)] + fn execute_simple_view_row_id_projection_from_store( + &self, + projection_columns: &[&str], + filter_column: &str, + lookup_row_id: i64, + view: &ViewSchema, + store: &S, + use_persistent_pk_index: bool, + resident_only: bool, + ) -> Result> { + if view.temporary { + return Ok(None); + } + let view_query = self.cached_view_query(view)?; + if view_query.recursive + || !view_query.ctes.is_empty() + || !view_query.order_by.is_empty() + || view_query.limit.is_some() + || view_query.offset.is_some() + { + return Ok(None); + } + let QueryBody::Select(view_select) = &view_query.body else { + return Ok(None); + }; + if view_select.distinct + || !view_select.distinct_on.is_empty() + || !view_select.group_by.is_empty() + || view_select.having.is_some() + || view_select.filter.is_some() + || projection_has_aggregate_items(&view_select.projection) + || view_select.from.len() != 1 + { + return Ok(None); + } + + let Some(filter_source_expr) = + view_projection_expr_for_output_column(&view_select.projection, filter_column) + else { + return Ok(None); + }; + let Expr::Column { + table: Some(filter_source_table), + column: filter_source_column, + } = &filter_source_expr + else { + return Ok(None); + }; + + let mut table_bindings = Vec::with_capacity(3); + let mut join_constraints = Vec::with_capacity(2); + if !flatten_inner_join_chain( + &view_select.from[0], + &mut table_bindings, + &mut join_constraints, + ) || table_bindings.len() < 2 + || join_constraints.len() + 1 != table_bindings.len() + { + return Ok(None); + } + + let mut table_schemas = Vec::with_capacity(table_bindings.len()); + for binding in &table_bindings { + if self + .visible_view(binding.name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(binding.name) + { + return Ok(None); + } + let Some(schema) = self.table_schema(binding.name) else { + return Ok(None); + }; + if !generated_columns_are_stored(schema) { + return Ok(None); + } + table_schemas.push(schema); + } + + let Some(source_table_index) = table_bindings + .iter() + .position(|binding| identifiers_equal(binding.binding_name(), filter_source_table)) + else { + return Ok(None); + }; + if source_table_index != 0 { + return Ok(None); + } + let Some(source_rowid_column) = row_id_alias_column_name(table_schemas[source_table_index]) + else { + return Ok(None); + }; + if !identifiers_equal(source_rowid_column, filter_source_column) { + return Ok(None); + } + + let mut projections = Vec::with_capacity(projection_columns.len()); + let mut column_names = Vec::with_capacity(projection_columns.len()); + for projection_column in projection_columns { + let Some(view_expr) = + view_projection_expr_for_output_column(&view_select.projection, projection_column) + else { + return Ok(None); + }; + let Expr::Column { + table: Some(base_table), + column: base_column, + } = view_expr + else { + return Ok(None); + }; + let Some(table_index) = table_bindings + .iter() + .position(|binding| identifiers_equal(binding.binding_name(), &base_table)) + else { + return Ok(None); + }; + let Some(column_index) = schema_column_index(table_schemas[table_index], &base_column) + else { + return Ok(None); + }; + projections.push(DeferredViewProjection { + table_index, + column_index, + is_rowid_alias: rowid_alias_column_index(table_schemas[table_index]) + == Some(column_index), + }); + column_names.push((*projection_column).to_string()); + } + + let mut join_steps = Vec::with_capacity(join_constraints.len()); + for current_table_index in 1..table_bindings.len() { + let Some(step) = self.deferred_view_join_step( + &table_bindings, + &table_schemas, + join_constraints[current_table_index - 1], + current_table_index, + )? + else { + return Ok(None); + }; + join_steps.push(step); + } + let table_projections = + build_deferred_view_table_projections(&table_schemas, &projections, &join_steps); + let projection_indexes = build_deferred_view_projection_indexes( + &projections, + &table_projections, + "simple view row-id projection", + )?; + let linear_tail_can_move = + deferred_view_linear_tail_projection_can_move(&projection_indexes); + + let mut join_keys = Vec::with_capacity(join_steps.len()); + let mut key_projection_indexes = Vec::with_capacity(join_steps.len()); + for step in &join_steps { + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&step.current_index_name) + else { + join_keys.clear(); + key_projection_indexes.clear(); + break; + }; + join_keys.push(keys); + key_projection_indexes.push(join_key_projection_index( + step, + &table_projections[step.previous_table_index], + )?); + } + + let mut table_readers = Vec::with_capacity(table_bindings.len()); + for (binding, schema) in table_bindings.iter().zip(table_schemas.iter()) { + if let Some(source) = self.visible_table_row_source(binding.name) { + table_readers.push(DeferredViewTableRowReader::Source(source)); + continue; + } + if resident_only && self.dirty_tables.contains(&schema.name) { + return Ok(None); + } + let Some(state) = self.persisted_table_state(binding.name) else { + return Ok(None); + }; + let cache = self + .catalog + .table(binding.name) + .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) + .map(|cache| cache.as_ref()); + if !deferred_rowid_lookup_available(state, schema, use_persistent_pk_index, cache) { + return Ok(None); + } + table_readers.push(DeferredViewTableRowReader::Deferred { + state, + schema, + paged_locator_cache: cache, + }); + } + + if resident_only { + return self.try_execute_observed_current_linear_three_table_view( + &table_readers, + &join_steps, + &join_keys, + &key_projection_indexes, + &table_projections, + &projection_indexes, + lookup_row_id, + column_names, + linear_tail_can_move, + ); + } + + let mut chunk_payload_cache = HashMap::new(); + let Some(source_row) = table_readers[source_table_index].read_projected_with_chunk_cache( + store, + lookup_row_id, + use_persistent_pk_index, + &table_projections[source_table_index].projection_indexes, + &mut chunk_payload_cache, + )? + else { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + }; + + let mut rows = Vec::with_capacity(64); + let mut join_partial_rows = Vec::with_capacity(join_steps.len() + 1); + if let Some(stopped) = self.stream_deferred_view_linear_three_table_rows_from_root( + store, + &table_readers, + &join_steps, + &join_keys, + &key_projection_indexes, + &table_projections, + &source_row, + use_persistent_pk_index, + &mut chunk_payload_cache, + &mut |root_row, row1, row2| { + let row = collect_deferred_view_query_row_from_linear_tail( + root_row, + row1, + row2, + &projection_indexes, + "simple view row-id projection", + linear_tail_can_move, + )?; + rows.push(row); + Ok(false) + }, + )? { + let _ = stopped; + return Ok(Some(QueryResult::with_rows(column_names, rows))); + } + + match self.stream_deferred_view_join_rows_from_root( + store, + &table_readers, + &join_steps, + &join_keys, + &key_projection_indexes, + &table_projections, + source_row, + &mut join_partial_rows, + use_persistent_pk_index, + false, + &mut chunk_payload_cache, + &mut |partial| { + let values = collect_deferred_view_projection_values( + partial, + &projection_indexes, + "simple view row-id projection", + )?; + rows.push(QueryRow::new(values)); + Ok(false) + }, + )? { + Some(_) => Ok(Some(QueryResult::with_rows(column_names, rows))), + None => Ok(None), + } + } + pub(crate) fn execute_resolved_simple_ordered_row_id_projection( + &self, + request: ResolvedSimpleOrderedRowIdProjectionRequest<'_>, + ) -> Result> { + if self + .visible_view(request.table_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(request.table_name) + { + return Ok(None); + } + let Some(table_schema) = self.table_schema(request.table_name) else { + return Ok(None); + }; + if !generated_columns_are_stored(table_schema) + || request + .projection_indexes + .iter() + .any(|index| *index >= table_schema.columns.len()) + { + return Ok(None); + } + let Some(order_index) = schema_column_index(table_schema, request.order_column) else { + return Ok(None); + }; + if !row_id_alias_column_name(table_schema) + .is_some_and(|column_name| identifiers_equal(column_name, request.order_column)) + || table_schema.columns[order_index].column_type != ColumnType::Int64 + { + return Ok(None); + } + if request.limit == Some(0) { + return Ok(Some(QueryResult::with_shared_columns( + request.column_names, + Vec::new(), + ))); + } + let Some(row_source) = self.visible_table_row_source(table_schema.name.as_str()) else { + return Ok(None); + }; + let take = request.limit.unwrap_or(usize::MAX); + let row_ids = if let Some(row_ids) = self.ordered_runtime_btree_row_ids( + table_schema.name.as_str(), + request.order_column, + request.limit, + request.offset, + request.descending, + )? { + row_ids + } else { + let mut ordered_row_ids = Vec::with_capacity(row_source.row_count()); + for stored_row in row_source.rows() { + ordered_row_ids.push(stored_row?.row_id()); + } + ordered_row_ids.sort_unstable(); + if request.descending { + ordered_row_ids.reverse(); + } + ordered_row_ids + .into_iter() + .skip(request.offset) + .take(take) + .collect() + }; + let mut rows = Vec::with_capacity(row_ids.len().min(64)); + for row_id in row_ids { + if let Some(row) = + row_source.projected_query_row_by_id(row_id, request.projection_indexes)? + { + rows.push(row); + } + } + Ok(Some(QueryResult::with_shared_columns( + request.column_names, + rows, + ))) + } + pub(crate) fn execute_resolved_simple_row_id_projection_at_snapshot( + &self, + request: ResolvedSimpleRowIdProjectionRequest<'_>, + ) -> Result> { + if self + .visible_view(request.table_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(request.table_name) + { + return Ok(None); + } + let Some(table_schema) = self.table_schema(request.table_name) else { + return Ok(None); + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + if request + .projection_indexes + .iter() + .any(|index| *index >= table_schema.columns.len()) + { + return Ok(None); + } + self.execute_validated_simple_row_id_projection_at_snapshot( + ValidatedSimpleRowIdProjectionRequest { + table_schema, + projection_indexes: request.projection_indexes, + column_names: Arc::clone(&request.column_names), + lookup_row_id: request.lookup_row_id, + pager: request.pager, + wal: request.wal, + snapshot_lsn: request.snapshot_lsn, + use_persistent_pk_index: request.use_persistent_pk_index, + }, + ) + } + pub(crate) fn execute_resolved_simple_row_id_range_projection_at_snapshot( + &self, + request: ResolvedSimpleRowIdRangeProjectionRequest<'_>, + ) -> Result> { + if self + .visible_view(request.table_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(request.table_name) + { + return Ok(None); + } + let Some(table_schema) = self.table_schema(request.table_name) else { + return Ok(None); + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + if request + .projection_indexes + .iter() + .any(|index| *index >= table_schema.columns.len()) + { + return Ok(None); + } + let Some(filter_column_index) = schema_column_index(table_schema, request.filter_column) + else { + return Ok(None); + }; + if !table_schema + .primary_key_columns + .iter() + .any(|column| identifiers_equal(column, request.filter_column)) + || table_schema.columns[filter_column_index].column_type != ColumnType::Int64 + { + return Ok(None); + } + + let canonical_table_name = table_schema.name.as_str(); + let no_alias = None; + if let Some(row_source) = self.visible_table_row_source(canonical_table_name) { + return self.try_simple_rowid_range_projection_result( + row_source, + table_schema, + TableBindingRef { + name: canonical_table_name, + alias: &no_alias, + }, + request.filter_column, + request.lower_bound.as_ref(), + request.upper_bound.as_ref(), + request.projection_indexes, + request.column_names.to_vec(), + &[], + request.limit, + 0, + ); + } + + if !self.has_deferred_tables() + || !self + .deferred_table_names() + .any(|candidate| identifiers_equal(candidate, canonical_table_name)) + { + return Ok(None); + } + let Some(state) = self.persisted_table_state(canonical_table_name) else { + return Ok(None); + }; + let store = SnapshotPageStore { + pager: request.pager, + wal: request.wal, + snapshot_lsn: request.snapshot_lsn, + }; + let paged_locator_cache = self + .catalog + .table(canonical_table_name) + .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) + .map(|cache| cache.as_ref()); + self.try_simple_deferred_rowid_range_projection_result( + &store, + state, + table_schema, + TableBindingRef { + name: canonical_table_name, + alias: &no_alias, + }, + request.filter_column, + request.lower_bound.as_ref(), + request.upper_bound.as_ref(), + request.projection_indexes, + request.column_names.to_vec(), + &[], + request.limit, + 0, + request.use_persistent_pk_index, + paged_locator_cache, + ) + } + pub(crate) fn execute_resolved_simple_row_id_join_projection_at_snapshot( + &self, + request: ResolvedSimpleRowIdJoinProjectionRequest<'_>, + ) -> Result> { + if self + .visible_view(request.left_table_name, NameResolutionScope::Session) + .is_some() + || self + .visible_view(request.right_table_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(request.left_table_name) + || self.visible_table_is_temporary(request.right_table_name) + { + return Ok(None); + } + let Some(left_schema) = self.table_schema(request.left_table_name) else { + return Ok(None); + }; + let Some(right_schema) = self.table_schema(request.right_table_name) else { + return Ok(None); + }; + if !generated_columns_are_stored(left_schema) || !generated_columns_are_stored(right_schema) + { + return Ok(None); + } + if request + .projections + .iter() + .any(|projection| match projection.side { + SimpleJoinProjectionSide::Left => { + projection.index >= request.left_projection_indexes.len() + } + SimpleJoinProjectionSide::Right => { + projection.index >= request.right_projection_indexes.len() + } + }) + || request + .left_projection_indexes + .iter() + .any(|index| *index >= left_schema.columns.len()) + || request + .right_projection_indexes + .iter() + .any(|index| *index >= right_schema.columns.len()) + { + return Ok(None); + } + + let left_name = left_schema.name.as_str(); + let right_name = right_schema.name.as_str(); + if let (Some(left_source), Some(right_source)) = ( + self.visible_table_row_source(left_name), + self.visible_table_row_source(right_name), + ) { + let Some(left_row) = left_source.row_by_id(request.lookup_row_id)? else { + return Ok(Some(QueryResult::with_shared_columns( + Arc::clone(&request.column_names), + Vec::new(), + ))); + }; + let Some(right_row) = right_source.row_by_id(request.lookup_row_id)? else { + return Ok(Some(QueryResult::with_shared_columns( + Arc::clone(&request.column_names), + Vec::new(), + ))); + }; + let row = project_resolved_simple_join_row_from_full_values( + request.projections, + request.left_projection_indexes, + request.right_projection_indexes, + left_row.values(), + right_row.values(), + )?; + return Ok(Some(QueryResult::with_shared_columns( + Arc::clone(&request.column_names), + vec![row], + ))); + } + + let Some(left_state) = self.persisted_table_state(left_name) else { + return Ok(None); + }; + let Some(right_state) = self.persisted_table_state(right_name) else { + return Ok(None); + }; + let left_cache = self + .catalog + .table(left_name) + .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) + .map(|cache| cache.as_ref()); + let right_cache = self + .catalog + .table(right_name) + .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) + .map(|cache| cache.as_ref()); + if !deferred_rowid_lookup_available( + left_state, + left_schema, + request.use_persistent_pk_index, + left_cache, + ) || !deferred_rowid_lookup_available( + right_state, + right_schema, + request.use_persistent_pk_index, + right_cache, + ) { + return Ok(None); + } + + let store = SnapshotPageStore { + pager: request.pager, + wal: request.wal, + snapshot_lsn: request.snapshot_lsn, + }; + let Some(left_values) = read_deferred_projected_values_by_id( + &store, + left_state, + left_schema, + request.lookup_row_id, + request.use_persistent_pk_index, + left_cache, + request.left_projection_indexes, + )? + else { + return Ok(Some(QueryResult::with_shared_columns( + Arc::clone(&request.column_names), + Vec::new(), + ))); + }; + let Some(right_values) = read_deferred_projected_values_by_id( + &store, + right_state, + right_schema, + request.lookup_row_id, + request.use_persistent_pk_index, + right_cache, + request.right_projection_indexes, + )? + else { + return Ok(Some(QueryResult::with_shared_columns( + Arc::clone(&request.column_names), + Vec::new(), + ))); + }; + let row = + project_resolved_simple_join_row(request.projections, &left_values, &right_values)?; + Ok(Some(QueryResult::with_shared_columns( + Arc::clone(&request.column_names), + vec![row], + ))) + } + pub(crate) fn try_execute_validated_resident_simple_row_id_projection( + &self, + table_schema: &TableSchema, + projection_indexes: &[usize], + column_names: Arc<[String]>, + lookup_row_id: i64, + ) -> Result> { + let canonical_table_name = table_schema.name.as_str(); + if let Some(row_source) = self.visible_table_row_source(canonical_table_name) { + let projects_complete_row = projection_indexes.len() == table_schema.columns.len() + && projection_indexes + .iter() + .enumerate() + .all(|(position, index)| position == *index); + let row = if projects_complete_row { + row_source.full_query_row_by_id(lookup_row_id)? + } else { + row_source.projected_query_row_by_id(lookup_row_id, projection_indexes)? + }; + let rows = row.map(|row| vec![row]).unwrap_or_default(); + return Ok(Some(QueryResult::with_shared_columns(column_names, rows))); + } + + // A checkpoint may re-defer a paged table while retaining its compact + // row locator directory and a bounded set of already-verified chunk + // payloads. Those immutable payloads are part of this runtime's + // observed snapshot, so a point lookup can be answered without a + // pager read or a cross-process reader slot. If the requested chunk + // was not retained, preserve the normal snapshot-backed fallback. + if !self.dirty_tables.contains(canonical_table_name) { + let state = self.persisted_table_state(canonical_table_name); + let cache = self + .deferred_paged_row_locator_caches + .get(canonical_table_name); + if let (Some(state), Some(cache)) = (state, cache) { + if cache.matches_state(state) { + if let Some(cached) = cache.locators.get(lookup_row_id) { + if let Some(payload) = + cache.verified_payload(cached.pointer, cached.checksum) + { + let projects_complete_row = projection_indexes.len() + == table_schema.columns.len() + && projection_indexes + .iter() + .enumerate() + .all(|(position, index)| position == *index); + let row = if projects_complete_row { + decode_row_by_locator_from_payload( + payload, + lookup_row_id, + cached.locator, + ) + .map(|row| QueryRow::new(row.values))? + } else { + QueryRow::new(decode_projected_values_by_locator_from_payload::< + page::InMemoryPageStore, + >( + None, payload, cached.locator, projection_indexes + )?) + }; + return Ok(Some(QueryResult::with_shared_columns( + column_names, + vec![row], + ))); + } + } + } + } + } + Ok(None) + } + fn try_execute_simple_deferred_rowid_join_projection_query( + &self, + query: &Query, + params: &[Value], + pager: &PagerHandle, + wal: &WalHandle, + snapshot_lsn: u64, + use_persistent_pk_index: bool, + ) -> Result> { + if !query.ctes.is_empty() || !query.order_by.is_empty() || query.limit.is_some() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if !select.group_by.is_empty() + || select.having.is_some() + || select.distinct + || !select.distinct_on.is_empty() + || projection_has_aggregate_items(&select.projection) + || select.from.len() != 1 + { + return Ok(None); + } + let Some(filter) = select.filter.as_ref() else { + return Ok(None); + }; + let FromItem::Join { + left, + right, + kind: JoinKind::Inner, + constraint, + } = &select.from[0] + else { + return Ok(None); + }; + let FromItem::Table { + name: left_name, + alias: left_alias, + } = &**left + else { + return Ok(None); + }; + let FromItem::Table { + name: right_name, + alias: right_alias, + } = &**right + else { + return Ok(None); + }; + if self + .visible_view(left_name, NameResolutionScope::Session) + .is_some() + || self + .visible_view(right_name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(left_name) + || self.visible_table_is_temporary(right_name) + { + return Ok(None); + } + + let left_schema = match self.table_schema(left_name) { + Some(table) => table, + None => return Ok(None), + }; + let right_schema = match self.table_schema(right_name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(left_schema) || !generated_columns_are_stored(right_schema) + { + return Ok(None); + } + let left_binding = TableBindingRef { + name: left_name, + alias: left_alias, + }; + let right_binding = TableBindingRef { + name: right_name, + alias: right_alias, + }; + let Some(join_equalities) = simple_indexed_join_constraint_equalities( + constraint, + left_binding, + right_binding, + left_schema, + right_schema, + ) else { + return Ok(None); + }; + let Some((left_join_columns, right_join_columns)) = + orient_join_equalities(&join_equalities, left_binding, right_binding) + else { + return Ok(None); + }; + if left_join_columns.len() != 1 || right_join_columns.len() != 1 { + return Ok(None); + } + let Some(left_rowid_column) = crate::exec::dml::row_id_alias_column_name(left_schema) + else { + return Ok(None); + }; + let Some(right_rowid_column) = crate::exec::dml::row_id_alias_column_name(right_schema) + else { + return Ok(None); + }; + if !identifiers_equal(left_join_columns[0], left_rowid_column) + || !identifiers_equal(right_join_columns[0], right_rowid_column) + { + return Ok(None); + } + + let Some((filter_table, filter_column, value_expr)) = simple_btree_lookup(filter) else { + return Ok(None); + }; + let filter_value = self.eval_expr( + value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?; + let Value::Int64(source_row_id) = filter_value else { + return Ok(None); + }; + + let source_is_left = if matches_table_binding(left_binding, filter_table) + && identifiers_equal(filter_column, left_rowid_column) + { + true + } else if matches_table_binding(right_binding, filter_table) + && identifiers_equal(filter_column, right_rowid_column) + { + false + } else { + return Ok(None); + }; + let using_join_columns = + simple_indexed_join_using_columns(constraint, left_schema, right_schema); + let join_eval_bindings = simple_join_projection_eval_bindings( + left_name, + left_alias, + left_schema, + right_name, + right_alias, + right_schema, + ); + let join_eval_dataset = Dataset::with_rows(join_eval_bindings, Vec::new()); + let Some((projection_plan, column_names)) = simple_join_projection_plan( + &select.projection, + &join_eval_dataset, + left_name, + left_alias, + left_schema, + right_name, + right_alias, + right_schema, + &using_join_columns, + ) else { + return Ok(None); + }; + + let source_join_column = if source_is_left { + left_join_columns[0] + } else { + right_join_columns[0] + }; + if let (Some(left_source), Some(right_source)) = ( + self.visible_table_row_source(left_name), + self.visible_table_row_source(right_name), + ) { + let (source_source, source_schema, probe_source) = if source_is_left { + (left_source, left_schema, right_source) + } else { + (right_source, right_schema, left_source) + }; + let Some(source_row) = source_source.row_by_id(source_row_id)? else { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + }; + let Some(source_join_index) = schema_column_index(source_schema, source_join_column) + else { + return Ok(None); + }; + let Some(Value::Int64(probe_row_id)) = source_row.values().get(source_join_index) + else { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + }; + let Some(probe_row) = probe_source.row_by_id(*probe_row_id)? else { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + }; + let left_values = if source_is_left { + source_row.values() + } else { + probe_row.values() + }; + let right_values = if source_is_left { + probe_row.values() + } else { + source_row.values() + }; + let row = project_simple_join_row( + self, + &projection_plan, + &join_eval_dataset, + Some(left_values), + left_schema.columns.len(), + Some(right_values), + right_schema.columns.len(), + params, + )?; + return Ok(Some(QueryResult::with_rows( + column_names, + vec![QueryRow::new(row)], + ))); + } + + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + let Some(left_state) = self.persisted_table_state(left_name) else { + return Ok(None); + }; + let Some(right_state) = self.persisted_table_state(right_name) else { + return Ok(None); + }; + let left_cache = self + .catalog + .table(left_name) + .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) + .map(|cache| cache.as_ref()); + let right_cache = self + .catalog + .table(right_name) + .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) + .map(|cache| cache.as_ref()); + if !deferred_rowid_lookup_available( + left_state, + left_schema, + use_persistent_pk_index, + left_cache, + ) || !deferred_rowid_lookup_available( + right_state, + right_schema, + use_persistent_pk_index, + right_cache, + ) { + return Ok(None); + } + + let (source_state, source_schema, source_cache, probe_state, probe_schema, probe_cache) = + if source_is_left { + ( + left_state, + left_schema, + left_cache, + right_state, + right_schema, + right_cache, + ) + } else { + ( + right_state, + right_schema, + right_cache, + left_state, + left_schema, + left_cache, + ) + }; + let Some(source_row) = read_deferred_stored_row_by_id( + &store, + source_state, + source_schema, + source_row_id, + use_persistent_pk_index, + source_cache, + )? + else { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + }; + let Some(source_join_index) = schema_column_index(source_schema, source_join_column) else { + return Ok(None); + }; + let join_value = source_row.values.get(source_join_index); + let Some(Value::Int64(probe_row_id)) = join_value else { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + }; + let Some(probe_row) = read_deferred_stored_row_by_id( + &store, + probe_state, + probe_schema, + *probe_row_id, + use_persistent_pk_index, + probe_cache, + )? + else { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + }; + + let left_values = if source_is_left { + source_row.values.as_slice() + } else { + probe_row.values.as_slice() + }; + let right_values = if source_is_left { + probe_row.values.as_slice() + } else { + source_row.values.as_slice() + }; + let row = project_simple_join_row( + self, + &projection_plan, + &join_eval_dataset, + Some(left_values), + left_schema.columns.len(), + Some(right_values), + right_schema.columns.len(), + params, + )?; + Ok(Some(QueryResult::with_rows( + column_names, + vec![QueryRow::new(row)], + ))) + } + #[allow(clippy::too_many_arguments)] + fn try_execute_simple_deferred_view_projection_limit_query( + &self, + query: &Query, + params: &[Value], + pager: &PagerHandle, + wal: &WalHandle, + snapshot_lsn: u64, + use_persistent_pk_index: bool, + ) -> Result> { + if query.recursive || !query.ctes.is_empty() || !query.order_by.is_empty() { + return Ok(None); + } + let Some(limit_expr) = query.limit.as_ref() else { + return Ok(None); + }; + let ctes = BTreeMap::new(); + let limit = usize::try_from(self.eval_constant_i64(limit_expr, params, &ctes)?.max(0)) + .unwrap_or(usize::MAX); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &ctes)) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || select.filter.is_some() + || !select.group_by.is_empty() + || select.having.is_some() + || projection_has_aggregate_items(&select.projection) + || select.from.len() != 1 + { + return Ok(None); + } + let FromItem::Table { + name: view_name, + alias: view_alias, + } = &select.from[0] + else { + return Ok(None); + }; + let Some(view) = self.visible_view(view_name, NameResolutionScope::Session) else { + return Ok(None); + }; + if view.temporary { + return Ok(None); + } + let view_binding = view_alias.as_deref().unwrap_or(view_name.as_str()); + let view_query = self.cached_view_query(view)?; + if view_query.recursive + || !view_query.ctes.is_empty() + || !view_query.order_by.is_empty() + || view_query.limit.is_some() + || view_query.offset.is_some() + { + return Ok(None); + } + let QueryBody::Select(view_select) = &view_query.body else { + return Ok(None); + }; + if view_select.distinct + || !view_select.distinct_on.is_empty() + || !view_select.group_by.is_empty() + || view_select.having.is_some() + || view_select.filter.is_some() + || projection_has_aggregate_items(&view_select.projection) + || view_select.from.len() != 1 + { + return Ok(None); + } + + let mut table_bindings = Vec::with_capacity(3); + let mut join_constraints = Vec::with_capacity(2); + if !flatten_inner_join_chain( + &view_select.from[0], + &mut table_bindings, + &mut join_constraints, + ) || !(2..=3).contains(&table_bindings.len()) + || join_constraints.len() + 1 != table_bindings.len() + { + return Ok(None); + } + + let mut table_schemas = Vec::with_capacity(table_bindings.len()); + for binding in &table_bindings { + if self + .visible_view(binding.name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(binding.name) + { + return Ok(None); + } + let Some(schema) = self.table_schema(binding.name) else { + return Ok(None); + }; + if !generated_columns_are_stored(schema) { + return Ok(None); + } + table_schemas.push(schema); + } + let deferred_row_count = table_bindings + .iter() + .filter(|binding| self.visible_table_row_source(binding.name).is_none()) + .filter_map(|binding| self.persisted_table_state(binding.name)) + .map(|state| state.row_count) + .sum::(); + if deferred_row_count < DEFERRED_VIEW_LIMIT_MIN_PERSISTED_ROWS { + return Ok(None); + } + + let mut projections = Vec::with_capacity(select.projection.len()); + let mut column_names = Vec::with_capacity(select.projection.len()); + for (ordinal, item) in select.projection.iter().enumerate() { + let SelectItem::Expr { expr, alias } = item else { + return Ok(None); + }; + let Expr::Column { + table: outer_table, + column: outer_column, + } = expr + else { + return Ok(None); + }; + if outer_table + .as_deref() + .is_some_and(|qualifier| !identifiers_equal(qualifier, view_binding)) + { + return Ok(None); + } + let Some(view_expr) = + view_projection_expr_for_output_column(&view_select.projection, outer_column) + else { + return Ok(None); + }; + let Expr::Column { + table: Some(base_table), + column: base_column, + } = view_expr + else { + return Ok(None); + }; + let Some(table_index) = table_bindings + .iter() + .position(|binding| identifiers_equal(binding.binding_name(), &base_table)) + else { + return Ok(None); + }; + let Some(column_index) = schema_column_index(table_schemas[table_index], &base_column) + else { + return Ok(None); + }; + let is_rowid_alias = + rowid_alias_column_index(table_schemas[table_index]) == Some(column_index); + projections.push(DeferredViewProjection { + table_index, + column_index, + is_rowid_alias, + }); + column_names.push( + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, ordinal + 1)), + ); + } + if limit == 0 { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + } + + let mut join_steps = Vec::with_capacity(join_constraints.len()); + for current_table_index in 1..table_bindings.len() { + let Some(step) = self.deferred_view_join_step( + &table_bindings, + &table_schemas, + join_constraints[current_table_index - 1], + current_table_index, + )? + else { + return Ok(None); + }; + join_steps.push(step); + } + let table_projections = + build_deferred_view_table_projections(&table_schemas, &projections, &join_steps); + let projection_indexes = build_deferred_view_projection_indexes( + &projections, + &table_projections, + "deferred view limit projection", + )?; + let linear_tail_can_move = + deferred_view_linear_tail_projection_can_move(&projection_indexes); + let mut join_keys = Vec::with_capacity(join_steps.len()); + let mut key_projection_indexes = Vec::with_capacity(join_steps.len()); + for step in &join_steps { + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&step.current_index_name) + else { + return Ok(None); + }; + join_keys.push(keys); + key_projection_indexes.push(join_key_projection_index( + step, + &table_projections[step.previous_table_index], + )?); + } + + let mut table_readers = Vec::with_capacity(table_bindings.len()); + for (binding, schema) in table_bindings.iter().zip(table_schemas.iter()) { + if let Some(source) = self.visible_table_row_source(binding.name) { + table_readers.push(DeferredViewTableRowReader::Source(source)); + continue; + } + let Some(state) = self.persisted_table_state(binding.name) else { + return Ok(None); + }; + let cache = self + .catalog + .table(binding.name) + .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) + .map(|cache| cache.as_ref()); + if !deferred_rowid_lookup_available(state, schema, use_persistent_pk_index, cache) { + return Ok(None); + } + table_readers.push(DeferredViewTableRowReader::Deferred { + state, + schema, + paged_locator_cache: cache, + }); + } + + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + let mut chunk_payload_cache = HashMap::new(); + let mut rows = Vec::with_capacity(limit); + let mut join_partial_rows = Vec::with_capacity(join_steps.len() + 1); + let mut offset_remaining = offset; + let mut limit_remaining = limit; + if let DeferredViewTableRowReader::Source(source) = &table_readers[0] { + for root_row in source.rows() { + let root_row = root_row?; + let values = if table_projections[0].projection_indexes.is_empty() { + Vec::new() + } else { + project_simple_projection_value_vec( + root_row.values(), + &table_projections[0].projection_indexes, + ) + }; + let root_row = StoredRow { + row_id: root_row.row_id(), + values, + }; + if self.push_deferred_view_limit_rows_from_root( + &store, + &table_readers, + &join_steps, + &join_keys, + &key_projection_indexes, + &table_projections, + &projection_indexes, + root_row, + &mut offset_remaining, + &mut limit_remaining, + &mut rows, + &mut join_partial_rows, + &mut chunk_payload_cache, + use_persistent_pk_index, + linear_tail_can_move, + )? { + break; + } + } + } else { + let DeferredViewTableRowReader::Deferred { state, .. } = &table_readers[0] else { + return Ok(None); + }; + visit_persisted_table_projected_values_until( + &store, + *state, + &table_projections[0].projection_indexes, + |row_id, root_values| { + let root_row = StoredRow { + row_id, + values: root_values.to_vec(), + }; + self.push_deferred_view_limit_rows_from_root( + &store, + &table_readers, + &join_steps, + &join_keys, + &key_projection_indexes, + &table_projections, + &projection_indexes, + root_row, + &mut offset_remaining, + &mut limit_remaining, + &mut rows, + &mut join_partial_rows, + &mut chunk_payload_cache, + use_persistent_pk_index, + linear_tail_can_move, + ) + }, + )?; + } + + Ok(Some(QueryResult::with_rows(column_names, rows))) + } + #[allow(clippy::too_many_arguments)] + fn try_execute_simple_deferred_view_filter_projection_query( + &self, + query: &Query, + params: &[Value], + pager: &PagerHandle, + wal: &WalHandle, + snapshot_lsn: u64, + use_persistent_pk_index: bool, + ) -> Result> { + if query.recursive + || !query.ctes.is_empty() + || !query.order_by.is_empty() + || query.limit.is_some() + || query.offset.is_some() + { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.distinct + || !select.distinct_on.is_empty() + || !select.group_by.is_empty() + || select.having.is_some() + || projection_has_aggregate_items(&select.projection) + || select.from.len() != 1 + { + return Ok(None); + } + let Some(filter) = select.filter.as_ref() else { + return Ok(None); + }; + let FromItem::Table { + name: view_name, + alias: view_alias, + } = &select.from[0] + else { + return Ok(None); + }; + let Some(view) = self.visible_view(view_name, NameResolutionScope::Session) else { + return Ok(None); + }; + if view.temporary { + return Ok(None); + } + let view_binding = view_alias.as_deref().unwrap_or(view_name.as_str()); + let Some((filter_qualifier, filter_column, value_expr)) = simple_btree_lookup(filter) + else { + return Ok(None); + }; + if filter_qualifier.is_some_and(|qualifier| !identifiers_equal(qualifier, view_binding)) { + return Ok(None); + } + + let view_query = self.cached_view_query(view)?; + if view_query.recursive + || !view_query.ctes.is_empty() + || !view_query.order_by.is_empty() + || view_query.limit.is_some() + || view_query.offset.is_some() + { + return Ok(None); + } + let QueryBody::Select(view_select) = &view_query.body else { + return Ok(None); + }; + if view_select.distinct + || !view_select.distinct_on.is_empty() + || !view_select.group_by.is_empty() + || view_select.having.is_some() + || view_select.filter.is_some() + || projection_has_aggregate_items(&view_select.projection) + || view_select.from.len() != 1 + { + return Ok(None); + } + + let Some(filter_source_expr) = + view_projection_expr_for_output_column(&view_select.projection, filter_column) + else { + return Ok(None); + }; + let Expr::Column { + table: Some(filter_source_table), + column: filter_source_column, + } = &filter_source_expr + else { + return Ok(None); + }; + + let mut table_bindings = Vec::with_capacity(3); + let mut join_constraints = Vec::with_capacity(2); + if !flatten_inner_join_chain( + &view_select.from[0], + &mut table_bindings, + &mut join_constraints, + ) || table_bindings.len() < 2 + || join_constraints.len() + 1 != table_bindings.len() + { + return Ok(None); + } + + let mut table_schemas = Vec::with_capacity(table_bindings.len()); + for binding in &table_bindings { + if self + .visible_view(binding.name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(binding.name) + { + return Ok(None); + } + let Some(schema) = self.table_schema(binding.name) else { + return Ok(None); + }; + if !generated_columns_are_stored(schema) { + return Ok(None); + } + table_schemas.push(schema); + } + + let Some(source_table_index) = table_bindings + .iter() + .position(|binding| identifiers_equal(binding.binding_name(), filter_source_table)) + else { + return Ok(None); + }; + if source_table_index != 0 { + return Ok(None); + } + let Some(source_rowid_column) = row_id_alias_column_name(table_schemas[source_table_index]) + else { + return Ok(None); + }; + if !identifiers_equal(source_rowid_column, filter_source_column) { + return Ok(None); + } + + let mut projections = Vec::with_capacity(select.projection.len()); + let mut column_names = Vec::with_capacity(select.projection.len()); + for (ordinal, item) in select.projection.iter().enumerate() { + let SelectItem::Expr { expr, alias } = item else { + return Ok(None); + }; + let Expr::Column { + table: outer_table, + column: outer_column, + } = expr + else { + return Ok(None); + }; + if outer_table + .as_deref() + .is_some_and(|qualifier| !identifiers_equal(qualifier, view_binding)) + { + return Ok(None); + } + let Some(view_expr) = + view_projection_expr_for_output_column(&view_select.projection, outer_column) + else { + return Ok(None); + }; + let Expr::Column { + table: Some(base_table), + column: base_column, + } = view_expr + else { + return Ok(None); + }; + let Some(table_index) = table_bindings + .iter() + .position(|binding| identifiers_equal(binding.binding_name(), &base_table)) + else { + return Ok(None); + }; + let Some(column_index) = schema_column_index(table_schemas[table_index], &base_column) + else { + return Ok(None); + }; + let is_rowid_alias = + rowid_alias_column_index(table_schemas[table_index]) == Some(column_index); + projections.push(DeferredViewProjection { + table_index, + column_index, + is_rowid_alias, + }); + column_names.push( + alias + .clone() + .unwrap_or_else(|| infer_expr_name(expr, ordinal + 1)), + ); + } + + let mut join_steps = Vec::with_capacity(join_constraints.len()); + for current_table_index in 1..table_bindings.len() { + let Some(step) = self.deferred_view_join_step( + &table_bindings, + &table_schemas, + join_constraints[current_table_index - 1], + current_table_index, + )? + else { + return Ok(None); + }; + join_steps.push(step); + } + let table_projections = + build_deferred_view_table_projections(&table_schemas, &projections, &join_steps); + let projection_indexes = build_deferred_view_projection_indexes( + &projections, + &table_projections, + "deferred view projection", + )?; + let linear_tail_can_move = + deferred_view_linear_tail_projection_can_move(&projection_indexes); + let mut join_keys = Vec::with_capacity(join_steps.len()); + let mut key_projection_indexes = Vec::with_capacity(join_steps.len()); + for step in &join_steps { + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&step.current_index_name) + else { + join_keys.clear(); + key_projection_indexes.clear(); + break; + }; + join_keys.push(keys); + key_projection_indexes.push(join_key_projection_index( + step, + &table_projections[step.previous_table_index], + )?); + } + + let mut table_readers = Vec::with_capacity(table_bindings.len()); + for (binding, schema) in table_bindings.iter().zip(table_schemas.iter()) { + if let Some(source) = self.visible_table_row_source(binding.name) { + table_readers.push(DeferredViewTableRowReader::Source(source)); + continue; + } + let Some(state) = self.persisted_table_state(binding.name) else { + return Ok(None); + }; + let cache = self + .catalog + .table(binding.name) + .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) + .map(|cache| cache.as_ref()); + if !deferred_rowid_lookup_available(state, schema, use_persistent_pk_index, cache) { + return Ok(None); + } + table_readers.push(DeferredViewTableRowReader::Deferred { + state, + schema, + paged_locator_cache: cache, + }); + } + + let source_row_id = match simple_int64_constant_expr_value(value_expr, params)? { + Some(value) => value, + None => { + let filter_value = self.eval_expr( + value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?; + let Value::Int64(value) = filter_value else { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + }; + value + } + }; + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + let mut chunk_payload_cache = HashMap::new(); + let Some(source_row) = table_readers[source_table_index].read_projected_with_chunk_cache( + &store, + source_row_id, + use_persistent_pk_index, + &table_projections[source_table_index].projection_indexes, + &mut chunk_payload_cache, + )? + else { + return Ok(Some(QueryResult::with_rows(column_names, Vec::new()))); + }; + + let mut rows = Vec::with_capacity(64); + let mut join_partial_rows = Vec::with_capacity(join_steps.len() + 1); + if let Some(stopped) = self.stream_deferred_view_linear_three_table_rows_from_root( + &store, + &table_readers, + &join_steps, + &join_keys, + &key_projection_indexes, + &table_projections, + &source_row, + use_persistent_pk_index, + &mut chunk_payload_cache, + &mut |root_row, row1, row2| { + let row = collect_deferred_view_query_row_from_linear_tail( + root_row, + row1, + row2, + &projection_indexes, + "deferred view projection", + linear_tail_can_move, + )?; + rows.push(row); + Ok(false) + }, + )? { + let _ = stopped; + return Ok(Some(QueryResult::with_rows(column_names, rows))); + } + + match self.stream_deferred_view_join_rows_from_root( + &store, + &table_readers, + &join_steps, + &join_keys, + &key_projection_indexes, + &table_projections, + source_row, + &mut join_partial_rows, + use_persistent_pk_index, + false, + &mut chunk_payload_cache, + &mut |partial| { + let values = collect_deferred_view_projection_values( + partial, + &projection_indexes, + "deferred view projection", + )?; + rows.push(QueryRow::new(values)); + Ok(false) + }, + )? { + Some(_) => Ok(Some(QueryResult::with_rows(column_names, rows))), + None => Ok(None), + } + } + pub(crate) fn simple_btree_index_for_table_column( + &self, + table_name: &str, + column_name: &str, + ) -> Option<&IndexSchema> { + self.catalog.indexes.values().find(|index| { + identifiers_equal(&index.table_name, table_name) + && index.fresh + && index.kind == IndexKind::Btree + && index.predicate_sql.is_none() + && index.columns.len() == 1 + && index.columns[0].expression_sql.is_none() + && index.columns[0] + .column_name + .as_deref() + .is_some_and(|indexed_column| identifiers_equal(indexed_column, column_name)) + && matches!(self.index(&index.name), Some(RuntimeIndex::Btree { .. })) + }) + } + pub(crate) fn try_execute_simple_deferred_paged_query( + &self, + query: &Query, + params: &[Value], + pager: &PagerHandle, + wal: &WalHandle, + snapshot_lsn: u64, + use_persistent_pk_index: bool, + ) -> Result> { + if let Some(result) = self.try_execute_simple_deferred_view_projection_limit_query( + query, + params, + pager, + wal, + snapshot_lsn, + use_persistent_pk_index, + )? { + return Ok(Some(result)); + } + if let Some(result) = self.try_execute_simple_deferred_view_filter_projection_query( + query, + params, + pager, + wal, + snapshot_lsn, + use_persistent_pk_index, + )? { + return Ok(Some(result)); + } + if let Some(result) = self.try_execute_simple_deferred_paged_grouped_count_query( + query, + params, + pager, + wal, + snapshot_lsn, + )? { + return Ok(Some(result)); + } + if let Some(result) = self + .try_execute_simple_deferred_paged_grouped_numeric_aggregate_query( + query, + params, + pager, + wal, + snapshot_lsn, + )? + { + return Ok(Some(result)); + } + if let Some(result) = self.try_execute_simple_deferred_rowid_join_projection_query( + query, + params, + pager, + wal, + snapshot_lsn, + use_persistent_pk_index, + )? { + return Ok(Some(result)); + } + if let Some(result) = self.try_execute_simple_deferred_distinct_filtered_projection_query( + query, + params, + pager, + wal, + snapshot_lsn, + )? { + return Ok(Some(result)); + } + if let Some(result) = self.try_execute_simple_deferred_distinct_projection_query( + query, + params, + pager, + wal, + snapshot_lsn, + )? { + return Ok(Some(result)); + } + if let Some(result) = self.try_execute_simple_deferred_filtered_projection_query( + query, + params, + pager, + wal, + snapshot_lsn, + use_persistent_pk_index, + )? { + return Ok(Some(result)); + } + if let Some(result) = self.try_execute_simple_deferred_table_projection_query( + query, + params, + pager, + wal, + snapshot_lsn, + use_persistent_pk_index, + )? { + return Ok(Some(result)); + } + if let Some(result) = self.try_execute_simple_deferred_expression_projection_query( + query, + params, + pager, + wal, + snapshot_lsn, + use_persistent_pk_index, + )? { + return Ok(Some(result)); + } + Ok(None) + } + fn try_execute_simple_deferred_expression_projection_query( + &self, + query: &Query, + params: &[Value], + pager: &PagerHandle, + wal: &WalHandle, + snapshot_lsn: u64, + use_persistent_pk_index: bool, + ) -> Result> { + if !query.ctes.is_empty() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if !select.group_by.is_empty() + || select.having.is_some() + || !select.distinct_on.is_empty() + || select.from.len() != 1 + { + return Ok(None); + } + if select_requires_grouped_evaluation(self, select)? { + return Ok(None); + } + if select.distinct + && (!query.order_by.is_empty() || query.limit.is_some() || query.offset.is_some()) + { + return Ok(None); + } + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(name) + || self.visible_table_row_source(name).is_some() + { + return Ok(None); + } + let table_schema = match self.table_schema(name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + if select + .projection + .iter() + .any(select_item_contains_window_or_subquery) + || select + .filter + .as_ref() + .is_some_and(expr_contains_recursive_unsupported_feature) + || query + .order_by + .iter() + .any(|order| expr_contains_recursive_unsupported_feature(&order.expr)) + { + return Ok(None); + } + if select + .projection + .iter() + .any(select_item_contains_fulltext_function) + || select + .filter + .as_ref() + .is_some_and(expr_contains_fulltext_function) + || query + .order_by + .iter() + .any(|order| expr_contains_fulltext_function(&order.expr)) + { + return Ok(None); + } + let has_expression_projection = select.projection.iter().any(|item| match item { + SelectItem::Expr { expr, .. } => !matches!(expr, Expr::Column { .. }), + SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => true, + }); + if !has_expression_projection + && select.filter.is_none() + && query.order_by.is_empty() + && query.limit.is_none() + && query.offset.is_none() + { + return Ok(None); + } + + let binding_name = alias.as_deref().unwrap_or(name); + let Some(projection_plan) = + simple_expression_projection_plan(table_schema, name, binding_name, &select.projection) + else { + return Ok(None); + }; + let ctes = BTreeMap::new(); + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &ctes)) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &ctes)) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let Some(state) = self.persisted_table_state(name) else { + return Ok(None); + }; + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + let paged_locator_cache = self + .catalog + .table(name) + .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) + .map(|cache| cache.as_ref()); + if deferred_rowid_lookup_available( + state, + table_schema, + use_persistent_pk_index, + paged_locator_cache, + ) { + if let Some(row_ids) = select + .filter + .as_ref() + .map(|filter| { + self.trigram_candidate_row_ids_for_filter(name, alias, filter, params, &ctes) + }) + .transpose()? + .flatten() + { + return Ok(Some( + self.simple_expression_projection_result_from_deferred_row_ids( + &store, + state, + table_schema, + binding_name, + &select.projection, + &projection_plan, + select.filter.as_ref(), + select.distinct, + &query.order_by, + params, + limit, + offset, + &row_ids, + use_persistent_pk_index, + paged_locator_cache, + )?, + )); + } + } + Ok(Some( + self.simple_expression_projection_result_from_persisted_state( + &store, + state, + table_schema, + binding_name, + &select.projection, + &projection_plan, + select.filter.as_ref(), + select.distinct, + &query.order_by, + params, + limit, + offset, + )?, + )) + } + fn try_execute_simple_deferred_distinct_projection_query( + &self, + query: &Query, + params: &[Value], + pager: &PagerHandle, + wal: &WalHandle, + snapshot_lsn: u64, + ) -> Result> { + if !query.ctes.is_empty() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.filter.is_some() + || !select.group_by.is_empty() + || select.having.is_some() + || !select.distinct + || !select.distinct_on.is_empty() + || select.from.len() != 1 + { + return Ok(None); + } + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(name) + || self.visible_table_row_source(name).is_some() + { + return Ok(None); + } + let table_schema = match self.table_schema(name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + let Some((projection_indexes, column_names)) = + self.simple_projection_plan(select, name, alias, table_schema) + else { + return Ok(None); + }; + let order_by = self.simple_projection_order_by_plan( + query, + table_schema, + name, + alias.as_deref().unwrap_or(name), + &projection_indexes, + )?; + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let Some(state) = self.persisted_table_state(name) else { + return Ok(None); + }; + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + Ok(Some( + self.simple_distinct_projection_result_from_persisted_state( + &store, + state, + &projection_indexes, + column_names, + order_by, + limit, + offset, + )?, + )) + } + fn try_execute_simple_deferred_table_projection_query( + &self, + query: &Query, + params: &[Value], + pager: &PagerHandle, + wal: &WalHandle, + snapshot_lsn: u64, + use_persistent_pk_index: bool, + ) -> Result> { + if !query.ctes.is_empty() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.filter.is_some() + || !select.group_by.is_empty() + || select.having.is_some() + || select.distinct + || select.from.len() != 1 + { + return Ok(None); + } + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(name) + || self.visible_table_row_source(name).is_some() + { + return Ok(None); + } + let table_schema = match self.table_schema(name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + let Some((projection_indexes, column_names)) = + self.simple_projection_plan(select, name, alias, table_schema) + else { + return Ok(None); + }; + let order_by = self.simple_projection_order_by_plan( + query, + table_schema, + name, + alias.as_deref().unwrap_or(name), + &projection_indexes, + )?; + let row_id_order = if query.order_by.len() == 1 { + if let Expr::Column { + table: order_table, + column: order_column, + } = &query.order_by[0].expr + { + if order_table.as_deref().is_some_and(|qualifier| { + !matches_table_binding(TableBindingRef { name, alias }, Some(qualifier)) + }) { + None + } else if let Some(filter_column_index) = + schema_column_index(table_schema, order_column) + { + if table_schema + .primary_key_columns + .iter() + .any(|column| identifiers_equal(column, order_column)) + && table_schema.columns[filter_column_index].column_type + == crate::catalog::ColumnType::Int64 + { + Some((order_column.as_str(), query.order_by[0].descending)) + } else { + None + } + } else { + None + } + } else { + None + } + } else { + None + }; + + if !query.order_by.is_empty() && order_by.is_none() && row_id_order.is_none() { + return Ok(None); + } + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let Some(state) = self.persisted_table_state(name) else { + return Ok(None); + }; + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + let paged_locator_cache = self + .catalog + .table(name) + .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) + .map(|cache| cache.as_ref()); + if let Some((filter_column, descending)) = row_id_order { + if limit != Some(0) && use_persistent_pk_index { + if let Some(result) = try_persistent_pk_ordered_projection_result( + &store, + state, + table_schema, + &projection_indexes, + column_names.clone(), + limit, + offset, + descending, + )? { + return Ok(Some(result)); + } + } + if limit != Some(0) { + if let Some(row_ids) = self.ordered_runtime_btree_row_ids( + name, + filter_column, + limit, + offset, + descending, + )? { + let mut rows = Vec::with_capacity(row_ids.len().min(64)); + for row_id in row_ids { + if let Some(values) = read_deferred_projected_values_by_id( + &store, + state, + table_schema, + row_id, + use_persistent_pk_index, + paged_locator_cache, + &projection_indexes, + )? { + rows.push(QueryRow::new(values)); + } + } + return Ok(Some(QueryResult::with_rows(column_names, rows))); + } + } + } + let mut unbounded_lower_bound = None; + if let Some(cache) = paged_locator_cache.filter(|cache| cache.matches_state(state)) { + if let Some(min_row_id) = cache.min_row_id() { + unbounded_lower_bound = Some(SimpleRangeBoundValue { + inclusive: true, + value: Value::Int64(min_row_id), + }); + } + } + if unbounded_lower_bound.is_none() && use_persistent_pk_index { + if let Some(min_row_id) = first_persistent_pk_row_id(&store, table_schema)? { + unbounded_lower_bound = Some(SimpleRangeBoundValue { + inclusive: true, + value: Value::Int64(min_row_id), + }); + } + } + if limit.is_some() && limit != Some(0) { + if let Some((filter_column, _descending)) = row_id_order { + if let Some(result) = self.try_simple_deferred_rowid_range_projection_result( + &store, + state, + table_schema, + TableBindingRef { name, alias }, + filter_column, + unbounded_lower_bound.as_ref(), + None, + &projection_indexes, + column_names.clone(), + &query.order_by, + limit, + offset, + use_persistent_pk_index, + paged_locator_cache, + )? { + return Ok(Some(result)); + } + } + } + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + Ok(Some(self.simple_projection_result_from_persisted_state( + &store, + state, + &projection_indexes, + column_names, + order_by, + limit, + offset, + )?)) + } + fn try_execute_simple_deferred_distinct_filtered_projection_query( + &self, + query: &Query, + params: &[Value], + pager: &PagerHandle, + wal: &WalHandle, + snapshot_lsn: u64, + ) -> Result> { + if !query.ctes.is_empty() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if !select.group_by.is_empty() + || select.having.is_some() + || !select.distinct + || !select.distinct_on.is_empty() + || select.from.len() != 1 + { + return Ok(None); + } + let Some(filter) = select.filter.as_ref() else { + return Ok(None); + }; + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(name) + || self.visible_table_row_source(name).is_some() + { + return Ok(None); + } + + let table_schema = match self.table_schema(name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + let Some((projection_indexes, column_names)) = + self.simple_projection_plan(select, name, alias, table_schema) + else { + return Ok(None); + }; + let binding_name = alias.as_deref().unwrap_or(name); + let Some(range_filter) = simple_range_projection_filter(filter) else { + return Ok(None); + }; + let filter_table = range_filter.table; + let filter_column = range_filter.column; + let lower_bound = range_filter.lower; + let upper_bound = range_filter.upper; + if let Some(table_name) = filter_table { + if !identifiers_equal(table_name, name) && !identifiers_equal(table_name, binding_name) + { + return Ok(None); + } + } + let filter_column_index = table_schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, filter_column)) + .ok_or_else(|| { + DbError::internal(format!( + "simple deferred filtered distinct projection column {filter_column} missing from {name}" + )) + })?; + let lower_bound = lower_bound + .map(|bound| { + Ok(SimpleRangeBoundValue { + inclusive: bound.inclusive, + value: self.eval_expr( + bound.value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?, + }) + }) + .transpose()?; + let upper_bound = upper_bound + .map(|bound| { + Ok(SimpleRangeBoundValue { + inclusive: bound.inclusive, + value: self.eval_expr( + bound.value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?, + }) + }) + .transpose()?; + if !range_filter.residual.is_empty() { + // The deferred distinct filtered fast path does not yet evaluate + // residual predicates; bail to the generic executor to preserve + // correctness. + return Ok(None); + } + let order_by = self.simple_projection_order_by_plan( + query, + table_schema, + name, + binding_name, + &projection_indexes, + )?; + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let Some(state) = self.persisted_table_state(name) else { + return Ok(None); + }; + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + Ok(Some( + self.simple_distinct_filtered_projection_result_from_persisted_state( + &store, + state, + filter_column_index, + lower_bound.as_ref(), + upper_bound.as_ref(), + &projection_indexes, + column_names, + order_by, + limit, + offset, + )?, + )) + } + fn try_execute_simple_deferred_filtered_projection_query( + &self, + query: &Query, + params: &[Value], + pager: &PagerHandle, + wal: &WalHandle, + snapshot_lsn: u64, + use_persistent_pk_index: bool, + ) -> Result> { + if !query.ctes.is_empty() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if !select.group_by.is_empty() + || select.having.is_some() + || select.distinct + || !select.distinct_on.is_empty() + || select.from.len() != 1 + { + return Ok(None); + } + let Some(filter) = select.filter.as_ref() else { + return Ok(None); + }; + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(name) + || self.visible_table_row_source(name).is_some() + { + return Ok(None); + } + + let table_schema = match self.table_schema(name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + let Some((projection_indexes, column_names)) = + self.simple_projection_plan(select, name, alias, table_schema) + else { + return Ok(None); + }; + let binding_name = alias.as_deref().unwrap_or(name); + + let Some(range_filter) = simple_range_projection_filter(filter) else { + return Ok(None); + }; + let filter_table = range_filter.table; + let filter_column = range_filter.column; + let lower_bound = range_filter.lower; + let upper_bound = range_filter.upper; + if let Some(table_name) = filter_table { + if !identifiers_equal(table_name, name) && !identifiers_equal(table_name, binding_name) + { + return Ok(None); + } + } + let filter_column_index = table_schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, filter_column)) + .ok_or_else(|| { + DbError::internal(format!( + "simple filtered projection column {filter_column} missing from {name}" + )) + })?; + + let lower_bound = lower_bound + .map(|bound| { + Ok(SimpleRangeBoundValue { + inclusive: bound.inclusive, + value: self.eval_expr( + bound.value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?, + }) + }) + .transpose()?; + let upper_bound = upper_bound + .map(|bound| { + Ok(SimpleRangeBoundValue { + inclusive: bound.inclusive, + value: self.eval_expr( + bound.value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?, + }) + }) + .transpose()?; + + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + let Some(state) = self.persisted_table_state(name) else { + return Ok(None); + }; + let store = SnapshotPageStore { + pager, + wal, + snapshot_lsn, + }; + let paged_locator_cache = self + .catalog + .table(name) + .and_then(|table| self.deferred_paged_row_locator_caches.get(&table.name)) + .map(|cache| cache.as_ref()); + if range_filter.residual.is_empty() { + if let Some(result) = self.try_simple_deferred_rowid_range_projection_result( + &store, + state, + table_schema, + TableBindingRef { name, alias }, + filter_column, + lower_bound.as_ref(), + upper_bound.as_ref(), + &projection_indexes, + column_names.clone(), + &query.order_by, + limit, + offset, + use_persistent_pk_index, + paged_locator_cache, + )? { + return Ok(Some(result)); + } + } + + let order_by = self.simple_projection_order_by_plan( + query, + table_schema, + name, + binding_name, + &projection_indexes, + )?; + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + let residual_plans = self.build_simple_residual_plans( + table_schema, + name, + binding_name, + &range_filter.residual, + params, + )?; + if residual_plans.len() != range_filter.residual.len() { + return Ok(None); + } + Ok(Some( + self.simple_filtered_projection_result_from_persisted_state( + &store, + state, + filter_column_index, + lower_bound.as_ref(), + upper_bound.as_ref(), + &residual_plans, + &projection_indexes, + column_names, + order_by, + limit, + offset, + )?, + )) + } + fn analyze_simple_indexed_projection_query<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result>> { + if !query.ctes.is_empty() { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if !select.group_by.is_empty() + || select.having.is_some() + || select.distinct + || select.from.len() != 1 + { + return Ok(None); + } + let Some(filter) = select.filter.as_ref() else { + return Ok(None); + }; + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(name) + { + return Ok(None); + } + + let table_schema = match self.table_schema(name) { + Some(table) => table, + None => return Ok(None), + }; + if !generated_columns_are_stored(table_schema) { + return Ok(None); + } + let binding_name = alias.as_deref().unwrap_or(name); + let Some(lookup_terms) = simple_btree_lookup_terms(filter) else { + return Ok(None); + }; + for (filter_table, _, _) in &lookup_terms { + if filter_table.as_ref().is_some_and(|table_name| { + !identifiers_equal(table_name, name) && !identifiers_equal(table_name, binding_name) + }) { + return Ok(None); + } + } + let ordered_lookup_terms = if lookup_terms.len() == 1 { + lookup_terms + } else { + let Some(index) = + self.compound_btree_index_for_lookup_terms(name, lookup_terms.as_slice()) + else { + return Ok(None); + }; + ordered_lookup_terms_for_index(index, lookup_terms.as_slice())? + }; + + let mut lookup_values = Vec::with_capacity(ordered_lookup_terms.len()); + for (_, _, value_expr) in &ordered_lookup_terms { + lookup_values.push(self.eval_expr( + value_expr, + &Dataset::empty(), + &[], + params, + &BTreeMap::new(), + None, + )?); + } + let filter_column = ordered_lookup_terms[0].1; + let lookup_value = lookup_values + .first() + .cloned() + .ok_or_else(|| DbError::internal("indexed projection lookup terms are empty"))?; + let extra_lookup_terms = ordered_lookup_terms + .iter() + .skip(1) + .zip(lookup_values.into_iter().skip(1)) + .map(|((_, column, _), value)| (*column, value)) + .collect::>(); + let Some((projection_indexes, column_names)) = + self.simple_projection_plan(select, name, alias, table_schema) + else { + return Ok(None); + }; + let order_by = self.simple_projection_order_by_plan( + query, + table_schema, + name, + binding_name, + &projection_indexes, + )?; + if !query.order_by.is_empty() && order_by.is_none() { + return Ok(None); + } + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + let offset = query + .offset + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)) + .unwrap_or(0); + + Ok(Some(SimpleIndexedProjectionPlan { + table_name: name, + table_schema, + filter_column, + lookup_value, + extra_lookup_terms, + projection_indexes, + column_names, + order_by, + limit, + offset, + })) + } + pub(crate) fn simple_indexed_projection_missing_runtime_btree<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_simple_indexed_projection_query(query, params)? else { + return Ok(None); + }; + if plan.extra_lookup_terms.is_empty() + && row_id_alias_column_name(plan.table_schema) + .is_some_and(|column_name| identifiers_equal(column_name, plan.filter_column)) + { + return Ok(None); + } + let Some(index) = self.btree_index_for_simple_indexed_projection_plan(&plan) else { + return Ok(None); + }; + if matches!(self.index(&index.name), Some(RuntimeIndex::Btree { .. })) { + Ok(None) + } else { + Ok(Some((plan.table_name, index.name.as_str()))) + } + } + pub(crate) fn simple_indexed_projection_missing_persistent_pk_root<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result> { + let Some(plan) = self.analyze_simple_indexed_projection_query(query, params)? else { + return Ok(None); + }; + if !plan.extra_lookup_terms.is_empty() || plan.table_schema.pk_index_root.is_some() { + return Ok(None); + } + let Some(filter_column_index) = schema_column_index(plan.table_schema, plan.filter_column) + else { + return Ok(None); + }; + if plan.table_schema.columns[filter_column_index].column_type != ColumnType::Int64 + || !plan + .table_schema + .primary_key_columns + .iter() + .any(|column| identifiers_equal(column, plan.filter_column)) + { + return Ok(None); + } + if self + .persisted_table_state(plan.table_name) + .is_some_and(|state| state.pointer.head_page_id != 0) + { + Ok(Some(plan.table_name)) + } else { + Ok(None) + } + } + pub(crate) fn simple_ordered_projection_missing_persistent_pk_root<'a>( + &'a self, + query: &'a Query, + params: &[Value], + ) -> Result> { + if !query.ctes.is_empty() || query.order_by.len() != 1 || query.order_by[0].descending { + return Ok(None); + } + let QueryBody::Select(select) = &query.body else { + return Ok(None); + }; + if select.filter.is_some() + || !select.group_by.is_empty() + || select.having.is_some() + || select.distinct + || projection_has_aggregate_items(&select.projection) + || select.from.len() != 1 + { + return Ok(None); + } + let FromItem::Table { name, alias } = &select.from[0] else { + return Ok(None); + }; + if self + .visible_view(name, NameResolutionScope::Session) + .is_some() + || self.visible_table_is_temporary(name) + || self.visible_table_row_source(name).is_some() + { + return Ok(None); + } + let Some(table_schema) = self.table_schema(name) else { + return Ok(None); + }; + if table_schema.pk_index_root.is_some() || !generated_columns_are_stored(table_schema) { + return Ok(None); + } + let Some((_projection_indexes, _)) = + self.simple_projection_plan(select, name, alias, table_schema) + else { + return Ok(None); + }; + let Expr::Column { + table: order_table, + column: order_column, + } = &query.order_by[0].expr + else { + return Ok(None); + }; + if order_table.as_deref().is_some_and(|qualifier| { + !matches_table_binding(TableBindingRef { name, alias }, Some(qualifier)) + }) { + return Ok(None); + } + let Some(order_column_index) = schema_column_index(table_schema, order_column) else { + return Ok(None); + }; + if table_schema.columns[order_column_index].column_type != ColumnType::Int64 + || !table_schema + .primary_key_columns + .iter() + .any(|column| identifiers_equal(column, order_column)) + { + return Ok(None); + } + let limit = query + .limit + .as_ref() + .map(|expr| self.eval_constant_i64(expr, params, &BTreeMap::new())) + .transpose()? + .map(|value| usize::try_from(value.max(0)).unwrap_or(usize::MAX)); + if limit == Some(0) { + return Ok(None); + } + if self + .persisted_table_state(name) + .is_some_and(|state| state.pointer.head_page_id != 0) + { + Ok(Some(name)) + } else { + Ok(None) + } + } + pub(crate) fn single_column_btree_index( + &self, + table_name: &str, + column_name: &str, + ) -> Option<&IndexSchema> { + self.catalog.indexes.values().find(|index| { + identifiers_equal(&index.table_name, table_name) + && index.fresh + && index.kind == IndexKind::Btree + && index.predicate_sql.is_none() + && index.columns.len() == 1 + && index.columns[0] + .column_name + .as_deref() + .is_some_and(|index_column| identifiers_equal(index_column, column_name)) + && index.columns[0].expression_sql.is_none() + }) + } + fn compound_btree_index_for_lookup_terms( + &self, + table_name: &str, + lookup_terms: &[(Option<&str>, &str, &Expr)], + ) -> Option<&IndexSchema> { + if lookup_terms.len() < 2 { + return None; + } + self.catalog.indexes.values().find(|index| { + identifiers_equal(&index.table_name, table_name) + && index.fresh + && index.kind == IndexKind::Btree + && index.predicate_sql.is_none() + && index.columns.len() >= lookup_terms.len() + && index + .columns + .iter() + .take(lookup_terms.len()) + .all(|index_column| { + index_column.expression_sql.is_none() + && index_column.column_name.as_deref().is_some_and(|column| { + lookup_terms.iter().any(|(_, lookup_column, _)| { + identifiers_equal(column, lookup_column) + }) + }) + }) + }) + } + fn btree_index_for_simple_indexed_projection_plan( + &self, + plan: &SimpleIndexedProjectionPlan<'_>, + ) -> Option<&IndexSchema> { + if plan.extra_lookup_terms.is_empty() { + return self.single_column_btree_index(plan.table_name, plan.filter_column); + } + let lookup_columns = std::iter::once(plan.filter_column) + .chain(plan.extra_lookup_terms.iter().map(|(column, _)| *column)) + .collect::>(); + self.catalog.indexes.values().find(|index| { + identifiers_equal(&index.table_name, plan.table_name) + && index.fresh + && index.kind == IndexKind::Btree + && index.predicate_sql.is_none() + && index.columns.len() >= lookup_columns.len() + && index + .columns + .iter() + .take(lookup_columns.len()) + .zip(lookup_columns.iter()) + .all(|(index_column, lookup_column)| { + index_column.expression_sql.is_none() + && index_column + .column_name + .as_deref() + .is_some_and(|index_column| { + identifiers_equal(index_column, lookup_column) + }) + }) + }) + } + fn ordered_runtime_btree_row_ids( + &self, + table_name: &str, + column_name: &str, + limit: Option, + offset: usize, + descending: bool, + ) -> Result>> { + let Some(index) = self.single_column_btree_index(table_name, column_name) else { + return Ok(None); + }; + let Some(RuntimeIndex::Btree { keys, .. }) = self.index(&index.name) else { + return Ok(None); + }; + let take = limit.unwrap_or(usize::MAX); + if take == 0 { + return Ok(Some(Vec::new())); + } + match keys { + RuntimeBtreeKeys::UniqueInt64(entries, deleted) => { + let mut ordered = entries + .iter() + .filter(|(_, row_id)| !deleted.contains(row_id)) + .collect::>(); + let window = offset.saturating_add(take).min(ordered.len()); + if window == 0 { + return Ok(Some(Vec::new())); + } + if window < ordered.len() { + if descending { + ordered + .select_nth_unstable_by(window - 1, |left, right| right.0.cmp(&left.0)); + ordered.truncate(window); + ordered.sort_unstable_by_key(|(key, _)| std::cmp::Reverse(*key)); + } else { + ordered.select_nth_unstable_by_key(window - 1, |(key, _)| *key); + ordered.truncate(window); + ordered.sort_unstable_by_key(|(key, _)| *key); + } + } else if descending { + ordered.sort_unstable_by_key(|(key, _)| std::cmp::Reverse(*key)); + } else { + ordered.sort_unstable_by_key(|(key, _)| *key); + } + Ok(Some( + ordered + .into_iter() + .skip(offset) + .take(take) + .map(|(_, row_id)| row_id) + .collect(), + )) + } + RuntimeBtreeKeys::NonUniqueInt64(entries, deleted) => { + let mut ordered = entries + .iter() + .map(|(key, row_ids)| (key, row_ids.to_vec())) + .collect::>(); + ordered.sort_unstable_by_key(|(key, _)| *key); + let mut skipped = 0usize; + let mut row_ids = Vec::with_capacity(take.min(64)); + let ordered = if descending { + ordered.into_iter().rev().collect::>() + } else { + ordered + }; + for (_, mut ids) in ordered { + ids.sort_unstable(); + for row_id in ids { + if deleted.contains(&row_id) { + continue; + } + if skipped < offset { + skipped += 1; + continue; + } + row_ids.push(row_id); + if row_ids.len() == take { + return Ok(Some(row_ids)); + } + } + } + Ok(Some(row_ids)) + } + RuntimeBtreeKeys::UniqueEncoded(..) + | RuntimeBtreeKeys::NonUniqueEncoded(..) + | RuntimeBtreeKeys::UniqueUuid(..) + | RuntimeBtreeKeys::NonUniqueUuid(..) => Ok(None), + } + } + pub(crate) fn simple_projection_plan( + &self, + select: &Select, + table_name: &str, + table_alias: &Option, + table_schema: &TableSchema, + ) -> Option<(Vec, Vec)> { + let binding_name = table_alias.as_deref().unwrap_or(table_name); + let mut projection_indexes = Vec::with_capacity(select.projection.len()); + let mut column_names = Vec::with_capacity(select.projection.len()); + + for item in &select.projection { + match item { + SelectItem::Expr { + expr, + alias: select_alias, + } => { + let Expr::Column { + table: table_name_expr, + column, + } = expr + else { + return None; + }; + if let Some(projection_table) = table_name_expr.as_deref() { + if !identifiers_equal(projection_table, table_name) + && !identifiers_equal(projection_table, binding_name) + { + return None; + } + } + let column_index = table_schema + .columns + .iter() + .position(|candidate| identifiers_equal(&candidate.name, column))?; + projection_indexes.push(column_index); + column_names.push(select_alias.clone().unwrap_or_else(|| column.clone())); + } + SelectItem::Wildcard => { + for (column_index, column) in table_schema.columns.iter().enumerate() { + projection_indexes.push(column_index); + column_names.push(column.name.clone()); + } + } + SelectItem::QualifiedWildcard(qualified_name) => { + if !identifiers_equal(qualified_name, table_name) + && !identifiers_equal(qualified_name, binding_name) + { + return None; + } + for (column_index, column) in table_schema.columns.iter().enumerate() { + projection_indexes.push(column_index); + column_names.push(column.name.clone()); + } + } + } + } + + Some((projection_indexes, column_names)) + } + pub(crate) fn simple_projection_order_by_plan( + &self, + query: &Query, + table_schema: &TableSchema, + table_name: &str, + binding_name: &str, + projection_indexes: &[usize], + ) -> Result>> { + if query.order_by.is_empty() { + return Ok(None); + } + let order_by = query + .order_by + .iter() + .map(|entry| { + let Expr::Column { + table: order_table, + column: order_column, + } = &entry.expr + else { + return None; + }; + if let Some(order_table) = order_table.as_deref() { + if !identifiers_equal(order_table, table_name) + && !identifiers_equal(order_table, binding_name) + { + return None; + } + } + let order_projection_index = + projection_indexes.iter().position(|projection_index| { + table_schema.columns[*projection_index] + .name + .as_str() + .eq_ignore_ascii_case(order_column) + })?; + Some(SimpleOrderByPlan { + projection_index: order_projection_index, + descending: entry.descending, + collation: entry.collation.clone(), + }) + }) + .collect::>>(); + Ok(order_by) + } + fn simple_grouped_order_by_plan( + &self, + query: &Query, + select: &Select, + table_name: &str, + binding_name: &str, + column_names: &[String], + ) -> Result>> { + if query.order_by.is_empty() { + return Ok(None); + } + let order_by = query + .order_by + .iter() + .map(|entry| { + let Expr::Column { + table: order_table, + column: order_column, + } = &entry.expr + else { + return None; + }; + let projection_index = if let Some(order_table) = order_table.as_deref() { + if !identifiers_equal(order_table, table_name) + && !identifiers_equal(order_table, binding_name) + { + return None; + } + select + .group_by + .iter() + .enumerate() + .find_map(|(index, expr)| match expr { + Expr::Column { column, .. } + if identifiers_equal(column, order_column) => + { + Some(index) + } + _ => None, + }) + } else { + column_names + .iter() + .position(|candidate| candidate.eq_ignore_ascii_case(order_column)) + .or_else(|| { + select.group_by.iter().enumerate().find_map( + |(index, expr)| match expr { + Expr::Column { column, .. } + if identifiers_equal(column, order_column) => + { + Some(index) + } + _ => None, + }, + ) + }) + }?; + Some(SimpleOrderByPlan { + projection_index, + descending: entry.descending, + collation: entry.collation.clone(), + }) + }) + .collect::>>(); + Ok(order_by) + } + #[allow(clippy::too_many_arguments)] + fn rewrite_simple_grouped_having_expr( + &self, + expr: &Expr, + select: &Select, + table_name: &str, + binding_name: &str, + column_names: &[String], + synthetic_names: &[String], + count_projection_index: usize, + sum_projection: Option<(&str, usize)>, + ) -> Result> { + let mut aggregate_bindings = vec![SimpleGroupedNumericAggregateBinding { + kind: SimpleGroupedNumericAggregateKind::CountRows, + projection_index: count_projection_index, + source_column_name: None, + source_column_index: None, + source_expr: None, + }]; + if let Some((sum_column_name, sum_projection_index)) = sum_projection { + aggregate_bindings.push(SimpleGroupedNumericAggregateBinding { + kind: SimpleGroupedNumericAggregateKind::Sum, + projection_index: sum_projection_index, + source_column_name: Some(sum_column_name.to_string()), + source_column_index: None, + source_expr: None, + }); + } + self.rewrite_simple_grouped_having_expr_with_bindings( + expr, + select, + table_name, + binding_name, + column_names, + synthetic_names, + &aggregate_bindings, + ) + } + #[allow(clippy::too_many_arguments)] + fn rewrite_simple_grouped_having_expr_with_bindings( + &self, + expr: &Expr, + select: &Select, + table_name: &str, + binding_name: &str, + column_names: &[String], + synthetic_names: &[String], + aggregate_bindings: &[SimpleGroupedNumericAggregateBinding], + ) -> Result> { + let rewritten = match expr { + Expr::Literal(_) | Expr::Parameter(_) => expr.clone(), + Expr::Column { table, column } => { + let Some(column_name) = simple_grouped_having_column_name( + select, + table_name, + binding_name, + column_names, + synthetic_names, + table.as_deref(), + column, + ) else { + return Ok(None); + }; + Expr::Column { + table: None, + column: column_name, + } + } + Expr::Unary { op, expr } => Expr::Unary { + op: *op, + expr: Box::new( + match self.rewrite_simple_grouped_having_expr_with_bindings( + expr, + select, + table_name, + binding_name, + column_names, + synthetic_names, + aggregate_bindings, + )? { + Some(expr) => expr, + None => return Ok(None), + }, + ), + }, + Expr::Binary { left, op, right } => Expr::Binary { + left: Box::new( + match self.rewrite_simple_grouped_having_expr_with_bindings( + left, + select, + table_name, + binding_name, + column_names, + synthetic_names, + aggregate_bindings, + )? { + Some(expr) => expr, + None => return Ok(None), + }, + ), + op: *op, + right: Box::new( + match self.rewrite_simple_grouped_having_expr_with_bindings( + right, + select, + table_name, + binding_name, + column_names, + synthetic_names, + aggregate_bindings, + )? { + Some(expr) => expr, + None => return Ok(None), + }, + ), + }, + Expr::Between { + expr, + low, + high, + negated, + } => Expr::Between { + expr: Box::new( + match self.rewrite_simple_grouped_having_expr_with_bindings( + expr, + select, + table_name, + binding_name, + column_names, + synthetic_names, + aggregate_bindings, + )? { + Some(expr) => expr, + None => return Ok(None), + }, + ), + low: Box::new( + match self.rewrite_simple_grouped_having_expr_with_bindings( + low, + select, + table_name, + binding_name, + column_names, + synthetic_names, + aggregate_bindings, + )? { + Some(expr) => expr, + None => return Ok(None), + }, + ), + high: Box::new( + match self.rewrite_simple_grouped_having_expr_with_bindings( + high, + select, + table_name, + binding_name, + column_names, + synthetic_names, + aggregate_bindings, + )? { + Some(expr) => expr, + None => return Ok(None), + }, + ), + negated: *negated, + }, + Expr::InList { + expr, + items, + negated, + } => Expr::InList { + expr: Box::new( + match self.rewrite_simple_grouped_having_expr_with_bindings( + expr, + select, + table_name, + binding_name, + column_names, + synthetic_names, + aggregate_bindings, + )? { + Some(expr) => expr, + None => return Ok(None), + }, + ), + items: { + let mut rewritten_items = Vec::with_capacity(items.len()); + for item in items { + let Some(item) = self.rewrite_simple_grouped_having_expr_with_bindings( + item, + select, + table_name, + binding_name, + column_names, + synthetic_names, + aggregate_bindings, + )? + else { + return Ok(None); + }; + rewritten_items.push(item); + } + rewritten_items + }, + negated: *negated, + }, + Expr::Like { + expr, + pattern, + escape, + case_insensitive, + negated, + } => Expr::Like { + expr: Box::new( + match self.rewrite_simple_grouped_having_expr_with_bindings( + expr, + select, + table_name, + binding_name, + column_names, + synthetic_names, + aggregate_bindings, + )? { + Some(expr) => expr, + None => return Ok(None), + }, + ), + pattern: Box::new( + match self.rewrite_simple_grouped_having_expr_with_bindings( + pattern, + select, + table_name, + binding_name, + column_names, + synthetic_names, + aggregate_bindings, + )? { + Some(expr) => expr, + None => return Ok(None), + }, + ), + escape: match escape { + Some(escape) => Some(Box::new( + match self.rewrite_simple_grouped_having_expr_with_bindings( + escape, + select, + table_name, + binding_name, + column_names, + synthetic_names, + aggregate_bindings, + )? { + Some(expr) => expr, + None => return Ok(None), + }, + )), + None => None, + }, + case_insensitive: *case_insensitive, + negated: *negated, + }, + Expr::IsNull { expr, negated } => Expr::IsNull { + expr: Box::new( + match self.rewrite_simple_grouped_having_expr_with_bindings( + expr, + select, + table_name, + binding_name, + column_names, + synthetic_names, + aggregate_bindings, + )? { + Some(expr) => expr, + None => return Ok(None), + }, + ), + negated: *negated, + }, + Expr::Function { name, args } => Expr::Function { + name: name.clone(), + args: { + let mut rewritten_args = Vec::with_capacity(args.len()); + for arg in args { + let Some(arg) = self.rewrite_simple_grouped_having_expr_with_bindings( + arg, + select, + table_name, + binding_name, + column_names, + synthetic_names, + aggregate_bindings, + )? + else { + return Ok(None); + }; + rewritten_args.push(arg); + } + rewritten_args + }, + }, + Expr::Aggregate { .. } => { + let Some(binding) = matching_simple_grouped_aggregate_binding( + expr, + table_name, + binding_name, + aggregate_bindings, + ) else { + return Ok(None); + }; + Expr::Column { + table: None, + column: synthetic_names[binding.projection_index].clone(), + } + } + _ => return Ok(None), + }; + Ok(Some(rewritten)) + } + pub(crate) fn try_execute_simple_integer_series_query(query: &Query) -> Option { + if !query.recursive + || query.ctes.len() != 1 + || !query.order_by.is_empty() + || query.limit.is_some() + || query.offset.is_some() + { + return None; + } + let cte = query.ctes.first()?; + let (column_name, start, step, upper_exclusive) = Self::simple_integer_series_bounds(cte)?; + + let QueryBody::Select(select) = &query.body else { + return None; + }; + if select.from.len() != 1 + || select.filter.is_some() + || !select.group_by.is_empty() + || select.having.is_some() + || select.distinct + || !select.distinct_on.is_empty() + { + return None; + } + let [FromItem::Table { name, alias }] = select.from.as_slice() else { + return None; + }; + if !identifiers_equal(name, &cte.name) { + return None; + } + let binding_name = alias.as_deref().unwrap_or(name); + let [SelectItem::Expr { expr, alias }] = select.projection.as_slice() else { + return None; + }; + if !Self::simple_integer_series_column_ref(expr, binding_name, &column_name) { + return None; + } + + let max_rows = upper_exclusive + .checked_sub(start)? + .checked_div(step)? + .checked_add(1)?; + let capacity = usize::try_from(max_rows) + .ok() + .filter(|rows| *rows <= RECURSIVE_CTE_MAX_ITERATIONS)?; + let mut rows = Vec::with_capacity(capacity); + let mut value = start; + rows.push(QueryRow::new(vec![Value::Int64(value)])); + while value < upper_exclusive { + value = value.checked_add(step)?; + rows.push(QueryRow::new(vec![Value::Int64(value)])); + if rows.len() > RECURSIVE_CTE_MAX_ITERATIONS { + return None; + } + } + + Some(QueryResult::with_rows( + vec![alias.clone().unwrap_or(column_name)], + rows, + )) + } + pub(crate) fn simple_integer_series_bounds( + cte: &CommonTableExpr, + ) -> Option<(String, i64, i64, i64)> { + if cte.column_names.len() != 1 + || !cte.query.recursive + || !cte.query.order_by.is_empty() + || cte.query.limit.is_some() + || cte.query.offset.is_some() + { + return None; + } + let QueryBody::SetOperation { + op: crate::sql::ast::SetOperation::Union, + all: true, + left, + right, + } = &cte.query.body + else { + return None; + }; + + let start = Self::simple_integer_series_anchor(left)?; + let (step, upper_exclusive) = + Self::simple_integer_series_recursive_term(right, &cte.name, &cte.column_names[0])?; + if step <= 0 { + return None; + } + Some((cte.column_names[0].clone(), start, step, upper_exclusive)) + } + fn simple_integer_series_anchor(body: &QueryBody) -> Option { + let QueryBody::Select(select) = body else { + return None; + }; + if !select.from.is_empty() + || select.filter.is_some() + || !select.group_by.is_empty() + || select.having.is_some() + || select.distinct + || !select.distinct_on.is_empty() + { + return None; + } + let [SelectItem::Expr { expr, .. }] = select.projection.as_slice() else { + return None; + }; + let Expr::Literal(Value::Int64(value)) = expr else { + return None; + }; + Some(*value) + } + fn simple_integer_series_recursive_term( + body: &QueryBody, + cte_name: &str, + column_name: &str, + ) -> Option<(i64, i64)> { + let QueryBody::Select(select) = body else { + return None; + }; + if select.from.len() != 1 + || !select.group_by.is_empty() + || select.having.is_some() + || select.distinct + || !select.distinct_on.is_empty() + { + return None; + } + let [FromItem::Table { name, alias }] = select.from.as_slice() else { + return None; + }; + if !identifiers_equal(name, cte_name) { + return None; + } + let binding_name = alias.as_deref().unwrap_or(name); + + let [SelectItem::Expr { expr, .. }] = select.projection.as_slice() else { + return None; + }; + let step = match expr { + Expr::Binary { left, op, right } if *op == BinaryOp::Add => { + if Self::simple_integer_series_column_ref(left, binding_name, column_name) { + Self::simple_int64_literal(right)? + } else if Self::simple_integer_series_column_ref(right, binding_name, column_name) { + Self::simple_int64_literal(left)? + } else { + return None; + } + } + _ => return None, + }; + + let filter = select.filter.as_ref()?; + let upper_exclusive = match filter { + Expr::Binary { left, op, right } if *op == BinaryOp::Lt => { + if !Self::simple_integer_series_column_ref(left, binding_name, column_name) { + return None; + } + Self::simple_int64_literal(right)? + } + _ => return None, + }; + Some((step, upper_exclusive)) + } + fn simple_integer_series_column_ref(expr: &Expr, table_name: &str, column_name: &str) -> bool { + matches!( + expr, + Expr::Column { table, column } + if identifiers_equal(column, column_name) + && table + .as_deref() + .is_none_or(|candidate| identifiers_equal(candidate, table_name)) + ) + } + fn simple_int64_literal(expr: &Expr) -> Option { + match expr { + Expr::Literal(Value::Int64(value)) => Some(*value), + _ => None, + } + } +} diff --git a/crates/decentdb/src/exec/table_data.rs b/crates/decentdb/src/exec/table_data.rs new file mode 100644 index 00000000..a296d937 --- /dev/null +++ b/crates/decentdb/src/exec/table_data.rs @@ -0,0 +1,887 @@ +//! Thematic extraction (mechanical split; no behavior change). + +use super::*; + +#[derive(Debug)] +pub(crate) struct TableData { + pub(crate) rows: Arc>, + tombstoned_row_ids: BTreeSet, + rows_sorted_by_id: bool, + cached_heap_bytes: usize, +} + +impl Default for TableData { + fn default() -> Self { + Self { + rows: Arc::new(Vec::new()), + tombstoned_row_ids: BTreeSet::new(), + rows_sorted_by_id: true, + cached_heap_bytes: 0, + } + } +} + +impl Clone for TableData { + fn clone(&self) -> Self { + Self { + rows: Arc::clone(&self.rows), + tombstoned_row_ids: self.tombstoned_row_ids.clone(), + rows_sorted_by_id: self.rows_sorted_by_id, + cached_heap_bytes: self.cached_heap_bytes, + } + } +} + +impl PartialEq for TableData { + fn eq(&self, other: &Self) -> bool { + self.rows == other.rows + && self.tombstoned_row_ids == other.tombstoned_row_ids + && self.rows_sorted_by_id == other.rows_sorted_by_id + } +} + +impl TableData { + pub(crate) fn from_rows(rows: Vec) -> Self { + let rows_sorted_by_id = rows.windows(2).all(|pair| pair[0].row_id <= pair[1].row_id); + let mut data = Self { + rows: Arc::new(rows), + tombstoned_row_ids: BTreeSet::new(), + rows_sorted_by_id, + cached_heap_bytes: 0, + }; + data.cached_heap_bytes = data.compute_heap_bytes(); + data + } + + pub(crate) fn row_count(&self) -> usize { + self.rows + .len() + .saturating_sub(self.tombstoned_row_ids.len()) + } + + fn is_row_tombstoned(&self, row_id: i64) -> bool { + self.tombstoned_row_ids.contains(&row_id) + } + + pub(crate) fn has_tombstoned_rows(&self) -> bool { + !self.tombstoned_row_ids.is_empty() + } + + pub(crate) fn visible_rows(&self) -> impl Iterator { + let has_tombstones = !self.tombstoned_row_ids.is_empty(); + self.rows + .iter() + .filter(move |row| !has_tombstones || !self.is_row_tombstoned(row.row_id)) + } + + fn mark_row_deleted(&mut self, row_id: i64) -> bool { + if self.row_index_by_id(row_id).is_some() { + self.tombstoned_row_ids.insert(row_id) + } else { + false + } + } + + pub(crate) fn mark_rows_deleted<'a, I>(&mut self, row_ids: I) -> usize + where + I: IntoIterator, + { + row_ids + .into_iter() + .filter(|row_id| self.mark_row_deleted(**row_id)) + .count() + } + + pub(crate) fn mark_existing_row_set_deleted(&mut self, row_ids: &BTreeSet) -> usize { + if row_ids.is_empty() { + return 0; + } + let before = self.tombstoned_row_ids.len(); + if self.tombstoned_row_ids.is_empty() { + self.tombstoned_row_ids = row_ids.clone(); + } else { + self.tombstoned_row_ids.extend(row_ids.iter().copied()); + } + self.tombstoned_row_ids.len().saturating_sub(before) + } + + #[cfg(test)] + pub(crate) fn reserve_rows(&mut self, additional: usize) { + let rows = Arc::make_mut(&mut self.rows); + let old_capacity = rows.capacity(); + rows.reserve(additional); + self.cached_heap_bytes = self.cached_heap_bytes.saturating_add( + rows.capacity() + .saturating_sub(old_capacity) + .saturating_mul(std::mem::size_of::()), + ); + } + + pub(crate) fn clear_rows(&mut self) { + self.rows = Arc::new(Vec::new()); + self.tombstoned_row_ids.clear(); + self.rows_sorted_by_id = true; + self.cached_heap_bytes = 0; + } + + fn shrink_to_fit_if_unique(&mut self) -> usize { + let Some(rows) = Arc::get_mut(&mut self.rows) else { + return 0; + }; + let old_capacity = rows.capacity(); + rows.shrink_to_fit(); + let freed = old_capacity + .saturating_sub(rows.capacity()) + .saturating_mul(std::mem::size_of::()); + self.cached_heap_bytes = self.cached_heap_bytes.saturating_sub(freed); + freed + } + + pub(crate) fn mutate_visible_rows(&mut self, mut f: F) -> Result<()> + where + F: FnMut(&mut StoredRow) -> Result<()>, + { + let rows = Arc::make_mut(&mut self.rows); + for row in rows.iter_mut() { + if !self.tombstoned_row_ids.contains(&row.row_id) { + f(row)?; + } + } + self.cached_heap_bytes = self.compute_heap_bytes(); + Ok(()) + } + + pub(super) fn row_index_by_id(&self, row_id: i64) -> Option { + if !self.tombstoned_row_ids.is_empty() && self.is_row_tombstoned(row_id) { + return None; + } + if let Some(index) = row_id + .checked_sub(1) + .and_then(|value| usize::try_from(value).ok()) + { + if let Some(row) = self.rows.get(index) { + if row.row_id == row_id { + return Some(index); + } + } + } + + if self.rows_sorted_by_id { + if let Ok(index) = self.rows.binary_search_by_key(&row_id, |row| row.row_id) { + return Some(index); + } + } + + self.rows.iter().position(|row| row.row_id == row_id) + } + + pub(crate) fn row_ids_in_range(&self, low: i64, high: i64) -> Vec { + if low > high { + return Vec::new(); + } + if self.rows_sorted_by_id { + let rows = self.rows.as_ref(); + let start = rows.partition_point(|row| row.row_id < low); + let end = start + rows[start..].partition_point(|row| row.row_id <= high); + if self.tombstoned_row_ids.is_empty() { + return rows[start..end].iter().map(|row| row.row_id).collect(); + } + return rows[start..end] + .iter() + .filter_map(|row| { + if self.is_row_tombstoned(row.row_id) { + None + } else { + Some(row.row_id) + } + }) + .collect(); + } + self.rows + .iter() + .filter_map(|row| { + if row.row_id >= low && row.row_id <= high && !self.is_row_tombstoned(row.row_id) { + Some(row.row_id) + } else { + None + } + }) + .collect() + } + + pub(super) fn row_by_id(&self, row_id: i64) -> Option<&StoredRow> { + self.row_index_by_id(row_id) + .and_then(|index| self.rows.get(index)) + } + + fn projected_values_by_id( + &self, + row_id: i64, + projection_indexes: &[usize], + ) -> Result>> { + Ok(self + .row_by_id(row_id) + .map(|row| project_simple_projection_value_vec(&row.values, projection_indexes))) + } + + fn projected_query_row_by_id( + &self, + row_id: i64, + projection_indexes: &[usize], + ) -> Result> { + Ok(self + .row_by_id(row_id) + .map(|row| project_simple_projection_values(&row.values, projection_indexes))) + } + + fn full_query_row_by_id(&self, row_id: i64) -> Option { + self.row_by_id(row_id) + .map(|row| QueryRow::new(row.values.clone())) + } + + fn projected_query_rows_in_id_range( + &self, + low: i64, + high_exclusive: i64, + limit: usize, + offset: usize, + projection_indexes: &[usize], + ) -> Option> { + if !self.rows_sorted_by_id || high_exclusive <= low { + return None; + } + let rows = self.rows.as_ref(); + let start = rows.partition_point(|row| row.row_id < low); + let end = start + rows[start..].partition_point(|row| row.row_id < high_exclusive); + let mut skipped = 0usize; + let mut projected = Vec::with_capacity(limit.min(end.saturating_sub(start))); + for row in &rows[start..end] { + if self.is_row_tombstoned(row.row_id) { + continue; + } + if skipped < offset { + skipped += 1; + continue; + } + if projected.len() >= limit { + break; + } + projected.push(project_simple_projection_values( + &row.values, + projection_indexes, + )); + } + Some(projected) + } + + fn visit_int64_column_values(&self, column_index: usize, mut visitor: F) -> Result<()> + where + F: FnMut(i64, Option) -> Result<()>, + { + for row in self.visible_rows() { + let value = int64_column_value(row.values.get(column_index))?; + visitor(row.row_id, value)?; + } + Ok(()) + } + + fn visit_float64_column_values(&self, column_index: usize, mut visitor: F) -> Result<()> + where + F: FnMut(i64, Option) -> Result<()>, + { + for row in self.visible_rows() { + let value = float64_column_value(row.values.get(column_index))?; + visitor(row.row_id, value)?; + } + Ok(()) + } + + /// Approximate heap residency of this table's row vector. Includes + /// `Vec` capacity plus each row's `Vec` capacity plus + /// each `Value`'s heap allocations. Excludes the `TableData` struct + /// itself. Used by storage instrumentation (ADR 0143 Phase A). This value + /// is cached and maintained by row mutation helpers. + #[must_use] + pub(crate) fn approximate_heap_bytes(&self) -> usize { + self.cached_heap_bytes + } + + pub(crate) fn compute_heap_bytes(&self) -> usize { + let row_struct = std::mem::size_of::(); + let mut total = self.rows.capacity() * row_struct; + for row in self.rows.iter() { + total += Self::row_heap_bytes(row); + } + total + } + + fn row_heap_bytes(row: &StoredRow) -> usize { + let value_struct = std::mem::size_of::(); + row.values.capacity() * value_struct + + row + .values + .iter() + .map(Value::approximate_heap_bytes) + .sum::() + } + + pub(crate) fn push_row(&mut self, row: StoredRow) { + if self.tombstoned_row_ids.is_empty() { + self.push_fresh_row(row); + return; + } + if self.tombstoned_row_ids.remove(&row.row_id) { + if let Some(index) = self + .rows + .iter() + .position(|candidate| candidate.row_id == row.row_id) + { + let rows = Arc::make_mut(&mut self.rows); + let old_heap_bytes = Self::row_heap_bytes(&rows[index]); + rows[index] = row; + let new_heap_bytes = Self::row_heap_bytes(&rows[index]); + self.cached_heap_bytes = self + .cached_heap_bytes + .saturating_sub(old_heap_bytes) + .saturating_add(new_heap_bytes); + return; + } + } + self.push_fresh_row(row); + } + + fn push_fresh_row(&mut self, row: StoredRow) { + let rows = Arc::make_mut(&mut self.rows); + let old_capacity = rows.capacity(); + if rows.len() == old_capacity { + let additional = if old_capacity < 1024 { + old_capacity.max(8) + } else { + old_capacity / 2 + }; + rows.reserve_exact(additional); + } + let row_heap_bytes = Self::row_heap_bytes(&row); + if rows + .last() + .is_some_and(|previous| previous.row_id > row.row_id) + { + self.rows_sorted_by_id = false; + } + rows.push(row); + self.cached_heap_bytes = self + .cached_heap_bytes + .saturating_add(row_heap_bytes) + .saturating_add( + rows.capacity() + .saturating_sub(old_capacity) + .saturating_mul(std::mem::size_of::()), + ); + } + + #[cfg(test)] + pub(crate) fn remove_row(&mut self, row_index: usize) -> StoredRow { + let rows = Arc::make_mut(&mut self.rows); + let row = rows.remove(row_index); + self.tombstoned_row_ids.remove(&row.row_id); + self.cached_heap_bytes = self + .cached_heap_bytes + .saturating_sub(Self::row_heap_bytes(&row)); + row + } + + #[cfg(test)] + pub(crate) fn retain_rows(&mut self, mut keep: F) + where + F: FnMut(&StoredRow) -> bool, + { + let mut removed_heap_bytes = 0usize; + let rows = Arc::make_mut(&mut self.rows); + rows.retain(|row| { + let retain = keep(row); + if !retain { + removed_heap_bytes = removed_heap_bytes.saturating_add(Self::row_heap_bytes(row)); + self.tombstoned_row_ids.remove(&row.row_id); + } + retain + }); + self.cached_heap_bytes = self.cached_heap_bytes.saturating_sub(removed_heap_bytes); + } + + pub(crate) fn replace_value( + &mut self, + row_index: usize, + column_index: usize, + value: Value, + ) -> Option<()> { + if self + .rows + .get(row_index) + .is_some_and(|row| self.is_row_tombstoned(row.row_id)) + { + return None; + } + let rows = Arc::make_mut(&mut self.rows); + let row = rows.get_mut(row_index)?; + let slot = row.values.get_mut(column_index)?; + let old_heap_bytes = slot.approximate_heap_bytes(); + *slot = value; + let new_heap_bytes = slot.approximate_heap_bytes(); + self.cached_heap_bytes = self + .cached_heap_bytes + .saturating_sub(old_heap_bytes) + .saturating_add(new_heap_bytes); + Some(()) + } + + pub(crate) fn replace_row_values( + &mut self, + row_index: usize, + values: Vec, + ) -> Option<()> { + if self + .rows + .get(row_index) + .is_some_and(|row| self.is_row_tombstoned(row.row_id)) + { + return None; + } + let rows = Arc::make_mut(&mut self.rows); + let row = rows.get_mut(row_index)?; + let old_heap_bytes = Self::row_heap_bytes(row); + row.values = values; + let new_heap_bytes = Self::row_heap_bytes(row); + self.cached_heap_bytes = self + .cached_heap_bytes + .saturating_sub(old_heap_bytes) + .saturating_add(new_heap_bytes); + Some(()) + } +} + +pub(crate) enum TableRowIter<'a> { + Empty(std::iter::Empty>>), + Resident(TableDataRowIter<'a>), + Paged(TablePageRowIter<'a>), +} + +impl<'a> Iterator for TableRowIter<'a> { + type Item = Result>; + + fn next(&mut self) -> Option { + match self { + Self::Empty(iter) => iter.next(), + Self::Resident(iter) => iter.next().map(|row| Ok(TableRowRef::Resident(row))), + Self::Paged(iter) => iter.next(), + } + } + + fn size_hint(&self) -> (usize, Option) { + match self { + Self::Empty(iter) => iter.size_hint(), + Self::Resident(iter) => iter.size_hint(), + Self::Paged(iter) => iter.size_hint(), + } + } +} + +impl ExactSizeIterator for TableRowIter<'_> { + fn len(&self) -> usize { + match self { + Self::Empty(iter) => iter.len(), + Self::Resident(iter) => iter.len(), + Self::Paged(iter) => iter.len(), + } + } +} + +pub(crate) struct TableDataRowIter<'a> { + rows: std::slice::Iter<'a, StoredRow>, + tombstoned_row_ids: &'a BTreeSet, + remaining: usize, +} + +impl<'a> TableDataRowIter<'a> { + fn new(data: &'a TableData) -> Self { + Self { + rows: data.rows.iter(), + tombstoned_row_ids: &data.tombstoned_row_ids, + remaining: data.row_count(), + } + } +} + +impl<'a> Iterator for TableDataRowIter<'a> { + type Item = &'a StoredRow; + + fn next(&mut self) -> Option { + for row in self.rows.by_ref() { + if self.tombstoned_row_ids.contains(&row.row_id) { + continue; + } + self.remaining = self.remaining.saturating_sub(1); + return Some(row); + } + self.remaining = 0; + None + } + + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) + } +} + +impl ExactSizeIterator for TableDataRowIter<'_> { + fn len(&self) -> usize { + self.remaining + } +} + +impl<'a> TableRowIter<'a> { + pub(crate) fn empty() -> Self { + Self::Empty(std::iter::empty()) + } +} + +pub(crate) struct TablePageRowIter<'a> { + pub(crate) manifest: &'a TablePageManifest, + pub(crate) position: usize, +} + +impl<'a> Iterator for TablePageRowIter<'a> { + type Item = Result>; + + fn next(&mut self) -> Option { + let position = self.position; + if position >= self.manifest.row_count() { + return None; + } + self.position += 1; + Some(self.manifest.row_at_position(position).and_then(|row| { + row.ok_or_else(|| { + DbError::corruption("paged row iterator advanced beyond manifest bounds") + }) + })) + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.manifest.row_count().saturating_sub(self.position); + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for TablePageRowIter<'_> { + fn len(&self) -> usize { + self.manifest.row_count().saturating_sub(self.position) + } +} + +#[derive(Clone, Copy)] +pub(crate) enum VisibleTableRowSource<'a> { + Temp(&'a TableData), + Base(&'a TableRowSource), +} + +impl<'a> VisibleTableRowSource<'a> { + pub(crate) fn rows(&self) -> TableRowIter<'a> { + match self { + Self::Temp(data) => TableRowIter::Resident(TableDataRowIter::new(data)), + Self::Base(source) => source.rows(), + } + } + + pub(crate) fn row_count(&self) -> usize { + match self { + Self::Temp(data) => data.row_count(), + Self::Base(source) => source.row_count(), + } + } + + pub(crate) fn has_tombstoned_rows(&self) -> bool { + match self { + Self::Temp(data) => data.has_tombstoned_rows(), + Self::Base(source) => source.has_tombstoned_rows(), + } + } + + pub(crate) fn row_by_id(&self, row_id: i64) -> Result>> { + match self { + Self::Temp(data) => Ok(data.row_by_id(row_id).map(TableRowRef::Resident)), + Self::Base(source) => source.row_by_id(row_id), + } + } + + pub(crate) fn row_ids_in_range(&self, low: i64, high: i64) -> Vec { + if low > high { + return Vec::new(); + } + match self { + Self::Temp(data) => data + .rows + .iter() + .filter_map(|row| { + if row.row_id >= low + && row.row_id <= high + && !data.is_row_tombstoned(row.row_id) + { + Some(row.row_id) + } else { + None + } + }) + .collect(), + Self::Base(source) => source.row_ids_in_range(low, high), + } + } + + pub(crate) fn projected_values_by_id( + &self, + row_id: i64, + projection_indexes: &[usize], + ) -> Result>> { + match self { + Self::Temp(data) => data.projected_values_by_id(row_id, projection_indexes), + Self::Base(source) => source.projected_values_by_id(row_id, projection_indexes), + } + } + + pub(crate) fn projected_query_row_by_id( + &self, + row_id: i64, + projection_indexes: &[usize], + ) -> Result> { + match self { + Self::Temp(data) => data.projected_query_row_by_id(row_id, projection_indexes), + Self::Base(source) => source.projected_query_row_by_id(row_id, projection_indexes), + } + } + + pub(crate) fn full_query_row_by_id(&self, row_id: i64) -> Result> { + match self { + Self::Temp(data) => Ok(data.full_query_row_by_id(row_id)), + Self::Base(source) => source.full_query_row_by_id(row_id), + } + } + + pub(crate) fn projected_query_rows_in_id_range( + &self, + low: i64, + high_exclusive: i64, + limit: usize, + offset: usize, + projection_indexes: &[usize], + ) -> Option> { + match self { + Self::Temp(data) => data.projected_query_rows_in_id_range( + low, + high_exclusive, + limit, + offset, + projection_indexes, + ), + Self::Base(source) => source.projected_query_rows_in_id_range( + low, + high_exclusive, + limit, + offset, + projection_indexes, + ), + } + } + + pub(crate) fn row_at_position(&self, position: usize) -> Result>> { + match self { + Self::Temp(data) => Ok(data.visible_rows().nth(position).map(TableRowRef::Resident)), + Self::Base(source) => source.row_at_position(position), + } + } + + pub(crate) fn visit_int64_column_values(&self, column_index: usize, visitor: F) -> Result<()> + where + F: FnMut(i64, Option) -> Result<()>, + { + match self { + Self::Temp(data) => data.visit_int64_column_values(column_index, visitor), + Self::Base(source) => source.visit_int64_column_values(column_index, visitor), + } + } + + pub(crate) fn visit_float64_column_values( + &self, + column_index: usize, + visitor: F, + ) -> Result<()> + where + F: FnMut(i64, Option) -> Result<()>, + { + match self { + Self::Temp(data) => data.visit_float64_column_values(column_index, visitor), + Self::Base(source) => source.visit_float64_column_values(column_index, visitor), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum TableRowSource { + Resident(Arc), + Paged(Arc), +} + +impl TableRowSource { + pub(crate) fn rows(&self) -> TableRowIter<'_> { + match self { + Self::Resident(data) => TableRowIter::Resident(TableDataRowIter::new(data)), + Self::Paged(manifest) => TableRowIter::Paged(manifest.rows()), + } + } + + pub(crate) fn resident_data(&self) -> &TableData { + match self { + Self::Resident(data) => data.as_ref(), + // Invariant: this method is only called for resident row sources. + Self::Paged(_) => unreachable!("paged row sources are not resident table data"), + } + } + + pub(crate) fn resident_data_mut(&mut self) -> &mut TableData { + match self { + Self::Resident(data) => Arc::make_mut(data), + // Invariant: this method is only called for mutable resident row sources. + Self::Paged(_) => unreachable!("paged row sources are not mutable resident table data"), + } + } + + pub(crate) fn row_count(&self) -> usize { + match self { + Self::Resident(data) => data.row_count(), + Self::Paged(manifest) => manifest.row_count(), + } + } + + pub(crate) fn has_tombstoned_rows(&self) -> bool { + match self { + Self::Resident(data) => data.has_tombstoned_rows(), + Self::Paged(manifest) => !manifest.tombstoned_row_ids.is_empty(), + } + } + + pub(crate) fn row_by_id(&self, row_id: i64) -> Result>> { + match self { + Self::Resident(data) => Ok(data.row_by_id(row_id).map(TableRowRef::Resident)), + Self::Paged(manifest) => manifest.row_by_id(row_id), + } + } + + pub(crate) fn row_ids_in_range(&self, low: i64, high: i64) -> Vec { + if low > high { + return Vec::new(); + } + match self { + Self::Resident(data) => data.row_ids_in_range(low, high), + Self::Paged(manifest) => manifest.row_ids_in_range(low, high), + } + } + + fn projected_values_by_id( + &self, + row_id: i64, + projection_indexes: &[usize], + ) -> Result>> { + match self { + Self::Resident(data) => data.projected_values_by_id(row_id, projection_indexes), + Self::Paged(manifest) => manifest.projected_values_by_id(row_id, projection_indexes), + } + } + + fn projected_query_row_by_id( + &self, + row_id: i64, + projection_indexes: &[usize], + ) -> Result> { + match self { + Self::Resident(data) => data.projected_query_row_by_id(row_id, projection_indexes), + Self::Paged(manifest) => manifest + .projected_values_by_id(row_id, projection_indexes) + .map(|values| values.map(QueryRow::new)), + } + } + + fn full_query_row_by_id(&self, row_id: i64) -> Result> { + match self { + Self::Resident(data) => Ok(data.full_query_row_by_id(row_id)), + Self::Paged(manifest) => manifest.full_query_row_by_id(row_id), + } + } + + fn projected_query_rows_in_id_range( + &self, + low: i64, + high_exclusive: i64, + limit: usize, + offset: usize, + projection_indexes: &[usize], + ) -> Option> { + match self { + Self::Resident(data) => data.projected_query_rows_in_id_range( + low, + high_exclusive, + limit, + offset, + projection_indexes, + ), + Self::Paged(_) => None, + } + } + + fn row_at_position(&self, position: usize) -> Result>> { + match self { + Self::Resident(data) => { + Ok(data.visible_rows().nth(position).map(TableRowRef::Resident)) + } + Self::Paged(manifest) => manifest.row_at_position(position), + } + } + + fn visit_int64_column_values(&self, column_index: usize, visitor: F) -> Result<()> + where + F: FnMut(i64, Option) -> Result<()>, + { + match self { + Self::Resident(data) => data.visit_int64_column_values(column_index, visitor), + Self::Paged(manifest) => manifest.visit_int64_column_values(column_index, visitor), + } + } + + fn visit_float64_column_values(&self, column_index: usize, visitor: F) -> Result<()> + where + F: FnMut(i64, Option) -> Result<()>, + { + match self { + Self::Resident(data) => data.visit_float64_column_values(column_index, visitor), + Self::Paged(manifest) => manifest.visit_float64_column_values(column_index, visitor), + } + } + + pub(crate) fn approximate_heap_bytes(&self) -> usize { + match self { + Self::Resident(data) => data.approximate_heap_bytes(), + Self::Paged(manifest) => manifest.approximate_heap_bytes(), + } + } + + pub(crate) fn shrink_resident_to_fit_if_unique(&mut self) -> usize { + match self { + Self::Resident(data) => Arc::get_mut(data) + .map(TableData::shrink_to_fit_if_unique) + .unwrap_or(0), + Self::Paged(_) => 0, + } + } + + pub(crate) fn paged_manifest(&self) -> Option<&TablePageManifest> { + match self { + Self::Resident(_) => None, + Self::Paged(manifest) => Some(manifest), + } + } +} diff --git a/crates/decentdb/src/lib.rs b/crates/decentdb/src/lib.rs index 8f69eb14..d0d1da12 100644 --- a/crates/decentdb/src/lib.rs +++ b/crates/decentdb/src/lib.rs @@ -41,6 +41,33 @@ mod wal; mod wasm; mod write_queue; +/// Internal entry points for coverage-guided fuzz targets (`fuzz/` crate). +/// +/// Only available with the `fuzz-internals` feature; not part of the stable +/// API surface. Every function here must tolerate arbitrary malformed input +/// and return a typed error instead of panicking. +#[cfg(feature = "fuzz-internals")] +#[doc(hidden)] +pub mod fuzzing { + use crate::error::Result; + use crate::record::row::Row; + + /// Decode a row from raw bytes, returning the column count on success. + pub fn row_decode(bytes: &[u8]) -> Result { + Row::decode(bytes).map(|row| row.values().len()) + } + + /// Decode the `INT64` value at `column_index` from raw row bytes. + pub fn row_decode_int64_at(bytes: &[u8], column_index: usize) -> Result> { + Row::decode_int64_at(bytes, column_index) + } + + /// Decode a varint-encoded `u64` from raw bytes. + pub fn decode_varint_u64(bytes: &[u8]) -> Result<(u64, usize)> { + crate::record::decode_varint_u64(bytes) + } +} + pub use crate::branch::{ BranchDiffReport, BranchInfo, BranchLogEntry, BranchMergeChange, BranchMergeConflict, BranchMergeOperation, BranchMergeReport, BranchRestoreReport, BranchRowDiff, BranchTableDiff, diff --git a/crates/decentdb/src/planner/mod.rs b/crates/decentdb/src/planner/mod.rs index 8fa43ca0..e1d0549b 100644 --- a/crates/decentdb/src/planner/mod.rs +++ b/crates/decentdb/src/planner/mod.rs @@ -1436,7 +1436,7 @@ fn maybe_ordered_row_id_scan_plan(query: &Query, catalog: &CatalogState) -> Opti } #[derive(Clone, Copy)] -struct TableBindingRef<'a> { +pub(crate) struct TableBindingRef<'a> { name: &'a str, alias: &'a Option, } @@ -1454,12 +1454,12 @@ struct QualifiedColumnRef<'a> { } #[derive(Clone, Copy)] -struct SimpleSpatialJoinPredicate<'a> { +pub(crate) struct SimpleSpatialJoinPredicate<'a> { left: QualifiedColumnRef<'a>, right: QualifiedColumnRef<'a>, } -fn simple_spatial_join_predicate<'a>( +pub(crate) fn simple_spatial_join_predicate<'a>( expr: &'a Expr, left_binding: TableBindingRef<'a>, right_binding: TableBindingRef<'a>, diff --git a/crates/decentdb/src/record/row.rs b/crates/decentdb/src/record/row.rs index 9a34af7f..7cee0f3a 100644 --- a/crates/decentdb/src/record/row.rs +++ b/crates/decentdb/src/record/row.rs @@ -661,6 +661,14 @@ impl Row { store: Option<&S>, ) -> Result { let (field_count, mut offset) = decode_varint_u64(bytes)?; + // Each field requires at least a tag byte and a payload-length byte, + // so a count larger than the remaining input cannot be valid. Reject + // it before sizing the allocation to avoid memory exhaustion on + // malformed input. + let max_possible_fields = bytes.len().saturating_sub(offset) / 2; + if field_count > max_possible_fields as u64 { + return Err(DbError::corruption("row field count exceeds input size")); + } let mut values = Vec::with_capacity(field_count as usize); for _ in 0..field_count { @@ -1543,6 +1551,19 @@ mod tests { ); } + #[test] + fn decode_rejects_oversized_field_count_without_huge_allocation() { + // Regression: a malformed varint field count must be rejected as + // corruption before sizing the values vector, not trigger an + // out-of-memory allocation (found by the record_decode fuzz target). + let encoded = vec![254_u8, 183, 183, 90]; + let err = Row::decode(&encoded).expect_err("oversized field count should fail"); + assert!( + err.to_string().contains("field count"), + "unexpected error: {err}" + ); + } + #[test] fn semantic_values_roundtrip_compact_payloads() { let row = Row::new(vec![ diff --git a/crates/libpg_query_sys/Cargo.toml b/crates/libpg_query_sys/Cargo.toml index 4e3c662b..429d633f 100644 --- a/crates/libpg_query_sys/Cargo.toml +++ b/crates/libpg_query_sys/Cargo.toml @@ -2,6 +2,7 @@ name = "libpg_query_sys" version.workspace = true edition.workspace = true +rust-version.workspace = true authors.workspace = true license.workspace = true diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..4efffda2 --- /dev/null +++ b/deny.toml @@ -0,0 +1,41 @@ +# cargo-deny configuration for DecentDB supply-chain checks. +# Run locally with: cargo deny check + +[advisories] +db-urls = ["https://github.com/rustsec/advisory-db"] +# Known vulnerabilities are denied by default in cargo-deny 0.20+; list any +# deliberate, reasoned exceptions here. +ignore = [ + # bincode 1.x is unmaintained with no safe upgrade path; it is only used + # by iai-callgrind (dev-only benchmark harness), never shipped in the + # engine, CLI, or bindings. + { id = "RUSTSEC-2025-0141", reason = "dev-only iai-callgrind benchmark dependency; not shipped" }, + # proc-macro-error2 is unmaintained with no safe upgrade path; likewise + # only reachable through iai-callgrind dev tooling. + { id = "RUSTSEC-2026-0173", reason = "dev-only iai-callgrind benchmark dependency; not shipped" }, +] + +[licenses] +confidence-threshold = 0.8 +allow = [ + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "MIT", + "BSD-3-Clause", + "ISC", + "Zlib", + "Unicode-3.0", + "BSL-1.0", + # Permissive data license used by the webpki-roots certificate bundles. + "CDLA-Permissive-2.0", +] + +[bans] +multiple-versions = "warn" +wildcards = "warn" +highlight = "all" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] diff --git a/design/FUTURE_WINS.md b/design/FUTURE_WINS.md index f50836d1..2c000e00 100644 --- a/design/FUTURE_WINS.md +++ b/design/FUTURE_WINS.md @@ -128,9 +128,9 @@ Status values: - `BACKLOG`: valuable, but not part of the near-term implementation path. Future version values are planning buckets, not release commitments. The -current public release in this repository is `2.17.0`, and the current -planning release bucket in this repository is `2.17.0`. `vNext` means -the first release bucket after `2.17.0` only when scope is explicitly accepted. +current public release in this repository is `2.17.1`, and the current +planning release bucket in this repository is `2.17.1`. `vNext` means +the first release bucket after `2.17.1` only when scope is explicitly accepted. `vNext+1` and `vNext+2` are follow-on planning buckets, not exact semantic versions. diff --git a/design/SPEC.md b/design/SPEC.md index 432d3675..78e99463 100644 --- a/design/SPEC.md +++ b/design/SPEC.md @@ -216,7 +216,7 @@ Use parser ADRs and current code as authoritative references. Primary reference: -- `design/adr/0035-sql-parser-libpg-query.md` +- `design/adr/0213-sql-parser-libpg-query.md` ### 6.2 Supported SQL subset (Compatibility Anchor) diff --git a/design/VERSIONING_GUIDE.md b/design/VERSIONING_GUIDE.md index ba38e182..a6009636 100644 --- a/design/VERSIONING_GUIDE.md +++ b/design/VERSIONING_GUIDE.md @@ -86,6 +86,15 @@ exists in the Rust repository today. For the Node packages, update both the manifest and the lockfile's top-level package version entries. +### Web binding + +- `bindings/web/package.json` +- `bindings/web/package-lock.json` + +The `@decentdb/web` package follows the workspace release version, like the +other in-repo bindings; update the manifest and the lockfile's top-level +package version entries (`scripts/bump_version.sh` handles both). + ### Documentation - `docs/about/changelog.md` @@ -163,7 +172,7 @@ dependency in `knex-decentdb`) after the underlying package version changes. After a version bump, verify: - `VERSION` and `Cargo.toml` have the intended workspace version. -- Python, Java, Dart, and Node package metadata all reflect the same DecentDB release version. +- Python, Java, Dart, Node, and Web package metadata all reflect the same DecentDB release version. - `docs/about/changelog.md` explains the release and any important versioning context. - No stale old-version references remain in the release-facing files. - The NuGet workflow still matches the current tag format. @@ -196,6 +205,8 @@ rg 'OLD_VERSION|vOLD_VERSION' \ bindings/node/decentdb/package-lock.json \ bindings/node/knex-decentdb/package.json \ bindings/node/knex-decentdb/package-lock.json \ + bindings/web/package.json \ + bindings/web/package-lock.json \ benchmarks/rust-baseline/Cargo.lock \ docs/about/changelog.md \ docs/user-guide/benchmarks.md \ diff --git a/design/adr/0049-constraint-index-deduplication.md b/design/adr/0049-constraint-index-deduplication.md index 1a61e51e..4956c99e 100644 --- a/design/adr/0049-constraint-index-deduplication.md +++ b/design/adr/0049-constraint-index-deduplication.md @@ -101,4 +101,4 @@ This avoids creating redundant indexes that differ only by name. ## References - [0006-foreign-key-index-creation.md](0006-foreign-key-index-creation.md) - [0036-catalog-constraints-index-metadata.md](0036-catalog-constraints-index-metadata.md) -- [0036-integer-primary-key.md](0036-integer-primary-key.md) +- [0214-integer-primary-key.md](0214-integer-primary-key.md) diff --git a/design/adr/0071-sql-null-three-valued-logic.md b/design/adr/0071-sql-null-three-valued-logic.md index 1c5e62e8..04512a32 100644 --- a/design/adr/0071-sql-null-three-valued-logic.md +++ b/design/adr/0071-sql-null-three-valued-logic.md @@ -58,5 +58,5 @@ This ADR defines the behavior for: ### References - SQL enhancements roadmap: `design/SQL_ENHANCEMENTS_PLAN.md` (Section 5.1, NULL semantics gate) -- SQL parser ADR: `design/adr/0035-sql-parser-libpg-query.md` +- SQL parser ADR: `design/adr/0213-sql-parser-libpg-query.md` - Repo workflow and ADR gating: `AGENTS.md` diff --git a/design/adr/0074-exists-subquery-surface-v0.md b/design/adr/0074-exists-subquery-surface-v0.md index d003470b..a63fe24d 100644 --- a/design/adr/0074-exists-subquery-surface-v0.md +++ b/design/adr/0074-exists-subquery-surface-v0.md @@ -50,4 +50,4 @@ Add initial SQL subquery support only for `EXISTS (subquery)` with strict limits ### References - SQL enhancements roadmap: `design/SQL_ENHANCEMENTS_PLAN.md` (EXISTS + subquery open question) -- Parser ADR: `design/adr/0035-sql-parser-libpg-query.md` +- Parser ADR: `design/adr/0213-sql-parser-libpg-query.md` diff --git a/design/adr/0078-cte-non-recursive-scope-v0.md b/design/adr/0078-cte-non-recursive-scope-v0.md index 8bfe6a57..9db6620a 100644 --- a/design/adr/0078-cte-non-recursive-scope-v0.md +++ b/design/adr/0078-cte-non-recursive-scope-v0.md @@ -50,5 +50,5 @@ Implement `WITH` for **non-recursive CTEs** in `SELECT` statements with the foll ### References - Roadmap: `design/SQL_ENHANCEMENTS_PLAN.md` (Section 5.3) -- Parser ADR: `design/adr/0035-sql-parser-libpg_query.md` +- Parser ADR: `design/adr/0213-sql-parser-libpg-query.md` - View semantics ADR: `design/adr/0070-views-catalog-and-semantics.md` diff --git a/design/adr/0085-after-triggers-v0.md b/design/adr/0085-after-triggers-v0.md index c16aa605..656bc73a 100644 --- a/design/adr/0085-after-triggers-v0.md +++ b/design/adr/0085-after-triggers-v0.md @@ -59,4 +59,4 @@ Implement a constrained trigger surface for 0.x: ### References - Roadmap: `design/SQL_ENHANCEMENTS_PLAN.md` (Section 5.8) -- Parser baseline: `design/adr/0035-libpg-query-parser-adoption.md` +- Parser baseline: `design/adr/0213-sql-parser-libpg-query.md` diff --git a/design/adr/0107-recursive-cte-execution.md b/design/adr/0107-recursive-cte-execution.md index 39b2cc8c..15745686 100644 --- a/design/adr/0107-recursive-cte-execution.md +++ b/design/adr/0107-recursive-cte-execution.md @@ -48,5 +48,5 @@ Implement `WITH RECURSIVE` using **iterative fixpoint evaluation in the executor ### References - Non-recursive CTE ADR: `design/adr/0078-cte-non-recursive-scope-v0.md` -- Parser ADR: `design/adr/0035-sql-parser-libpg-query.md` +- Parser ADR: `design/adr/0213-sql-parser-libpg-query.md` - PostgreSQL WITH RECURSIVE docs: https://www.postgresql.org/docs/current/queries-with.html diff --git a/design/adr/0123-phase1-table-btree-foundation.md b/design/adr/0123-phase1-table-btree-foundation.md index 0f0494dd..78bf13e0 100644 --- a/design/adr/0123-phase1-table-btree-foundation.md +++ b/design/adr/0123-phase1-table-btree-foundation.md @@ -61,5 +61,5 @@ Rejected. That would leave signed rowid ordering, row encoding, and cursor decod - `design/adr/0120-core-storage-engine-btree.md` - `design/adr/0035-btree-page-layout-v2.md` - `design/adr/0030-record-format.md` -- `design/adr/0036-integer-primary-key.md` +- `design/adr/0214-integer-primary-key.md` - `crates/decentdb/src/btree/` diff --git a/design/adr/0203-compact-dense-runtime-int64-index.md b/design/adr/0203-compact-dense-runtime-int64-index.md index 31f1aa7a..5f27c9d3 100644 --- a/design/adr/0203-compact-dense-runtime-int64-index.md +++ b/design/adr/0203-compact-dense-runtime-int64-index.md @@ -113,7 +113,7 @@ than references because its entries are computed rather than allocated. ## References -- `design/adr/0036-integer-primary-key.md` +- `design/adr/0214-integer-primary-key.md` - `design/adr/0092-integer-pk-auto-increment.md` - `design/adr/0184-default-fast-planner-and-runtime-contract.md` - `design/adr/0200-resident-table-delete-tombstones-and-format-14.md` diff --git a/design/adr/0205-compact-paged-row-directory.md b/design/adr/0205-compact-paged-row-directory.md index cc0d89f0..45fabe83 100644 --- a/design/adr/0205-compact-paged-row-directory.md +++ b/design/adr/0205-compact-paged-row-directory.md @@ -122,7 +122,7 @@ allocation. ## References -- `design/adr/0036-integer-primary-key.md` +- `design/adr/0214-integer-primary-key.md` - `design/adr/0184-default-fast-planner-and-runtime-contract.md` - `design/adr/0200-resident-table-delete-tombstones-and-format-14.md` - `design/adr/0203-compact-dense-runtime-int64-index.md` diff --git a/design/adr/0207-inline-runtime-encoded-index-keys.md b/design/adr/0207-inline-runtime-encoded-index-keys.md index 83230bc6..5b6f54f1 100644 --- a/design/adr/0207-inline-runtime-encoded-index-keys.md +++ b/design/adr/0207-inline-runtime-encoded-index-keys.md @@ -135,7 +135,7 @@ positives are permitted and merely retain the old scan. ## References -- `design/adr/0036-integer-primary-key.md` +- `design/adr/0214-integer-primary-key.md` - `design/adr/0184-default-fast-planner-and-runtime-contract.md` - `design/adr/0203-compact-dense-runtime-int64-index.md` - `design/PRD.md` diff --git a/design/adr/0214-integer-primary-key.md b/design/adr/0214-integer-primary-key.md index 2267d315..b6d929f2 100644 --- a/design/adr/0214-integer-primary-key.md +++ b/design/adr/0214-integer-primary-key.md @@ -1,4 +1,4 @@ -# 36. Integer Primary Key Optimization +# ADR 0214: Integer Primary Key Optimization Date: 2026-01-31 diff --git a/docs/about/changelog.md b/docs/about/changelog.md index cc4fe43c..393ee031 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -5,6 +5,74 @@ All notable changes to DecentDB will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.17.1] - [2026-08-06] + +### Added + +- Added a `fuzz/` cargo-fuzz crate with coverage-guided libFuzzer targets + (`wal_recovery`, `record_decode`) that assert WAL recovery and record + decoding never panic on malformed input, plus a `fuzz-internals` engine + feature exposing doc-hidden decode shims, and a `coverage-guided-fuzz` + matrix job in the memory-safety nightly workflow running each target for + ten minutes with crash-artifact upload. +- Added the four grouped-commit fault-injection harness scenarios from + `tests/harness/grouped_commit_fault_injection_plan.md` + (`grouped_commit_all_commit`, `grouped_commit_fail_during_second_commit`, + `grouped_commit_fail_sync_before_flush`, `grouped_commit_crash_after_sync`) + to the pre-commit suite (stage 5) and the release validation workflow. +- Added a `coverage-nightly` workflow that runs `cargo llvm-cov nextest` and + uploads `lcov.info`, an `msrv` CI job that checks the workspace on the newly + pinned Rust 1.88 MSRV (`rust-version` in the workspace manifest), and a + `supply-chain` CI job running `cargo deny check` with a new `deny.toml` + (advisories, licenses, bans, sources). +- Added a Web binding browser smoke (WASM build + Playwright OPFS suite) to + the release validation workflow so browser regressions block releases. +- Added a verified cross-binding feature coverage matrix to + `bindings/README.md` documenting which bindings expose the write queue, + watch/change streams, branch/snapshot workflows, and extension lifecycle + APIs. + +### Changed + +- Split the two largest engine files into thematic submodules with no + behavior change: `exec/mod.rs` (49,649 -> 14,036 lines) into + `bench_queries`, `codec`, `deferred`, `evaluate`, `grouped`, `indexes`, + `joins`, `manifest`, `paged_tables`, `runtime_eval`, `runtime_keys`, + `simple_queries`, and `table_data`; and `db.rs` (17,181 -> 10,483 lines) + into `db/sync_ops`, `db/prepared_fast_paths`, `db/pragmas`, `db/branch_ops`, + and `db/reactive_ops`. rust-baseline medium-scale timings before/after the + split are within noise. +- Aligned `@decentdb/web` (`bindings/web`) to the workspace release version + and taught `scripts/bump_version.sh` to keep the web package and lockfile + versions in sync on future bumps. +- Updated `Cargo.lock` to clear RUSTSEC advisories flagged by the new + supply-chain job: `rustls-webpki` 0.103.10 -> 0.103.13 (RUSTSEC-2026-0104), + `anyhow` 1.0.102 -> 1.0.104 (RUSTSEC-2026-0190), `crossbeam-epoch` + 0.9.18 -> 0.9.20 (RUSTSEC-2026-0204), and `rand` 0.8.5 -> 0.8.7 / + 0.9.2 -> 0.9.5 (RUSTSEC-2026-0097). Two dev-only `iai-callgrind` + transitive advisories (unmaintained `bincode`, `proc-macro-error2`) are + documented exceptions in `deny.toml`. +- Renumbered the duplicate ADRs `0027-bulk-load-api-specification`, + `0035-sql-parser-libpg-query`, and `0036-integer-primary-key` to 0212, + 0213, and 0214, updated all inbound references (including two pre-existing + broken link variants), and added `bindings/web`, `tests/bindings/c`, and + `tests/bindings/web` to the `AGENTS.md` and `bindings/README.md` inventory + docs. + +### Fixed + +- Fixed a memory-exhaustion vulnerability in row decoding found by the new + `record_decode` fuzz target: `Row::decode` sized its values vector from an + untrusted varint field count, so a 4-byte malformed input could force a + multi-gigabyte allocation (OOM). The decoder now rejects field counts that + exceed what the remaining input bytes can physically contain, and a + regression test covers the crafted input. + +### Removed + +- Stopped tracking the generated `bindings/python/decentdb.egg-info/` + packaging artifacts in git and added `*.egg-info/` to `.gitignore`. + ## [2.17.0] - [2026-08-02] ### Added diff --git a/docs/user-guide/benchmarks.md b/docs/user-guide/benchmarks.md index 3ddc3ab3..5818c99e 100644 --- a/docs/user-guide/benchmarks.md +++ b/docs/user-guide/benchmarks.md @@ -13,7 +13,7 @@ This page collects the current Python embedded comparison charts and a plain-lan | Engine | Version stamp | Source | | --- | --- | --- | -| DecentDB | 2.17.0 | Workspace package version | +| DecentDB | 2.17.1 | Workspace package version | | SQLite (`SQLite_wal_full`) | 3.52.0 | Benchmark-reported engine version | | DuckDB | 1.5.1 | Benchmark-reported engine version | | H2 (`JDBC`) | 2.2.224 | Benchmark-reported engine version | diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 00000000..8f84131c --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,3 @@ +target/ +corpus/ +artifacts/ diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 00000000..80b22f7a --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,1390 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bindgen" +version = "0.66.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", + "which", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "decentdb" +version = "2.17.0" +dependencies = [ + "base64", + "chacha20", + "chrono", + "ed25519-dalek", + "getrandom 0.3.4", + "js-sys", + "libc", + "libpg_query_sys", + "md5", + "miniz_oxide", + "regex", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.19", + "toml", + "wasm-bindgen", + "windows-sys 0.61.2", + "zeroize", +] + +[[package]] +name = "decentdb-fuzz" +version = "0.0.0" +dependencies = [ + "decentdb", + "libfuzzer-sys", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libpg_query_sys" +version = "2.17.0" +dependencies = [ + "pg_query", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pg_query" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ca6fdb8f9d32182abf17328789f87f305dd8c8ce5bf48c5aa2b5cffc94e1c04" +dependencies = [ + "bindgen", + "cc", + "fs_extra", + "glob", + "itertools 0.10.5", + "prost", + "prost-build", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.119", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 00000000..5d65d461 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "decentdb-fuzz" +version = "0.0.0" +publish = false +edition = "2021" +license = "Apache-2.0" + +[package.metadata] +cargo-fuzz = true + +# Independent workspace so this crate is not pulled into the main build. +[workspace] + +[dependencies] +libfuzzer-sys = "0.4" +decentdb = { path = "../crates/decentdb", default-features = false, features = [ + "fuzz-internals", +] } + +[[bin]] +name = "wal_recovery" +path = "fuzz_targets/wal_recovery.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "record_decode" +path = "fuzz_targets/record_decode.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/record_decode.rs b/fuzz/fuzz_targets/record_decode.rs new file mode 100644 index 00000000..ff1d3d53 --- /dev/null +++ b/fuzz/fuzz_targets/record_decode.rs @@ -0,0 +1,19 @@ +//! Fuzz target: record/row decoding must never panic on malformed input. +//! +//! Exercises the engine's row decoding entry points with arbitrary bytes. +//! Any failure must surface as a typed `DbError`, not a panic (libFuzzer +//! reports panics as crashes). + +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + let _ = decentdb::fuzzing::row_decode(data); + let _ = decentdb::fuzzing::decode_varint_u64(data); + + if data.len() >= 2 { + let column_index = u16::from_le_bytes([data[0], data[1]]) as usize % 64; + let _ = decentdb::fuzzing::row_decode_int64_at(&data[2..], column_index); + } +}); diff --git a/fuzz/fuzz_targets/wal_recovery.rs b/fuzz/fuzz_targets/wal_recovery.rs new file mode 100644 index 00000000..53d31cbd --- /dev/null +++ b/fuzz/fuzz_targets/wal_recovery.rs @@ -0,0 +1,102 @@ +//! Fuzz target: WAL recovery must never panic on malformed input. +//! +//! Builds a valid template database once per process, then for each fuzz +//! input materializes the template database file alongside a fuzz-derived +//! WAL and reopens it. Recovery may succeed or return a typed corruption +//! error — it must never panic (libFuzzer reports panics as crashes). +//! +//! Run with: `cargo +nightly fuzz run wal_recovery` + +#![no_main] + +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::OnceLock; + +use decentdb::{Db, DbConfig, WalSyncMode}; +use libfuzzer_sys::fuzz_target; + +/// Snapshot of a valid, checkpointed database used as the per-iteration base. +struct Template { + db_bytes: Vec, +} + +static TEMPLATE: OnceLock