sqlrite 0.3.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.3.0 → sqlrite-0.5.0}/Cargo.lock +7 -7
- {sqlrite-0.3.0 → sqlrite-0.5.0}/Cargo.toml +2 -2
- {sqlrite-0.3.0 → sqlrite-0.5.0}/PKG-INFO +1 -1
- {sqlrite-0.3.0 → sqlrite-0.5.0}/desktop/package.json +1 -1
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/file-format.md +7 -4
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/pager.md +28 -1
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/supported-sql.md +38 -1
- {sqlrite-0.3.0 → sqlrite-0.5.0}/pyproject.toml +1 -1
- {sqlrite-0.3.0 → sqlrite-0.5.0}/scripts/bump-version.sh +38 -3
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sdk/python/Cargo.toml +1 -1
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sqlrite-ask/Cargo.toml +1 -1
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/connection.rs +55 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/db/database.rs +40 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/executor.rs +54 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/mod.rs +71 -2
- sqlrite-0.5.0/src/sql/pager/allocator.rs +222 -0
- sqlrite-0.5.0/src/sql/pager/freelist.rs +291 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/pager/header.rs +26 -7
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/pager/mod.rs +965 -112
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/pager/overflow.rs +39 -22
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/pager/pager.rs +21 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/.github/workflows/ci.yml +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/.github/workflows/release-pr.yml +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/.github/workflows/release.yml +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/.github/workflows/rust.yml +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/.gitignore +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/CLAUDE.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/CODE_OF_CONDUCT.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/LICENSE +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/MAINTAINERS +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/Makefile +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/README.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/desktop/index.html +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/desktop/package-lock.json +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/desktop/src/App.svelte +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/desktop/src/app.css +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/desktop/src/main.ts +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/desktop/src/vite-env.d.ts +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/desktop/svelte.config.js +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/desktop/tsconfig.json +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/desktop/vite.config.ts +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/_index.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/architecture.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/ask-backend-examples.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/ask.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/design-decisions.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/desktop.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/embedding.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/fts.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/getting-started.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/mcp.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/phase-7-plan.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/phase-8-plan.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/release-plan.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/release-secrets.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/roadmap.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/smoke-test.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/sql-engine.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/storage-model.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/docs/usage.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/examples/README.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/examples/c/Makefile +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/examples/c/hello.c +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/examples/go/go.mod +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/examples/go/hello.go +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/examples/hybrid-retrieval/README.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/examples/hybrid-retrieval/hybrid_retrieval.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/examples/nodejs/hello.mjs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/examples/python/hello.py +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/examples/rust/quickstart.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/examples/wasm/Makefile +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/examples/wasm/index.html +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/examples/wasm/server.mjs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/images/SQLRite - Desktop.png +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/images/SQLRite Data Structures.png +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/images/SQLRite Simple SQL Execution High Level Diagram.png +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/images/SQLRite Simple SQL INSERT Execution High Level Diagram (Insert Row).png +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/images/SQLRite Simple SQL INSERT Execution High Level Diagram.png +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/images/SQLRite_logo.png +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/images/architecture.png +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/rust-toolchain.toml +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/samples/AST.delete.example +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/samples/AST.insert.exemple +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/samples/AST.select.example +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/samples/AST.update.example +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/samples/CREATE TABLE sqlrite_schema.sql +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/samples/CREATE_TABLE with duplicate.sql +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/samples/CREATE_TABLE.sql +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/samples/INSERT.sql +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sdk/go/README.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sdk/go/ask.go +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sdk/go/ask_test.go +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sdk/go/conn.go +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sdk/go/go.mod +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sdk/go/rows.go +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sdk/go/sqlrite.go +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sdk/go/sqlrite_test.go +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sdk/go/stmt.go +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sdk/python/README.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sdk/python/src/lib.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sdk/python/tests/test_ask.py +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sdk/python/tests/test_sqlrite.py +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sqlrite-ask/README.md +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sqlrite-ask/src/lib.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sqlrite-ask/src/prompt.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sqlrite-ask/src/provider/anthropic.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sqlrite-ask/src/provider/mock.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sqlrite-ask/src/provider/mod.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/sqlrite-ask/tests/anthropic_http.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/ask/mod.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/ask/schema.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/error.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/lib.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/main.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/meta_command/mod.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/repl/mod.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/db/mod.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/db/secondary_index.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/db/table.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/fts/bm25.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/fts/mod.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/fts/posting_list.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/fts/tokenizer.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/hnsw.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/pager/cell.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/pager/file.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/pager/fts_cell.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/pager/hnsw_cell.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/pager/index_cell.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/pager/interior_page.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/pager/page.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/pager/table_page.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/pager/varint.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/pager/wal.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/parser/create.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/parser/insert.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/parser/mod.rs +0 -0
- {sqlrite-0.3.0 → sqlrite-0.5.0}/src/sql/parser/select.rs +0 -0
- {sqlrite-0.3.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 }
|
|
@@ -4,7 +4,7 @@ A SQLRite database is a single file, by convention named `*.sqlrite`. The file i
|
|
|
4
4
|
|
|
5
5
|
All multi-byte integers in this format are **little-endian**.
|
|
6
6
|
|
|
7
|
-
The current on-disk format is **version 4** (Phase 7) by default, with **version 5** written on demand whenever an FTS index is attached to the database (Phase 8c). Decoders accept
|
|
7
|
+
The current on-disk format is **version 4** (Phase 7) by default, with **version 5** written on demand whenever an FTS index is attached to the database (Phase 8c) and **version 6** written on demand whenever a save produces a non-empty freelist (SQLR-6). Decoders accept v4, v5, and v6; writers preserve the existing version on no-op resaves so a v4 database without FTS or freelist stays v4. Files produced by versions 1 – 3 are rejected on open.
|
|
8
8
|
|
|
9
9
|
## Page 0 — the database header
|
|
10
10
|
|
|
@@ -15,17 +15,18 @@ The first 4096 bytes of every file are the header page. Only the first 28 bytes
|
|
|
15
15
|
│ offset │ length │ content │
|
|
16
16
|
├────────┼────────┼─────────────────────────────────────────────────┤
|
|
17
17
|
│ 0 │ 16 │ magic: "SQLRiteFormat\0\0\0" │
|
|
18
|
-
│ 16 │ 2 │ format version (u16 LE) = 4 or
|
|
18
|
+
│ 16 │ 2 │ format version (u16 LE) = 4, 5, or 6 │
|
|
19
19
|
│ 18 │ 2 │ page size (u16 LE) = 4096 │
|
|
20
20
|
│ 20 │ 4 │ total page count (u32 LE), includes page 0 │
|
|
21
21
|
│ 24 │ 4 │ root page of sqlrite_master (u32 LE) │
|
|
22
|
-
│ 28 │
|
|
22
|
+
│ 28 │ 4 │ freelist head (u32 LE; 0 = empty) — v6 only │
|
|
23
|
+
│ 32 │ 4064 │ reserved / zero │
|
|
23
24
|
└────────┴────────┴─────────────────────────────────────────────────┘
|
|
24
25
|
```
|
|
25
26
|
|
|
26
27
|
The magic string is 14 ASCII bytes (`SQLRiteFormat`) padded with two NUL bytes to fill 16 bytes. It's deliberately different from SQLite's `"SQLite format 3\0"` so the two formats can't be confused on inspection.
|
|
27
28
|
|
|
28
|
-
`decode_header` in [`src/sql/pager/header.rs`](../src/sql/pager/header.rs) validates all three of (magic, format version, page size) on open. A wrong magic produces `not a SQLRite database`; a wrong version or page size produces `unsupported ...` errors. The decoder accepts
|
|
29
|
+
`decode_header` in [`src/sql/pager/header.rs`](../src/sql/pager/header.rs) validates all three of (magic, format version, page size) on open. A wrong magic produces `not a SQLRite database`; a wrong version or page size produces `unsupported ...` errors. The decoder accepts v4, v5, and v6 (anything else is rejected); the parsed `format_version` is propagated through the in-memory `DbHeader` so the writer can preserve it on resave when no version-bumping feature has been added. `freelist_head` is read from bytes [28..32]: v4/v5 files leave that region zero so it always decodes as `0` (an empty freelist), and v6 files store the page number of the first freelist trunk there.
|
|
29
30
|
|
|
30
31
|
## Pages 1..page_count — payload pages
|
|
31
32
|
|
|
@@ -53,6 +54,7 @@ Every non-header page starts with a 7-byte header:
|
|
|
53
54
|
| `2` | `TableLeaf` | Holds a slot directory and a set of cells representing rows of a table. Leaves for one table are linked by sibling `next_page` pointers. |
|
|
54
55
|
| `3` | `Overflow` | Continuation page carrying the spilled body of a single oversized cell. |
|
|
55
56
|
| `4` | `InteriorNode` | Interior B-Tree node. Holds a slot directory of divider cells routing to child pages plus a rightmost-child pointer in the payload header. |
|
|
57
|
+
| `5` | `FreelistTrunk` | One link of the persisted free-page list (SQLR-6). Payload carries `count: u16` followed by `count × u32` free leaf-page numbers; `next_page` chains to the next trunk (0 = end). |
|
|
56
58
|
|
|
57
59
|
Tag `1` is reserved (it was `SchemaRoot` in format v1; unused in v2). Any other tag on open is a corruption error.
|
|
58
60
|
|
|
@@ -307,6 +309,7 @@ These are not all enforced on open — we validate the header strictly and rely
|
|
|
307
309
|
- **v3** (Phase 3e) — `sqlrite_master` gains a `type` column; secondary indexes persist as their own cell-based B-Trees whose leaves carry `KIND_INDEX` cells.
|
|
308
310
|
- **v4** (Phase 7a) — value block dispatch gains the `0x04 Vector` tag for the new `VECTOR(N)` column type. Per the [Phase 7 plan's Q8](phase-7-plan.md#q8-file-format-version-bump), later Phase 7 sub-phases (JSON storage, HNSW indexes) added their own value/cell tags inside this same v4 envelope. The `CREATE TABLE` SQL stored in `sqlrite_master` carries vector columns as `VECTOR(N)` in the type position; on open, the engine re-parses that SQL and reconstructs `DataType::Vector(N)` from the `Custom` AST node sqlparser produces.
|
|
309
311
|
- **v5** (Phase 8c, current for FTS-bearing files) — adds the `KIND_FTS_POSTING` cell tag for persisted FTS posting lists. Bumped **on demand** per the [Phase 8 plan's Q10](phase-8-plan.md#q10-file-format-version-bump-strategy): existing v4 databases without FTS keep writing v4 across non-FTS saves; the first save with at least one FTS index attached promotes the file to v5. Decoders accept both v4 and v5; opening a v4 file with a build that supports v5 is a no-op until the user creates an FTS index.
|
|
312
|
+
- **v6** (SQLR-6, current for files with persisted free-page lists) — adds the `freelist_head` field at header bytes [28..32] and the `FreelistTrunk` page tag (`5`). Bumped **on demand**: a save that ends with an empty freelist preserves the existing version; the first save that produces a non-empty freelist promotes the file to v6. Decoders accept v4, v5, and v6; v6 is a strict superset, so opening a v4/v5 file with a v6-aware build is a no-op until the user creates a freelist (e.g., by dropping a table or index). VACUUM clears the freelist but doesn't downgrade.
|
|
310
313
|
|
|
311
314
|
The page header (7 bytes) and chaining mechanism are stable across future phases. Phase 4's WAL introduces a sibling file (`.sqlrite-wal`) rather than changing the main file format.
|
|
312
315
|
|
|
@@ -187,10 +187,37 @@ Without the diff, step 3's "re-serialize every table" would trigger a full file
|
|
|
187
187
|
|
|
188
188
|
This only works because `save_database` iterates tables in sorted order — if the order were random, a table that didn't change might land at a different page number, appearing dirty. See [Design decisions §7](design-decisions.md#7-deterministic-page-number-ordering-when-saving).
|
|
189
189
|
|
|
190
|
+
## Free-page list and VACUUM (SQLR-6)
|
|
191
|
+
|
|
192
|
+
Save now uses a [`PageAllocator`](../src/sql/pager/allocator.rs) instead of a bare `next_free_page` counter. The allocator pulls pages from three sources, in preference order:
|
|
193
|
+
|
|
194
|
+
1. **Per-table preferred pool** — every table/index/master is given the page numbers it occupied last save (collected by walking from its old `rootpage`). An unchanged table re-stages byte-identical pages at the same numbers, so the diff pager skips every write for it.
|
|
195
|
+
2. **Global freelist** — pages from dropped tables/indexes that are recorded in the persisted freelist (rooted at `header.freelist_head`).
|
|
196
|
+
3. **Extend** — `next_extend++`, monotonic past the high-water mark.
|
|
197
|
+
|
|
198
|
+
After staging, pages that were live before this save but didn't get restaged this round (e.g., the leaves of a dropped table) move onto the new freelist. The freelist itself is encoded into a chain of `FreelistTrunk` pages — each trunk holds up to 1021 free leaf-page numbers plus a `next_page` pointer to the following trunk. Trunks consume some of the free pages they describe (a trunk page IS a free page borrowed for metadata), so a freelist of N pages takes `ceil(N / 1022)` trunks and persists `N − T` leaf entries.
|
|
199
|
+
|
|
200
|
+
`VACUUM;` (a SQL statement) calls [`vacuum_database`](../src/sql/pager/mod.rs), which is `save_database` with empty per-table preferred pools and an empty initial freelist. Allocation falls through to extend on every page → contiguous layout from page 1, no freelist trunks, file truncates to the new high-water mark on the next checkpoint.
|
|
201
|
+
|
|
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
|
+
|
|
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
|
+
|
|
190
218
|
## What it doesn't do (yet)
|
|
191
219
|
|
|
192
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.
|
|
193
|
-
- **No free-page management.** When a table shrinks, the main file's tail pages are truncated at checkpoint, but there's no free-list to reuse pages inside a grown file.
|
|
194
221
|
- **No per-statement granularity.** The whole database is re-serialized on every commit; the diff keeps the *written* set small but the CPU cost of reserialization is unchanged.
|
|
195
222
|
- **No concurrent reader-and-writer.** Phase 4e graduated to shared/exclusive lock modes (multi-reader *or* single-writer), but POSIX flock can't give us both at once. True concurrent access would need a shared-memory coordination file with read marks — not on the roadmap.
|
|
196
223
|
- **Savepoints / nested transactions.** Phase 4f added top-level `BEGIN` / `COMMIT` / `ROLLBACK` (snapshot-based rollback, auto-save suppressed inside a transaction), but nested `BEGIN` is rejected — real savepoints aren't on the roadmap.
|
|
@@ -17,6 +17,7 @@ If you're looking for _how_ to use SQLRite (REPL flow, meta-commands, history, e
|
|
|
17
17
|
| [`ALTER TABLE`](#alter-table) | `RENAME TO`, `RENAME COLUMN`, `ADD COLUMN`, `DROP COLUMN` (one operation per statement) |
|
|
18
18
|
| [`DROP TABLE`](#drop-table) / [`DROP INDEX`](#drop-index) | `IF EXISTS`; single target; auto-indexes refused for `DROP INDEX` |
|
|
19
19
|
| [`BEGIN`](#transactions) / [`COMMIT`](#transactions) / [`ROLLBACK`](#transactions) | Snapshot-based; single-level; WAL-backed commit; auto-rollback on COMMIT disk failure |
|
|
20
|
+
| [`VACUUM`](#vacuum) | Compacts the file: rewrites every live B-Tree contiguously from page 1 and clears the freelist. Bare `VACUUM;` only — no modifiers. |
|
|
20
21
|
|
|
21
22
|
Statements the parser accepts (because sqlparser understands them in the SQLite dialect) but SQLRite doesn't execute yet return `SQL Statement not supported yet`. The [Not yet supported](#not-yet-supported) section below enumerates the common ones.
|
|
22
23
|
|
|
@@ -259,7 +260,7 @@ DROP TABLE [IF EXISTS] <table>;
|
|
|
259
260
|
- Reserved-name rejection: `DROP TABLE sqlrite_master` errors with the same message `CREATE TABLE` uses.
|
|
260
261
|
- All indexes attached to the table (auto, explicit, HNSW, FTS) disappear with the table — they live inside the `Table` struct and ride along.
|
|
261
262
|
- Without `IF EXISTS`, dropping a table that doesn't exist errors. With it, that's a benign 0-tables-dropped no-op.
|
|
262
|
-
- **Disk pages
|
|
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.
|
|
263
264
|
|
|
264
265
|
---
|
|
265
266
|
|
|
@@ -428,6 +429,42 @@ ROLLBACK; -- nothing was actually deleted
|
|
|
428
429
|
|
|
429
430
|
---
|
|
430
431
|
|
|
432
|
+
## `VACUUM`
|
|
433
|
+
|
|
434
|
+
```sql
|
|
435
|
+
VACUUM;
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Compacts the database file: rewrites every live table, index, HNSW graph, FTS posting tree, and `sqlrite_master` itself contiguously from page 1, drops the freelist, and lets the next checkpoint truncate the tail.
|
|
439
|
+
|
|
440
|
+
- **Bare `VACUUM;` only.** Modifiers — `VACUUM FULL`, `VACUUM REINDEX`, table targets, `TO ... PERCENT`, `BOOST` — are parsed (sqlparser supports them) but rejected at execution with `VACUUM modifiers (FULL, REINDEX, table targets, etc.) are not supported`.
|
|
441
|
+
- **Refused inside a transaction.** `BEGIN; VACUUM;` errors with `VACUUM cannot run inside a transaction`. Use `COMMIT;` first, then `VACUUM;`.
|
|
442
|
+
- **No-op on in-memory databases.** Returns a `VACUUM is a no-op for in-memory databases` status string and does nothing — there's no file to compact.
|
|
443
|
+
- **Status string** carries pages and bytes reclaimed: `VACUUM completed. <N> pages reclaimed (<B> bytes).`
|
|
444
|
+
- **Format-version side effect.** A v4/v5 file that has been promoted to v6 by an earlier drop stays at v6 after VACUUM (v6 is a strict superset; we don't downgrade). A file that's already at v4/v5 because no drop ever happened on it doesn't get bumped by VACUUM.
|
|
445
|
+
|
|
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
|
+
|
|
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
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
431
468
|
## Read-only databases
|
|
432
469
|
|
|
433
470
|
A REPL launched with `sqlrite --readonly foo.sqlrite` (or `sqlrite::open_database_read_only(path, name)` programmatically) takes a shared POSIX advisory lock instead of an exclusive one. In that mode:
|
|
@@ -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" }
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
# scripts/bump-version.sh 0.2.0
|
|
7
7
|
#
|
|
8
8
|
# Rewrites the version field in every manifest that carries one
|
|
9
|
-
# (
|
|
10
|
-
# —
|
|
9
|
+
# (nine Cargo.toml / pyproject.toml files, plus three JSON manifests
|
|
10
|
+
# — twelve files total). Then you run `cargo build` yourself to
|
|
11
11
|
# refresh Cargo.lock. Idempotent: running twice with the same version
|
|
12
12
|
# is a no-op; running twice with different versions lands on the
|
|
13
13
|
# second.
|
|
@@ -84,6 +84,25 @@ for file in "${TOML_FILES[@]}"; do
|
|
|
84
84
|
fi
|
|
85
85
|
sed "s/^version = \"[^\"]*\"/version = \"${VERSION}\"/" "$file" > "$file.tmp"
|
|
86
86
|
mv "$file.tmp" "$file"
|
|
87
|
+
|
|
88
|
+
# Inter-workspace dep pins — lines like:
|
|
89
|
+
# sqlrite-ask = { version = "0.3", path = "sqlrite-ask", ... }
|
|
90
|
+
# sqlrite = { package = "sqlrite-engine", path = "..", version = "0.3", ... }
|
|
91
|
+
#
|
|
92
|
+
# These carry BOTH version and path because crates.io publishing
|
|
93
|
+
# rejects path-only deps (see PR #58 retrospective). The version
|
|
94
|
+
# field has to track the workspace bump or `cargo build` fails to
|
|
95
|
+
# resolve a candidate (SQLR-9; failed run for v0.3.0 hit exactly
|
|
96
|
+
# this — `failed to select a version for the requirement
|
|
97
|
+
# sqlrite-ask = "^0.2"`).
|
|
98
|
+
#
|
|
99
|
+
# Detection: any line containing both `version = "..."` and
|
|
100
|
+
# `path = "..."`. The package-level `^version = "..."` line at
|
|
101
|
+
# the top of each manifest has no `path` on it and can't match.
|
|
102
|
+
# Both inline-table orderings (version-first and path-first) work
|
|
103
|
+
# because sed acts per-line, not per-token-order.
|
|
104
|
+
sed -E '/path *= *"[^"]*"/ s/version *= *"[^"]*"/version = "'"${VERSION}"'"/' "$file" > "$file.tmp"
|
|
105
|
+
mv "$file.tmp" "$file"
|
|
87
106
|
done
|
|
88
107
|
|
|
89
108
|
# ---------------------------------------------------------------------------
|
|
@@ -147,6 +166,22 @@ for file in "${JSON_FILES[@]}"; do
|
|
|
147
166
|
fi
|
|
148
167
|
done
|
|
149
168
|
|
|
169
|
+
# Inter-workspace pin sweep — any surviving `version = "X"` on a TOML
|
|
170
|
+
# line that also has `path = "..."` and isn't already at $VERSION is a
|
|
171
|
+
# pin we missed. Catches future refactors that change pin shape (e.g.
|
|
172
|
+
# someone splits a long dep line across multiple TOML lines, where the
|
|
173
|
+
# single-line address would no longer match).
|
|
174
|
+
for file in "${TOML_FILES[@]}"; do
|
|
175
|
+
bad="$(grep -nE 'path *= *"[^"]*"' "$file" \
|
|
176
|
+
| grep -E 'version *= *"[^"]*"' \
|
|
177
|
+
| grep -vE "version *= *\"${VERSION}\"" || true)"
|
|
178
|
+
if [[ -n "$bad" ]]; then
|
|
179
|
+
echo " ✗ $file — inter-workspace pin not at ${VERSION}:" >&2
|
|
180
|
+
echo "$bad" | sed 's/^/ /' >&2
|
|
181
|
+
FAILURES=$((FAILURES + 1))
|
|
182
|
+
fi
|
|
183
|
+
done
|
|
184
|
+
|
|
150
185
|
if [[ $FAILURES -gt 0 ]]; then
|
|
151
186
|
echo
|
|
152
187
|
echo "error: $FAILURES file(s) did not update as expected." >&2
|
|
@@ -157,5 +192,5 @@ fi
|
|
|
157
192
|
echo
|
|
158
193
|
echo "Done. Next steps:"
|
|
159
194
|
echo " cargo build # refresh Cargo.lock with the new versions"
|
|
160
|
-
echo " git diff # inspect the
|
|
195
|
+
echo " git diff # inspect the twelve-file bump"
|
|
161
196
|
echo " git checkout . # or back out if it looks wrong"
|
|
@@ -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 {
|
|
@@ -717,6 +717,60 @@ pub fn execute_alter_table(alter: AlterTable, db: &mut Database) -> Result<Strin
|
|
|
717
717
|
}
|
|
718
718
|
}
|
|
719
719
|
|
|
720
|
+
/// Executes `VACUUM;` (SQLR-6). Compacts the database file: rewrites
|
|
721
|
+
/// every live table, index, and the catalog contiguously from page 1,
|
|
722
|
+
/// drops the freelist, and truncates the tail at the next checkpoint.
|
|
723
|
+
///
|
|
724
|
+
/// Refuses to run inside a transaction (would publish in-flight writes
|
|
725
|
+
/// out of band); refuses on read-only databases (handled upstream by
|
|
726
|
+
/// the read-only mutation gate); and is a no-op on in-memory databases
|
|
727
|
+
/// (no file to compact). Bare `VACUUM;` only — non-default options
|
|
728
|
+
/// (`FULL`, `REINDEX`, table targets, etc.) are rejected.
|
|
729
|
+
pub fn execute_vacuum(db: &mut Database) -> Result<String> {
|
|
730
|
+
if db.in_transaction() {
|
|
731
|
+
return Err(SQLRiteError::General(
|
|
732
|
+
"VACUUM cannot run inside a transaction".to_string(),
|
|
733
|
+
));
|
|
734
|
+
}
|
|
735
|
+
let path = match db.source_path.clone() {
|
|
736
|
+
Some(p) => p,
|
|
737
|
+
None => {
|
|
738
|
+
return Ok("VACUUM is a no-op for in-memory databases".to_string());
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
// Checkpoint before AND after VACUUM so the main-file size we report
|
|
742
|
+
// reflects only what VACUUM actually reclaimed — without the leading
|
|
743
|
+
// checkpoint, `size_before` would be the stale main-file snapshot
|
|
744
|
+
// (typically 2 pages) while WAL holds the live bytes, making the
|
|
745
|
+
// bytes-reclaimed delta meaningless.
|
|
746
|
+
if let Some(pager) = db.pager.as_mut() {
|
|
747
|
+
let _ = pager.checkpoint();
|
|
748
|
+
}
|
|
749
|
+
let size_before = std::fs::metadata(&path).ok().map(|m| m.len()).unwrap_or(0);
|
|
750
|
+
let pages_before = db
|
|
751
|
+
.pager
|
|
752
|
+
.as_ref()
|
|
753
|
+
.map(|p| p.header().page_count)
|
|
754
|
+
.unwrap_or(0);
|
|
755
|
+
crate::sql::pager::vacuum_database(db, &path)?;
|
|
756
|
+
// Second checkpoint so the main file shrinks now — VACUUM's whole
|
|
757
|
+
// purpose is to reclaim bytes, so paying the I/O up front is fair.
|
|
758
|
+
if let Some(pager) = db.pager.as_mut() {
|
|
759
|
+
let _ = pager.checkpoint();
|
|
760
|
+
}
|
|
761
|
+
let size_after = std::fs::metadata(&path).ok().map(|m| m.len()).unwrap_or(0);
|
|
762
|
+
let pages_after = db
|
|
763
|
+
.pager
|
|
764
|
+
.as_ref()
|
|
765
|
+
.map(|p| p.header().page_count)
|
|
766
|
+
.unwrap_or(0);
|
|
767
|
+
let pages_reclaimed = pages_before.saturating_sub(pages_after);
|
|
768
|
+
let bytes_reclaimed = size_before.saturating_sub(size_after);
|
|
769
|
+
Ok(format!(
|
|
770
|
+
"VACUUM completed. {pages_reclaimed} pages reclaimed ({bytes_reclaimed} bytes)."
|
|
771
|
+
))
|
|
772
|
+
}
|
|
773
|
+
|
|
720
774
|
/// Renames a table in `db.tables`. Updates `tb_name`, every secondary
|
|
721
775
|
/// index's `table_name` field, and any auto-index whose name embedded
|
|
722
776
|
/// the old table name. HNSW / FTS index entries don't carry a
|