sqlrite 0.4.0__tar.gz → 0.5.1__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.1}/Cargo.lock +7 -7
- {sqlrite-0.4.0 → sqlrite-0.5.1}/Cargo.toml +2 -2
- {sqlrite-0.4.0 → sqlrite-0.5.1}/PKG-INFO +1 -1
- {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/package.json +1 -1
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/file-format.md +2 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/pager.md +14 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/supported-sql.md +19 -1
- {sqlrite-0.4.0 → sqlrite-0.5.1}/pyproject.toml +1 -1
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/python/Cargo.toml +1 -1
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/Cargo.toml +1 -1
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/connection.rs +55 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/db/database.rs +40 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/mod.rs +41 -1
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/freelist.rs +33 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/mod.rs +496 -26
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/overflow.rs +84 -2
- {sqlrite-0.4.0 → sqlrite-0.5.1}/.github/workflows/ci.yml +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/.github/workflows/release-pr.yml +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/.github/workflows/release.yml +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/.github/workflows/rust.yml +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/.gitignore +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/CLAUDE.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/CODE_OF_CONDUCT.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/LICENSE +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/MAINTAINERS +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/Makefile +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/README.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/index.html +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/package-lock.json +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/src/App.svelte +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/src/app.css +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/src/main.ts +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/src/vite-env.d.ts +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/svelte.config.js +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/tsconfig.json +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/vite.config.ts +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/_index.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/architecture.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/ask-backend-examples.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/ask.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/design-decisions.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/desktop.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/embedding.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/fts.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/getting-started.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/mcp.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/phase-7-plan.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/phase-8-plan.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/release-plan.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/release-secrets.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/roadmap.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/smoke-test.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/sql-engine.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/storage-model.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/usage.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/README.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/c/Makefile +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/c/hello.c +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/go/go.mod +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/go/hello.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/hybrid-retrieval/README.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/hybrid-retrieval/hybrid_retrieval.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/nodejs/hello.mjs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/python/hello.py +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/rust/quickstart.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/wasm/Makefile +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/wasm/index.html +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/wasm/server.mjs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/images/SQLRite - Desktop.png +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/images/SQLRite Data Structures.png +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/images/SQLRite Simple SQL Execution High Level Diagram.png +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/images/SQLRite Simple SQL INSERT Execution High Level Diagram (Insert Row).png +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/images/SQLRite Simple SQL INSERT Execution High Level Diagram.png +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/images/SQLRite_logo.png +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/images/architecture.png +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/rust-toolchain.toml +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/AST.delete.example +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/AST.insert.exemple +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/AST.select.example +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/AST.update.example +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/CREATE TABLE sqlrite_schema.sql +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/CREATE_TABLE with duplicate.sql +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/CREATE_TABLE.sql +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/INSERT.sql +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/scripts/bump-version.sh +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/README.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/ask.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/ask_test.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/conn.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/go.mod +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/rows.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/sqlrite.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/sqlrite_test.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/stmt.go +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/python/README.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/python/src/lib.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/python/tests/test_ask.py +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/python/tests/test_sqlrite.py +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/README.md +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/src/lib.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/src/prompt.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/src/provider/anthropic.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/src/provider/mock.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/src/provider/mod.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/tests/anthropic_http.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/ask/mod.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/ask/schema.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/error.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/lib.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/main.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/meta_command/mod.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/repl/mod.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/db/mod.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/db/secondary_index.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/db/table.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/executor.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/fts/bm25.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/fts/mod.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/fts/posting_list.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/fts/tokenizer.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/hnsw.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/allocator.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/cell.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/file.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/fts_cell.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/header.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/hnsw_cell.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/index_cell.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/interior_page.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/page.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/pager.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/table_page.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/varint.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/wal.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/parser/create.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/parser/insert.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/parser/mod.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/parser/select.rs +0 -0
- {sqlrite-0.4.0 → sqlrite-0.5.1}/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.1"
|
|
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.1"
|
|
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.1"
|
|
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.1"
|
|
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.1"
|
|
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.1"
|
|
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.1"
|
|
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.1"
|
|
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.1", path = "sqlrite-ask", optional = true }
|
|
@@ -139,6 +139,8 @@ body variable depends on kind_tag
|
|
|
139
139
|
|
|
140
140
|
The shared prefix means `Cell::peek_rowid` works uniformly across all kinds — useful for binary search over a page's slot directory without decoding full bodies.
|
|
141
141
|
|
|
142
|
+
**Decoder dispatch is per-B-tree, not per-cell.** Each B-Tree owns one cell kind on its leaves: table B-Trees carry `Local`/`Overflow` cells (decoded via `PagedEntry::decode`), secondary-index B-Trees carry `Index` cells (`IndexCell::decode`), HNSW carries `HNSW` cells (`HnswNodeCell::decode`), FTS carries `FTS Posting` cells (`FtsPostingCell::decode`), and every interior node — regardless of the leaf kind below it — carries `Interior` divider cells (`InteriorCell::decode`). Callers must dispatch on the *page's owning B-Tree*, not on the kind tag they happen to see; pointing the wrong decoder at a leaf yields an `Internal` error that names both the offending kind and the B-Tree it belongs to (SQLR-1).
|
|
143
|
+
|
|
142
144
|
### Local cell body
|
|
143
145
|
|
|
144
146
|
```
|
|
@@ -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.1"
|
|
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.1"
|
|
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::*;
|