sqlrite 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sqlrite-0.4.0 → sqlrite-0.5.0}/Cargo.lock +7 -7
- {sqlrite-0.4.0 → sqlrite-0.5.0}/Cargo.toml +2 -2
- {sqlrite-0.4.0 → sqlrite-0.5.0}/PKG-INFO +1 -1
- {sqlrite-0.4.0 → sqlrite-0.5.0}/desktop/package.json +1 -1
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/pager.md +14 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/supported-sql.md +19 -1
- {sqlrite-0.4.0 → sqlrite-0.5.0}/pyproject.toml +1 -1
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sdk/python/Cargo.toml +1 -1
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sqlrite-ask/Cargo.toml +1 -1
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/connection.rs +55 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/db/database.rs +40 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/mod.rs +41 -1
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/freelist.rs +33 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/mod.rs +301 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/.github/workflows/ci.yml +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/.github/workflows/release-pr.yml +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/.github/workflows/release.yml +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/.github/workflows/rust.yml +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/.gitignore +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/CLAUDE.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/CODE_OF_CONDUCT.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/LICENSE +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/MAINTAINERS +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/Makefile +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/README.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/desktop/index.html +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/desktop/package-lock.json +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/desktop/src/App.svelte +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/desktop/src/app.css +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/desktop/src/main.ts +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/desktop/src/vite-env.d.ts +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/desktop/svelte.config.js +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/desktop/tsconfig.json +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/desktop/vite.config.ts +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/_index.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/architecture.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/ask-backend-examples.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/ask.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/design-decisions.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/desktop.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/embedding.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/file-format.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/fts.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/getting-started.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/mcp.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/phase-7-plan.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/phase-8-plan.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/release-plan.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/release-secrets.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/roadmap.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/smoke-test.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/sql-engine.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/storage-model.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/docs/usage.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/examples/README.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/examples/c/Makefile +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/examples/c/hello.c +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/examples/go/go.mod +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/examples/go/hello.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/examples/hybrid-retrieval/README.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/examples/hybrid-retrieval/hybrid_retrieval.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/examples/nodejs/hello.mjs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/examples/python/hello.py +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/examples/rust/quickstart.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/examples/wasm/Makefile +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/examples/wasm/index.html +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/examples/wasm/server.mjs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/images/SQLRite - Desktop.png +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/images/SQLRite Data Structures.png +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/images/SQLRite Simple SQL Execution High Level Diagram.png +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/images/SQLRite Simple SQL INSERT Execution High Level Diagram (Insert Row).png +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/images/SQLRite Simple SQL INSERT Execution High Level Diagram.png +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/images/SQLRite_logo.png +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/images/architecture.png +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/rust-toolchain.toml +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/samples/AST.delete.example +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/samples/AST.insert.exemple +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/samples/AST.select.example +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/samples/AST.update.example +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/samples/CREATE TABLE sqlrite_schema.sql +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/samples/CREATE_TABLE with duplicate.sql +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/samples/CREATE_TABLE.sql +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/samples/INSERT.sql +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/scripts/bump-version.sh +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sdk/go/README.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sdk/go/ask.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sdk/go/ask_test.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sdk/go/conn.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sdk/go/go.mod +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sdk/go/rows.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sdk/go/sqlrite.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sdk/go/sqlrite_test.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sdk/go/stmt.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sdk/python/README.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sdk/python/src/lib.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sdk/python/tests/test_ask.py +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sdk/python/tests/test_sqlrite.py +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sqlrite-ask/README.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sqlrite-ask/src/lib.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sqlrite-ask/src/prompt.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sqlrite-ask/src/provider/anthropic.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sqlrite-ask/src/provider/mock.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sqlrite-ask/src/provider/mod.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/sqlrite-ask/tests/anthropic_http.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/ask/mod.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/ask/schema.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/error.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/lib.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/main.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/meta_command/mod.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/repl/mod.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/db/mod.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/db/secondary_index.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/db/table.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/executor.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/fts/bm25.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/fts/mod.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/fts/posting_list.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/fts/tokenizer.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/hnsw.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/allocator.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/cell.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/file.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/fts_cell.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/header.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/hnsw_cell.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/index_cell.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/interior_page.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/overflow.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/page.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/pager.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/table_page.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/varint.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/pager/wal.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/parser/create.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/parser/insert.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/parser/mod.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/parser/select.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.0}/src/sql/tokenizer.rs +0 -0
|
@@ -3817,7 +3817,7 @@ dependencies = [
|
|
|
3817
3817
|
|
|
3818
3818
|
[[package]]
|
|
3819
3819
|
name = "sqlrite-ask"
|
|
3820
|
-
version = "0.
|
|
3820
|
+
version = "0.5.0"
|
|
3821
3821
|
dependencies = [
|
|
3822
3822
|
"serde",
|
|
3823
3823
|
"serde_json",
|
|
@@ -3828,7 +3828,7 @@ dependencies = [
|
|
|
3828
3828
|
|
|
3829
3829
|
[[package]]
|
|
3830
3830
|
name = "sqlrite-desktop"
|
|
3831
|
-
version = "0.
|
|
3831
|
+
version = "0.5.0"
|
|
3832
3832
|
dependencies = [
|
|
3833
3833
|
"serde",
|
|
3834
3834
|
"serde_json",
|
|
@@ -3840,7 +3840,7 @@ dependencies = [
|
|
|
3840
3840
|
|
|
3841
3841
|
[[package]]
|
|
3842
3842
|
name = "sqlrite-engine"
|
|
3843
|
-
version = "0.
|
|
3843
|
+
version = "0.5.0"
|
|
3844
3844
|
dependencies = [
|
|
3845
3845
|
"clap",
|
|
3846
3846
|
"env_logger",
|
|
@@ -3857,7 +3857,7 @@ dependencies = [
|
|
|
3857
3857
|
|
|
3858
3858
|
[[package]]
|
|
3859
3859
|
name = "sqlrite-ffi"
|
|
3860
|
-
version = "0.
|
|
3860
|
+
version = "0.5.0"
|
|
3861
3861
|
dependencies = [
|
|
3862
3862
|
"cbindgen",
|
|
3863
3863
|
"serde",
|
|
@@ -3867,7 +3867,7 @@ dependencies = [
|
|
|
3867
3867
|
|
|
3868
3868
|
[[package]]
|
|
3869
3869
|
name = "sqlrite-mcp"
|
|
3870
|
-
version = "0.
|
|
3870
|
+
version = "0.5.0"
|
|
3871
3871
|
dependencies = [
|
|
3872
3872
|
"clap",
|
|
3873
3873
|
"libc",
|
|
@@ -3878,7 +3878,7 @@ dependencies = [
|
|
|
3878
3878
|
|
|
3879
3879
|
[[package]]
|
|
3880
3880
|
name = "sqlrite-nodejs"
|
|
3881
|
-
version = "0.
|
|
3881
|
+
version = "0.5.0"
|
|
3882
3882
|
dependencies = [
|
|
3883
3883
|
"napi",
|
|
3884
3884
|
"napi-build",
|
|
@@ -3888,7 +3888,7 @@ dependencies = [
|
|
|
3888
3888
|
|
|
3889
3889
|
[[package]]
|
|
3890
3890
|
name = "sqlrite-python"
|
|
3891
|
-
version = "0.
|
|
3891
|
+
version = "0.5.0"
|
|
3892
3892
|
dependencies = [
|
|
3893
3893
|
"pyo3",
|
|
3894
3894
|
"sqlrite-engine",
|
|
@@ -27,7 +27,7 @@ resolver = "3"
|
|
|
27
27
|
# `package =` key so the import name stays `sqlrite` internally:
|
|
28
28
|
# sqlrite = { package = "sqlrite-engine", path = "…" }
|
|
29
29
|
name = "sqlrite-engine"
|
|
30
|
-
version = "0.
|
|
30
|
+
version = "0.5.0"
|
|
31
31
|
authors = ["Joao Henrique Machado Silva <joaoh82@gmail.com>"]
|
|
32
32
|
edition = "2024"
|
|
33
33
|
rust-version = "1.85"
|
|
@@ -138,4 +138,4 @@ fs2 = { version = "0.4", optional = true }
|
|
|
138
138
|
# crate publishes to crates.io, and a path-only dep without a
|
|
139
139
|
# version field fails the manifest verification step. See PR #58
|
|
140
140
|
# retrospective in docs/roadmap.md.
|
|
141
|
-
sqlrite-ask = { version = "0.
|
|
141
|
+
sqlrite-ask = { version = "0.5.0", path = "sqlrite-ask", optional = true }
|
|
@@ -201,6 +201,20 @@ After staging, pages that were live before this save but didn't get restaged thi
|
|
|
201
201
|
|
|
202
202
|
Format-version side effect: a save that produces a non-empty freelist promotes the file from v4/v5 to v6 (mirrors Phase 8c's v4→v5 FTS rule). VACUUM clears the freelist but doesn't downgrade — v6 is a strict superset.
|
|
203
203
|
|
|
204
|
+
### Auto-VACUUM trigger (SQLR-10)
|
|
205
|
+
|
|
206
|
+
After SQLR-6, the file still required a manual `VACUUM;` to actually shrink — the freelist absorbed orphan pages but the high-water mark stayed put. SQLR-10 adds a heuristic that fires `vacuum_database` automatically after a page-releasing DDL (`DROP TABLE`, `DROP INDEX`, `ALTER TABLE DROP COLUMN`) when the freelist exceeds a configurable fraction of `page_count`.
|
|
207
|
+
|
|
208
|
+
Configuration lives on `Database::auto_vacuum_threshold: Option<f32>` and is exposed at the connection level via `Connection::set_auto_vacuum_threshold` / `auto_vacuum_threshold`. Defaults: `Some(0.25)` (SQLite parity at 25%); pass `None` to opt out per connection. The threshold is per-`Connection` runtime state and is not persisted in the file header — every reopen starts at the default. A SQL-level `PRAGMA auto_vacuum` is tracked separately (out of scope for SQLR-10).
|
|
209
|
+
|
|
210
|
+
The trigger lives at the end of [`process_command_with_render`](../src/sql/mod.rs), immediately after the auto-save. Order matters: the freelist isn't accurate until the bottom-up rebuild runs during save, so we save first, then check the ratio. The check itself is `freelist::should_auto_vacuum(pager, threshold)`, which:
|
|
211
|
+
|
|
212
|
+
- skips databases under `MIN_PAGES_FOR_AUTO_VACUUM` (16 pages = 64 KiB) so tiny files don't churn,
|
|
213
|
+
- counts both leaf and trunk pages in the freelist (trunks are reclaimable bytes too),
|
|
214
|
+
- returns `true` iff `(leaves + trunks) / page_count > threshold`.
|
|
215
|
+
|
|
216
|
+
Auto-VACUUM is also skipped mid-transaction (no save → freelist is stale and the compact would publish in-flight work) and on in-memory databases (no file). The path bypasses `executor::execute_vacuum` — that wrapper builds a user-facing status string and rejects in-transaction calls, both wrong for a silent maintenance hook — and calls `vacuum_database` directly.
|
|
217
|
+
|
|
204
218
|
## What it doesn't do (yet)
|
|
205
219
|
|
|
206
220
|
- **No LRU eviction.** `on_disk` + `wal_cache` together grow with the page count. For a 1 GiB database, that's ~1 GiB of page cache. Bounded cache is future work.
|
|
@@ -260,7 +260,7 @@ DROP TABLE [IF EXISTS] <table>;
|
|
|
260
260
|
- Reserved-name rejection: `DROP TABLE sqlrite_master` errors with the same message `CREATE TABLE` uses.
|
|
261
261
|
- All indexes attached to the table (auto, explicit, HNSW, FTS) disappear with the table — they live inside the `Table` struct and ride along.
|
|
262
262
|
- Without `IF EXISTS`, dropping a table that doesn't exist errors. With it, that's a benign 0-tables-dropped no-op.
|
|
263
|
-
- **Disk pages move onto the freelist.** Pages the dropped table occupied are pushed onto a persisted free-page list (SQLR-6) so subsequent `CREATE TABLE` or inserts can reuse them. The file
|
|
263
|
+
- **Disk pages move onto the freelist.** Pages the dropped table occupied are pushed onto a persisted free-page list (SQLR-6) so subsequent `CREATE TABLE` or inserts can reuse them. The file shrinks automatically when the freelist crosses 25% of `page_count` (SQLR-10 auto-VACUUM, default-on); embedders that need the prior "manual `VACUUM;` only" behavior can call `Connection::set_auto_vacuum_threshold(None)` at open time.
|
|
264
264
|
|
|
265
265
|
---
|
|
266
266
|
|
|
@@ -445,6 +445,24 @@ Compacts the database file: rewrites every live table, index, HNSW graph, FTS po
|
|
|
445
445
|
|
|
446
446
|
When to run it: any time after a string of `DROP TABLE` / `DROP INDEX` / `ALTER TABLE DROP COLUMN` operations if you care about file size. SQLRite reuses freelist pages on subsequent inserts, so a write-heavy workload may not need VACUUM at all — its main use is reclaiming space when you don't expect to grow back.
|
|
447
447
|
|
|
448
|
+
### Auto-VACUUM (SQLR-10)
|
|
449
|
+
|
|
450
|
+
Manual `VACUUM;` is rarely needed in practice: by default, every page-releasing DDL (`DROP TABLE`, `DROP INDEX`, `ALTER TABLE DROP COLUMN`) checks the freelist after committing and runs `vacuum_database` automatically when the freelist exceeds **25%** of `page_count` (SQLite parity). The trigger:
|
|
451
|
+
|
|
452
|
+
- skips databases under 16 pages (64 KiB) so tiny files don't churn,
|
|
453
|
+
- skips inside an explicit transaction (the freelist isn't accurate until `COMMIT`),
|
|
454
|
+
- skips on in-memory and read-only databases.
|
|
455
|
+
|
|
456
|
+
The threshold is tunable per-connection from Rust:
|
|
457
|
+
|
|
458
|
+
```rust
|
|
459
|
+
let mut conn = Connection::open("db.sqlrite")?;
|
|
460
|
+
conn.set_auto_vacuum_threshold(Some(0.5))?; // fire only when freelist > 50%
|
|
461
|
+
conn.set_auto_vacuum_threshold(None)?; // disable entirely (manual VACUUM only)
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
The setting is per-`Connection` runtime state — it's not persisted in the file header, so every reopen starts at the default `Some(0.25)`. A SQL-level `PRAGMA auto_vacuum` knob is on the roadmap but not yet implemented (SDK consumers currently configure it via the per-binding glue or fall back to the default).
|
|
465
|
+
|
|
448
466
|
---
|
|
449
467
|
|
|
450
468
|
## Read-only databases
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlrite"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Python bindings for SQLRite — a small, embeddable SQLite clone written in Rust."
|
|
9
9
|
authors = [{ name = "Joao Henrique Machado Silva", email = "joaoh82@gmail.com" }]
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# Published to crates.io as `sqlrite-ask`. Joins the lockstep release
|
|
11
11
|
# wave (`sqlrite-ask-vX.Y.Z` tag) — see `docs/release-plan.md`.
|
|
12
12
|
name = "sqlrite-ask"
|
|
13
|
-
version = "0.
|
|
13
|
+
version = "0.5.0"
|
|
14
14
|
authors = ["Joao Henrique Machado Silva <joaoh82@gmail.com>"]
|
|
15
15
|
edition = "2024"
|
|
16
16
|
rust-version = "1.85"
|
|
@@ -153,6 +153,30 @@ impl Connection {
|
|
|
153
153
|
self.db.in_transaction()
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/// Returns the current auto-VACUUM threshold (SQLR-10). After a
|
|
157
|
+
/// page-releasing DDL (DROP TABLE / DROP INDEX / ALTER TABLE DROP
|
|
158
|
+
/// COLUMN) commits, the engine compacts the file in place if the
|
|
159
|
+
/// freelist exceeds this fraction of `page_count`. New connections
|
|
160
|
+
/// default to `Some(0.25)` (SQLite parity); `None` means the
|
|
161
|
+
/// trigger is disabled. See [`Connection::set_auto_vacuum_threshold`].
|
|
162
|
+
pub fn auto_vacuum_threshold(&self) -> Option<f32> {
|
|
163
|
+
self.db.auto_vacuum_threshold()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Sets the auto-VACUUM threshold (SQLR-10). `Some(t)` with `t` in
|
|
167
|
+
/// `0.0..=1.0` arms the trigger; `None` disables it. Values outside
|
|
168
|
+
/// `0.0..=1.0` (or NaN / infinite) return a typed error rather than
|
|
169
|
+
/// silently saturating. The setting is per-connection runtime
|
|
170
|
+
/// state — closing the connection drops it; new connections start
|
|
171
|
+
/// at the default `Some(0.25)`.
|
|
172
|
+
///
|
|
173
|
+
/// Calling this on an in-memory or read-only database is allowed
|
|
174
|
+
/// (it just won't fire — there's nothing to compact / no writes
|
|
175
|
+
/// will reach the trigger).
|
|
176
|
+
pub fn set_auto_vacuum_threshold(&mut self, threshold: Option<f32>) -> Result<()> {
|
|
177
|
+
self.db.set_auto_vacuum_threshold(threshold)
|
|
178
|
+
}
|
|
179
|
+
|
|
156
180
|
/// Returns `true` if the connection was opened read-only. Mutating
|
|
157
181
|
/// statements on a read-only connection return a typed error.
|
|
158
182
|
pub fn is_read_only(&self) -> bool {
|
|
@@ -634,6 +658,37 @@ mod tests {
|
|
|
634
658
|
assert!(format!("{err}").contains("SELECT"));
|
|
635
659
|
}
|
|
636
660
|
|
|
661
|
+
/// SQLR-10: fresh connections expose the SQLite-parity 25% default,
|
|
662
|
+
/// the setter validates its input, and `None` opts out cleanly.
|
|
663
|
+
#[test]
|
|
664
|
+
fn auto_vacuum_threshold_default_and_setter() {
|
|
665
|
+
let mut conn = Connection::open_in_memory().unwrap();
|
|
666
|
+
assert_eq!(
|
|
667
|
+
conn.auto_vacuum_threshold(),
|
|
668
|
+
Some(0.25),
|
|
669
|
+
"fresh connection should ship with the SQLite-parity default"
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
conn.set_auto_vacuum_threshold(None).unwrap();
|
|
673
|
+
assert_eq!(conn.auto_vacuum_threshold(), None);
|
|
674
|
+
|
|
675
|
+
conn.set_auto_vacuum_threshold(Some(0.5)).unwrap();
|
|
676
|
+
assert_eq!(conn.auto_vacuum_threshold(), Some(0.5));
|
|
677
|
+
|
|
678
|
+
// Out-of-range values must be rejected with a typed error and
|
|
679
|
+
// must not stomp the previously-set value.
|
|
680
|
+
let err = conn.set_auto_vacuum_threshold(Some(1.5)).unwrap_err();
|
|
681
|
+
assert!(
|
|
682
|
+
format!("{err}").contains("auto_vacuum_threshold"),
|
|
683
|
+
"expected typed range error, got: {err}"
|
|
684
|
+
);
|
|
685
|
+
assert_eq!(
|
|
686
|
+
conn.auto_vacuum_threshold(),
|
|
687
|
+
Some(0.5),
|
|
688
|
+
"rejected setter call must not mutate the threshold"
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
637
692
|
#[test]
|
|
638
693
|
fn index_out_of_bounds_errors_cleanly() {
|
|
639
694
|
let mut conn = Connection::open_in_memory().unwrap();
|
|
@@ -14,6 +14,13 @@ pub struct TxnSnapshot {
|
|
|
14
14
|
pub(crate) tables: HashMap<String, Table>,
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/// Default fraction of free pages that triggers an auto-VACUUM after
|
|
18
|
+
/// a page-releasing DDL (DROP TABLE / DROP INDEX / ALTER TABLE DROP
|
|
19
|
+
/// COLUMN). Matches SQLite's classic 25% heuristic. Override per
|
|
20
|
+
/// connection with [`Database::set_auto_vacuum_threshold`] (or
|
|
21
|
+
/// `Connection::set_auto_vacuum_threshold`); pass `None` to disable.
|
|
22
|
+
pub const DEFAULT_AUTO_VACUUM_THRESHOLD: f32 = 0.25;
|
|
23
|
+
|
|
17
24
|
/// The database is represented by this structure.assert_eq!
|
|
18
25
|
#[derive(Debug)]
|
|
19
26
|
pub struct Database {
|
|
@@ -36,6 +43,14 @@ pub struct Database {
|
|
|
36
43
|
/// - nested `BEGIN` is rejected
|
|
37
44
|
/// - `ROLLBACK` restores `tables` from the snapshot
|
|
38
45
|
pub txn: Option<TxnSnapshot>,
|
|
46
|
+
/// Auto-VACUUM trigger (SQLR-10). After a page-releasing DDL
|
|
47
|
+
/// (DROP TABLE / DROP INDEX / ALTER TABLE DROP COLUMN) commits and
|
|
48
|
+
/// flushes, if the freelist exceeds this fraction of `page_count`
|
|
49
|
+
/// the engine quietly compacts the file. `None` disables the
|
|
50
|
+
/// trigger; defaults to `Some(DEFAULT_AUTO_VACUUM_THRESHOLD)`
|
|
51
|
+
/// (SQLite parity at 25%). Per-connection runtime state — not
|
|
52
|
+
/// persisted across reopens.
|
|
53
|
+
pub auto_vacuum_threshold: Option<f32>,
|
|
39
54
|
}
|
|
40
55
|
|
|
41
56
|
impl Database {
|
|
@@ -54,9 +69,34 @@ impl Database {
|
|
|
54
69
|
source_path: None,
|
|
55
70
|
pager: None,
|
|
56
71
|
txn: None,
|
|
72
|
+
auto_vacuum_threshold: Some(DEFAULT_AUTO_VACUUM_THRESHOLD),
|
|
57
73
|
}
|
|
58
74
|
}
|
|
59
75
|
|
|
76
|
+
/// Returns the current auto-VACUUM threshold, or `None` if disabled.
|
|
77
|
+
/// See [`Database::set_auto_vacuum_threshold`] for semantics.
|
|
78
|
+
pub fn auto_vacuum_threshold(&self) -> Option<f32> {
|
|
79
|
+
self.auto_vacuum_threshold
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Sets the auto-VACUUM threshold (SQLR-10). `Some(t)` with `t` in
|
|
83
|
+
/// `0.0..=1.0` arms the trigger: after a page-releasing DDL
|
|
84
|
+
/// commits, if the freelist exceeds `t * page_count` the engine
|
|
85
|
+
/// runs a full-file compact. `None` disables the trigger. Values
|
|
86
|
+
/// outside `0.0..=1.0` (or NaN / infinite) return a typed error
|
|
87
|
+
/// rather than silently saturating.
|
|
88
|
+
pub fn set_auto_vacuum_threshold(&mut self, threshold: Option<f32>) -> Result<()> {
|
|
89
|
+
if let Some(t) = threshold {
|
|
90
|
+
if !t.is_finite() || !(0.0..=1.0).contains(&t) {
|
|
91
|
+
return Err(SQLRiteError::General(format!(
|
|
92
|
+
"auto_vacuum_threshold must be in 0.0..=1.0, got {t}"
|
|
93
|
+
)));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
self.auto_vacuum_threshold = threshold;
|
|
97
|
+
Ok(())
|
|
98
|
+
}
|
|
99
|
+
|
|
60
100
|
/// Returns true if the database contains a table with the specified key as a table name.
|
|
61
101
|
///
|
|
62
102
|
pub fn contains_table(&self, table_name: String) -> bool {
|
|
@@ -10,7 +10,7 @@ use parser::create::CreateQuery;
|
|
|
10
10
|
use parser::insert::InsertQuery;
|
|
11
11
|
use parser::select::SelectQuery;
|
|
12
12
|
|
|
13
|
-
use sqlparser::ast::{ObjectType, Statement};
|
|
13
|
+
use sqlparser::ast::{AlterTableOperation, ObjectType, Statement};
|
|
14
14
|
use sqlparser::dialect::SQLiteDialect;
|
|
15
15
|
use sqlparser::parser::{Parser, ParserError};
|
|
16
16
|
|
|
@@ -176,6 +176,22 @@ pub fn process_command_with_render(query: &str, db: &mut Database) -> Result<Com
|
|
|
176
176
|
);
|
|
177
177
|
let is_vacuum = matches!(&query, Statement::Vacuum(_));
|
|
178
178
|
|
|
179
|
+
// SQLR-10: statements that release pages onto the freelist.
|
|
180
|
+
// After the auto-save flushes them, we'll consult
|
|
181
|
+
// `db.auto_vacuum_threshold` and possibly compact in place.
|
|
182
|
+
// ALTER TABLE here matches only DROP COLUMN — RENAME / ADD COLUMN
|
|
183
|
+
// don't grow the freelist, so they shouldn't pay the trigger cost.
|
|
184
|
+
let releases_pages = match &query {
|
|
185
|
+
Statement::Drop { object_type, .. } => {
|
|
186
|
+
matches!(object_type, ObjectType::Table | ObjectType::Index)
|
|
187
|
+
}
|
|
188
|
+
Statement::AlterTable(alter) => alter
|
|
189
|
+
.operations
|
|
190
|
+
.iter()
|
|
191
|
+
.any(|op| matches!(op, AlterTableOperation::DropColumn { .. })),
|
|
192
|
+
_ => false,
|
|
193
|
+
};
|
|
194
|
+
|
|
179
195
|
// Early-reject mutations on a read-only database before they touch
|
|
180
196
|
// in-memory state. Phase 4e: without this, a user running INSERT
|
|
181
197
|
// on a `--readonly` REPL would see the row appear in the printed
|
|
@@ -389,6 +405,30 @@ pub fn process_command_with_render(query: &str, db: &mut Database) -> Result<Com
|
|
|
389
405
|
pager::save_database(db, &path)?;
|
|
390
406
|
}
|
|
391
407
|
|
|
408
|
+
// SQLR-10 auto-VACUUM trigger. Runs *after* the auto-save above so
|
|
409
|
+
// the orphaned pages from the just-executed DROP/ALTER have actually
|
|
410
|
+
// landed on the freelist (the bottom-up rebuild populates it during
|
|
411
|
+
// save). Skipped mid-transaction (no commit yet → no save → freelist
|
|
412
|
+
// is stale), on in-memory DBs (nothing to compact), and when the
|
|
413
|
+
// user has explicitly disabled the trigger via
|
|
414
|
+
// `set_auto_vacuum_threshold(None)`. We deliberately bypass
|
|
415
|
+
// `executor::execute_vacuum` and call `pager::vacuum_database`
|
|
416
|
+
// directly: the executor wrapper builds a user-facing status string
|
|
417
|
+
// and rejects in-transaction calls — both wrong for this silent
|
|
418
|
+
// maintenance path.
|
|
419
|
+
if releases_pages && !db.in_transaction() {
|
|
420
|
+
if let (Some(threshold), Some(path)) = (db.auto_vacuum_threshold(), db.source_path.clone())
|
|
421
|
+
{
|
|
422
|
+
let should = match db.pager.as_ref() {
|
|
423
|
+
Some(p) => pager::freelist::should_auto_vacuum(p, threshold)?,
|
|
424
|
+
None => false,
|
|
425
|
+
};
|
|
426
|
+
if should {
|
|
427
|
+
pager::vacuum_database(db, &path)?;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
392
432
|
Ok(CommandOutput {
|
|
393
433
|
status: message,
|
|
394
434
|
rendered,
|
|
@@ -202,6 +202,39 @@ pub fn freelist_to_deque(leaves: Vec<u32>) -> VecDeque<u32> {
|
|
|
202
202
|
VecDeque::from(sorted)
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/// Auto-VACUUM (SQLR-10) does not fire on databases below this many
|
|
206
|
+
/// pages. The 4 KiB page size makes 16 pages = 64 KiB — small enough
|
|
207
|
+
/// that the cost of a full-file rewrite is negligible if the user
|
|
208
|
+
/// genuinely wants it (manual `VACUUM;` still works), but large enough
|
|
209
|
+
/// that single-table churn doesn't blow past the threshold and trigger
|
|
210
|
+
/// a noisy compact every few statements.
|
|
211
|
+
pub const MIN_PAGES_FOR_AUTO_VACUUM: u32 = 16;
|
|
212
|
+
|
|
213
|
+
/// Returns `true` if the on-disk freelist (counting both leaf and
|
|
214
|
+
/// trunk pages — they're all reclaimable bytes) exceeds `threshold`
|
|
215
|
+
/// of `header.page_count`, i.e. the file is bloated enough to be
|
|
216
|
+
/// worth compacting. Returns `false` for tiny databases (under
|
|
217
|
+
/// [`MIN_PAGES_FOR_AUTO_VACUUM`]) and for empty freelists, both as
|
|
218
|
+
/// fast paths to keep the auto-VACUUM hook cheap on the common case.
|
|
219
|
+
///
|
|
220
|
+
/// This is a read-only inspection of the pager's current header and
|
|
221
|
+
/// freelist chain — it does not mutate any state. The caller is
|
|
222
|
+
/// responsible for actually invoking
|
|
223
|
+
/// [`crate::sql::pager::vacuum_database`] when this returns `true`.
|
|
224
|
+
pub fn should_auto_vacuum(pager: &Pager, threshold: f32) -> Result<bool> {
|
|
225
|
+
let header = pager.header();
|
|
226
|
+
if header.page_count < MIN_PAGES_FOR_AUTO_VACUUM {
|
|
227
|
+
return Ok(false);
|
|
228
|
+
}
|
|
229
|
+
if header.freelist_head == 0 {
|
|
230
|
+
return Ok(false);
|
|
231
|
+
}
|
|
232
|
+
let (leaves, trunks) = read_freelist(pager, header.freelist_head)?;
|
|
233
|
+
let free_pages = leaves.len() + trunks.len();
|
|
234
|
+
let ratio = free_pages as f32 / header.page_count as f32;
|
|
235
|
+
Ok(ratio > threshold)
|
|
236
|
+
}
|
|
237
|
+
|
|
205
238
|
#[cfg(test)]
|
|
206
239
|
mod tests {
|
|
207
240
|
use super::*;
|
|
@@ -1896,6 +1896,7 @@ fn emit_interior(pager: &mut Pager, page_num: u32, interior: &InteriorPage) {
|
|
|
1896
1896
|
#[cfg(test)]
|
|
1897
1897
|
mod tests {
|
|
1898
1898
|
use super::*;
|
|
1899
|
+
use crate::sql::pager::freelist::MIN_PAGES_FOR_AUTO_VACUUM;
|
|
1899
1900
|
use crate::sql::process_command;
|
|
1900
1901
|
|
|
1901
1902
|
fn seed_db() -> Database {
|
|
@@ -3308,6 +3309,306 @@ mod tests {
|
|
|
3308
3309
|
cleanup(&path);
|
|
3309
3310
|
}
|
|
3310
3311
|
|
|
3312
|
+
// ---- SQLR-10: auto-VACUUM trigger after page-releasing DDL ----
|
|
3313
|
+
|
|
3314
|
+
/// Builds a file-backed DB with one small "keep" table and one
|
|
3315
|
+
/// large "bloat" table, sized so the post-drop freelist will
|
|
3316
|
+
/// comfortably cross the default 25% threshold and the
|
|
3317
|
+
/// `MIN_PAGES_FOR_AUTO_VACUUM` floor (16 pages). Used by the
|
|
3318
|
+
/// auto-VACUUM happy-path tests.
|
|
3319
|
+
fn auto_vacuum_setup(path: &std::path::Path) -> Database {
|
|
3320
|
+
let mut db = Database::new("av".to_string());
|
|
3321
|
+
db.source_path = Some(path.to_path_buf());
|
|
3322
|
+
process_command(
|
|
3323
|
+
"CREATE TABLE keep (id INTEGER PRIMARY KEY, n INTEGER);",
|
|
3324
|
+
&mut db,
|
|
3325
|
+
)
|
|
3326
|
+
.unwrap();
|
|
3327
|
+
process_command("INSERT INTO keep (n) VALUES (1);", &mut db).unwrap();
|
|
3328
|
+
process_command(
|
|
3329
|
+
"CREATE TABLE bloat (id INTEGER PRIMARY KEY, payload TEXT);",
|
|
3330
|
+
&mut db,
|
|
3331
|
+
)
|
|
3332
|
+
.unwrap();
|
|
3333
|
+
// Wrap the bulk insert in a transaction so we pay one save at
|
|
3334
|
+
// COMMIT instead of 5000 round-trips through auto-save.
|
|
3335
|
+
process_command("BEGIN;", &mut db).unwrap();
|
|
3336
|
+
for i in 0..5000 {
|
|
3337
|
+
process_command(
|
|
3338
|
+
&format!("INSERT INTO bloat (payload) VALUES ('p-{i:08}');"),
|
|
3339
|
+
&mut db,
|
|
3340
|
+
)
|
|
3341
|
+
.unwrap();
|
|
3342
|
+
}
|
|
3343
|
+
process_command("COMMIT;", &mut db).unwrap();
|
|
3344
|
+
db
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
/// Default threshold (0.25) is engaged for fresh `Database`s and
|
|
3348
|
+
/// fires when a `DROP TABLE` orphans enough pages — file shrinks
|
|
3349
|
+
/// without anyone calling `VACUUM;`.
|
|
3350
|
+
#[test]
|
|
3351
|
+
fn auto_vacuum_default_threshold_triggers_on_drop_table() {
|
|
3352
|
+
let path = tmp_path("av_default_drop_table");
|
|
3353
|
+
let mut db = auto_vacuum_setup(&path);
|
|
3354
|
+
// Sanity: setup respects the shipped default.
|
|
3355
|
+
assert_eq!(db.auto_vacuum_threshold(), Some(0.25));
|
|
3356
|
+
|
|
3357
|
+
// Checkpoint before measuring `size_before` so the bloat actually
|
|
3358
|
+
// lives in the main file and not just the WAL — otherwise
|
|
3359
|
+
// `size_before` is the bare 2-page header and any post-vacuum
|
|
3360
|
+
// checkpoint will look like the file *grew*.
|
|
3361
|
+
if let Some(p) = db.pager.as_mut() {
|
|
3362
|
+
let _ = p.checkpoint();
|
|
3363
|
+
}
|
|
3364
|
+
let pages_before = db.pager.as_ref().unwrap().header().page_count;
|
|
3365
|
+
let size_before = std::fs::metadata(&path).unwrap().len();
|
|
3366
|
+
assert!(
|
|
3367
|
+
pages_before >= MIN_PAGES_FOR_AUTO_VACUUM,
|
|
3368
|
+
"setup should produce >= MIN_PAGES_FOR_AUTO_VACUUM ({MIN_PAGES_FOR_AUTO_VACUUM}) \
|
|
3369
|
+
pages so the floor doesn't suppress the trigger; got {pages_before}"
|
|
3370
|
+
);
|
|
3371
|
+
|
|
3372
|
+
// Drop the bloat table — freelist should pass 25% of page_count
|
|
3373
|
+
// and the auto-VACUUM hook should compact in place. Note: no
|
|
3374
|
+
// explicit `VACUUM;` statement is issued.
|
|
3375
|
+
process_command("DROP TABLE bloat;", &mut db).expect("drop");
|
|
3376
|
+
|
|
3377
|
+
let pages_after = db.pager.as_ref().unwrap().header().page_count;
|
|
3378
|
+
let head_after = db.pager.as_ref().unwrap().header().freelist_head;
|
|
3379
|
+
// Second checkpoint so the post-vacuum file shrinks on disk
|
|
3380
|
+
// (auto-VACUUM stages the compact through WAL just like manual
|
|
3381
|
+
// VACUUM does).
|
|
3382
|
+
if let Some(p) = db.pager.as_mut() {
|
|
3383
|
+
let _ = p.checkpoint();
|
|
3384
|
+
}
|
|
3385
|
+
let size_after = std::fs::metadata(&path).unwrap().len();
|
|
3386
|
+
|
|
3387
|
+
assert!(
|
|
3388
|
+
pages_after < pages_before,
|
|
3389
|
+
"auto-VACUUM must reduce page_count: was {pages_before}, now {pages_after}"
|
|
3390
|
+
);
|
|
3391
|
+
assert_eq!(head_after, 0, "auto-VACUUM must clear the freelist");
|
|
3392
|
+
assert!(
|
|
3393
|
+
size_after < size_before,
|
|
3394
|
+
"auto-VACUUM must shrink the file on disk: was {size_before}, now {size_after}"
|
|
3395
|
+
);
|
|
3396
|
+
|
|
3397
|
+
cleanup(&path);
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
/// Setting the threshold to `None` disables the trigger entirely:
|
|
3401
|
+
/// the same workload that shrinks under the default leaves the file
|
|
3402
|
+
/// at its high-water mark.
|
|
3403
|
+
#[test]
|
|
3404
|
+
fn auto_vacuum_disabled_keeps_file_at_hwm() {
|
|
3405
|
+
let path = tmp_path("av_disabled");
|
|
3406
|
+
let mut db = auto_vacuum_setup(&path);
|
|
3407
|
+
db.set_auto_vacuum_threshold(None).expect("disable");
|
|
3408
|
+
assert_eq!(db.auto_vacuum_threshold(), None);
|
|
3409
|
+
|
|
3410
|
+
let pages_before = db.pager.as_ref().unwrap().header().page_count;
|
|
3411
|
+
|
|
3412
|
+
process_command("DROP TABLE bloat;", &mut db).expect("drop");
|
|
3413
|
+
|
|
3414
|
+
let pages_after = db.pager.as_ref().unwrap().header().page_count;
|
|
3415
|
+
let head_after = db.pager.as_ref().unwrap().header().freelist_head;
|
|
3416
|
+
assert_eq!(
|
|
3417
|
+
pages_after, pages_before,
|
|
3418
|
+
"with auto-VACUUM disabled, drop must keep page_count at the HWM"
|
|
3419
|
+
);
|
|
3420
|
+
assert!(
|
|
3421
|
+
head_after != 0,
|
|
3422
|
+
"drop must still populate the freelist (manual VACUUM would be needed to reclaim)"
|
|
3423
|
+
);
|
|
3424
|
+
|
|
3425
|
+
cleanup(&path);
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
/// `DROP INDEX` is the second of three page-releasing DDL paths
|
|
3429
|
+
/// covered by SQLR-10. We bloat the freelist via a separate
|
|
3430
|
+
/// `DROP TABLE` first (with auto-VACUUM disabled so it doesn't
|
|
3431
|
+
/// compact early), then re-arm the trigger and drop a small index
|
|
3432
|
+
/// — the cumulative freelist crosses 25% on the index drop and
|
|
3433
|
+
/// auto-VACUUM fires.
|
|
3434
|
+
///
|
|
3435
|
+
/// The detour around bloat is necessary because building a
|
|
3436
|
+
/// secondary index on a 5000-row column would need multi-level
|
|
3437
|
+
/// interior nodes, and the cell-decoder's interior-page support
|
|
3438
|
+
/// is a separate work item from SQLR-10.
|
|
3439
|
+
#[test]
|
|
3440
|
+
fn auto_vacuum_triggers_on_drop_index() {
|
|
3441
|
+
let path = tmp_path("av_drop_index");
|
|
3442
|
+
let mut db = auto_vacuum_setup(&path);
|
|
3443
|
+
|
|
3444
|
+
// Phase 1: drop the bloat table with auto-VACUUM disabled so
|
|
3445
|
+
// its pages land on the freelist without being reclaimed.
|
|
3446
|
+
db.set_auto_vacuum_threshold(None).expect("disable");
|
|
3447
|
+
process_command("DROP TABLE bloat;", &mut db).expect("drop bloat");
|
|
3448
|
+
let pages_after_bloat_drop = db.pager.as_ref().unwrap().header().page_count;
|
|
3449
|
+
let head_after_bloat_drop = db.pager.as_ref().unwrap().header().freelist_head;
|
|
3450
|
+
assert!(
|
|
3451
|
+
head_after_bloat_drop != 0,
|
|
3452
|
+
"bloat drop must populate the freelist (else later index drop won't trip the threshold)"
|
|
3453
|
+
);
|
|
3454
|
+
|
|
3455
|
+
// Phase 2: a small index on the surviving `keep` table. The
|
|
3456
|
+
// index reuses one page from the freelist (which is fine —
|
|
3457
|
+
// freelist still holds plenty more).
|
|
3458
|
+
process_command("CREATE INDEX idx_keep_n ON keep (n);", &mut db).expect("create idx");
|
|
3459
|
+
|
|
3460
|
+
// Phase 3: re-arm the trigger and drop the index. The freelist
|
|
3461
|
+
// is already heavily populated from phase 1; this drop just
|
|
3462
|
+
// adds the index page on top, keeping the ratio well above
|
|
3463
|
+
// 25%, so auto-VACUUM should fire.
|
|
3464
|
+
db.set_auto_vacuum_threshold(Some(0.25)).expect("re-arm");
|
|
3465
|
+
process_command("DROP INDEX idx_keep_n;", &mut db).expect("drop index");
|
|
3466
|
+
|
|
3467
|
+
let pages_after = db.pager.as_ref().unwrap().header().page_count;
|
|
3468
|
+
let head_after = db.pager.as_ref().unwrap().header().freelist_head;
|
|
3469
|
+
assert!(
|
|
3470
|
+
pages_after < pages_after_bloat_drop,
|
|
3471
|
+
"DROP INDEX should fire auto-VACUUM and reduce page_count: \
|
|
3472
|
+
was {pages_after_bloat_drop}, now {pages_after}"
|
|
3473
|
+
);
|
|
3474
|
+
assert_eq!(
|
|
3475
|
+
head_after, 0,
|
|
3476
|
+
"auto-VACUUM after DROP INDEX must clear the freelist"
|
|
3477
|
+
);
|
|
3478
|
+
|
|
3479
|
+
cleanup(&path);
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
/// `ALTER TABLE … DROP COLUMN` releases pages too — the third path
|
|
3483
|
+
/// the SQLR-10 trigger covers.
|
|
3484
|
+
#[test]
|
|
3485
|
+
fn auto_vacuum_triggers_on_alter_drop_column() {
|
|
3486
|
+
let path = tmp_path("av_alter_drop_col");
|
|
3487
|
+
let mut db = auto_vacuum_setup(&path);
|
|
3488
|
+
let pages_before = db.pager.as_ref().unwrap().header().page_count;
|
|
3489
|
+
|
|
3490
|
+
// Drop the wide `payload` column — this rewrites every row in
|
|
3491
|
+
// `bloat` without the column, so the old leaf pages get freed.
|
|
3492
|
+
process_command("ALTER TABLE bloat DROP COLUMN payload;", &mut db).expect("alter drop");
|
|
3493
|
+
|
|
3494
|
+
let pages_after = db.pager.as_ref().unwrap().header().page_count;
|
|
3495
|
+
assert!(
|
|
3496
|
+
pages_after < pages_before,
|
|
3497
|
+
"ALTER TABLE DROP COLUMN should fire auto-VACUUM and reduce page_count: \
|
|
3498
|
+
was {pages_before}, now {pages_after}"
|
|
3499
|
+
);
|
|
3500
|
+
assert_eq!(db.pager.as_ref().unwrap().header().freelist_head, 0);
|
|
3501
|
+
|
|
3502
|
+
cleanup(&path);
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
/// A high threshold (0.99) suppresses the trigger when the freelist
|
|
3506
|
+
/// ratio is well below it — the file stays at HWM.
|
|
3507
|
+
#[test]
|
|
3508
|
+
fn auto_vacuum_skips_below_threshold() {
|
|
3509
|
+
let path = tmp_path("av_below_threshold");
|
|
3510
|
+
let mut db = auto_vacuum_setup(&path);
|
|
3511
|
+
db.set_auto_vacuum_threshold(Some(0.99)).expect("set");
|
|
3512
|
+
|
|
3513
|
+
let pages_before = db.pager.as_ref().unwrap().header().page_count;
|
|
3514
|
+
|
|
3515
|
+
process_command("DROP TABLE bloat;", &mut db).expect("drop");
|
|
3516
|
+
|
|
3517
|
+
let pages_after = db.pager.as_ref().unwrap().header().page_count;
|
|
3518
|
+
assert_eq!(
|
|
3519
|
+
pages_after, pages_before,
|
|
3520
|
+
"freelist ratio after a single drop is far below 0.99 — \
|
|
3521
|
+
page_count must stay at the HWM"
|
|
3522
|
+
);
|
|
3523
|
+
assert!(
|
|
3524
|
+
db.pager.as_ref().unwrap().header().freelist_head != 0,
|
|
3525
|
+
"drop must still populate the freelist"
|
|
3526
|
+
);
|
|
3527
|
+
|
|
3528
|
+
cleanup(&path);
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
/// Inside an explicit transaction, the page-releasing DDL doesn't
|
|
3532
|
+
/// flush to disk yet — the freelist isn't accurate, so the trigger
|
|
3533
|
+
/// must skip. The compact would also publish in-flight work out of
|
|
3534
|
+
/// band, which is exactly what the manual `VACUUM;` rejection
|
|
3535
|
+
/// inside a txn already prevents.
|
|
3536
|
+
#[test]
|
|
3537
|
+
fn auto_vacuum_skips_inside_transaction() {
|
|
3538
|
+
let path = tmp_path("av_in_txn");
|
|
3539
|
+
let mut db = auto_vacuum_setup(&path);
|
|
3540
|
+
let pages_before = db.pager.as_ref().unwrap().header().page_count;
|
|
3541
|
+
|
|
3542
|
+
process_command("BEGIN;", &mut db).expect("begin");
|
|
3543
|
+
process_command("DROP TABLE bloat;", &mut db).expect("drop in txn");
|
|
3544
|
+
// Mid-transaction: no save has occurred, so the on-disk
|
|
3545
|
+
// freelist_head must be unchanged and page_count must not have
|
|
3546
|
+
// shifted from a sneaky compact.
|
|
3547
|
+
let pages_mid = db.pager.as_ref().unwrap().header().page_count;
|
|
3548
|
+
assert_eq!(
|
|
3549
|
+
pages_mid, pages_before,
|
|
3550
|
+
"auto-VACUUM must not fire mid-transaction"
|
|
3551
|
+
);
|
|
3552
|
+
|
|
3553
|
+
process_command("ROLLBACK;", &mut db).expect("rollback");
|
|
3554
|
+
cleanup(&path);
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
/// Tiny databases (under `MIN_PAGES_FOR_AUTO_VACUUM`) skip the
|
|
3558
|
+
/// trigger even if the ratio would otherwise qualify — the cost of
|
|
3559
|
+
/// rewriting a 64 KiB file isn't worth the few bytes reclaimed.
|
|
3560
|
+
#[test]
|
|
3561
|
+
fn auto_vacuum_skips_under_min_pages_floor() {
|
|
3562
|
+
let path = tmp_path("av_under_floor");
|
|
3563
|
+
let mut db = seed_db(); // small: just users + notes, ~5 pages
|
|
3564
|
+
db.source_path = Some(path.clone());
|
|
3565
|
+
save_database(&mut db, &path).expect("save");
|
|
3566
|
+
// Confirm we're below the floor so the test is meaningful.
|
|
3567
|
+
let pages_before = db.pager.as_ref().unwrap().header().page_count;
|
|
3568
|
+
assert!(
|
|
3569
|
+
pages_before < MIN_PAGES_FOR_AUTO_VACUUM,
|
|
3570
|
+
"test setup is too large: floor would not apply (got {pages_before} pages, \
|
|
3571
|
+
floor is {MIN_PAGES_FOR_AUTO_VACUUM})"
|
|
3572
|
+
);
|
|
3573
|
+
|
|
3574
|
+
process_command("DROP TABLE users;", &mut db).expect("drop");
|
|
3575
|
+
|
|
3576
|
+
let pages_after = db.pager.as_ref().unwrap().header().page_count;
|
|
3577
|
+
assert_eq!(
|
|
3578
|
+
pages_after, pages_before,
|
|
3579
|
+
"below MIN_PAGES_FOR_AUTO_VACUUM, drop must not trigger compaction"
|
|
3580
|
+
);
|
|
3581
|
+
assert!(
|
|
3582
|
+
db.pager.as_ref().unwrap().header().freelist_head != 0,
|
|
3583
|
+
"drop must still populate the freelist normally"
|
|
3584
|
+
);
|
|
3585
|
+
|
|
3586
|
+
cleanup(&path);
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
/// Setter rejects NaN, infinities, and values outside `0.0..=1.0`
|
|
3590
|
+
/// rather than silently saturating.
|
|
3591
|
+
#[test]
|
|
3592
|
+
fn set_auto_vacuum_threshold_rejects_out_of_range() {
|
|
3593
|
+
let mut db = Database::new("t".to_string());
|
|
3594
|
+
for bad in [-0.01_f32, 1.01, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
|
|
3595
|
+
let err = db.set_auto_vacuum_threshold(Some(bad)).unwrap_err();
|
|
3596
|
+
assert!(
|
|
3597
|
+
format!("{err}").contains("auto_vacuum_threshold"),
|
|
3598
|
+
"expected a typed range error for {bad}, got: {err}"
|
|
3599
|
+
);
|
|
3600
|
+
}
|
|
3601
|
+
// The default survives the rejected sets unchanged.
|
|
3602
|
+
assert_eq!(db.auto_vacuum_threshold(), Some(0.25));
|
|
3603
|
+
// And valid values land.
|
|
3604
|
+
db.set_auto_vacuum_threshold(Some(0.0)).unwrap();
|
|
3605
|
+
assert_eq!(db.auto_vacuum_threshold(), Some(0.0));
|
|
3606
|
+
db.set_auto_vacuum_threshold(Some(1.0)).unwrap();
|
|
3607
|
+
assert_eq!(db.auto_vacuum_threshold(), Some(1.0));
|
|
3608
|
+
db.set_auto_vacuum_threshold(None).unwrap();
|
|
3609
|
+
assert_eq!(db.auto_vacuum_threshold(), None);
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3311
3612
|
/// VACUUM modifiers (FULL, REINDEX, table targets, …) are rejected
|
|
3312
3613
|
/// with NotImplemented — only bare `VACUUM;` is supported.
|
|
3313
3614
|
#[test]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlrite-0.4.0 → sqlrite-0.5.0}/images/SQLRite Simple SQL INSERT Execution High Level Diagram.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|