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.
Files changed (139) hide show
  1. {sqlrite-0.4.0 → sqlrite-0.5.1}/Cargo.lock +7 -7
  2. {sqlrite-0.4.0 → sqlrite-0.5.1}/Cargo.toml +2 -2
  3. {sqlrite-0.4.0 → sqlrite-0.5.1}/PKG-INFO +1 -1
  4. {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/package.json +1 -1
  5. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/file-format.md +2 -0
  6. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/pager.md +14 -0
  7. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/supported-sql.md +19 -1
  8. {sqlrite-0.4.0 → sqlrite-0.5.1}/pyproject.toml +1 -1
  9. {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/python/Cargo.toml +1 -1
  10. {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/Cargo.toml +1 -1
  11. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/connection.rs +55 -0
  12. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/db/database.rs +40 -0
  13. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/mod.rs +41 -1
  14. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/freelist.rs +33 -0
  15. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/mod.rs +496 -26
  16. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/overflow.rs +84 -2
  17. {sqlrite-0.4.0 → sqlrite-0.5.1}/.github/workflows/ci.yml +0 -0
  18. {sqlrite-0.4.0 → sqlrite-0.5.1}/.github/workflows/release-pr.yml +0 -0
  19. {sqlrite-0.4.0 → sqlrite-0.5.1}/.github/workflows/release.yml +0 -0
  20. {sqlrite-0.4.0 → sqlrite-0.5.1}/.github/workflows/rust.yml +0 -0
  21. {sqlrite-0.4.0 → sqlrite-0.5.1}/.gitignore +0 -0
  22. {sqlrite-0.4.0 → sqlrite-0.5.1}/CLAUDE.md +0 -0
  23. {sqlrite-0.4.0 → sqlrite-0.5.1}/CODE_OF_CONDUCT.md +0 -0
  24. {sqlrite-0.4.0 → sqlrite-0.5.1}/LICENSE +0 -0
  25. {sqlrite-0.4.0 → sqlrite-0.5.1}/MAINTAINERS +0 -0
  26. {sqlrite-0.4.0 → sqlrite-0.5.1}/Makefile +0 -0
  27. {sqlrite-0.4.0 → sqlrite-0.5.1}/README.md +0 -0
  28. {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/index.html +0 -0
  29. {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/package-lock.json +0 -0
  30. {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/src/App.svelte +0 -0
  31. {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/src/app.css +0 -0
  32. {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/src/main.ts +0 -0
  33. {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/src/vite-env.d.ts +0 -0
  34. {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/svelte.config.js +0 -0
  35. {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/tsconfig.json +0 -0
  36. {sqlrite-0.4.0 → sqlrite-0.5.1}/desktop/vite.config.ts +0 -0
  37. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/_index.md +0 -0
  38. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/architecture.md +0 -0
  39. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/ask-backend-examples.md +0 -0
  40. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/ask.md +0 -0
  41. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/design-decisions.md +0 -0
  42. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/desktop.md +0 -0
  43. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/embedding.md +0 -0
  44. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/fts.md +0 -0
  45. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/getting-started.md +0 -0
  46. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/mcp.md +0 -0
  47. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/phase-7-plan.md +0 -0
  48. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/phase-8-plan.md +0 -0
  49. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/release-plan.md +0 -0
  50. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/release-secrets.md +0 -0
  51. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/roadmap.md +0 -0
  52. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/smoke-test.md +0 -0
  53. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/sql-engine.md +0 -0
  54. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/storage-model.md +0 -0
  55. {sqlrite-0.4.0 → sqlrite-0.5.1}/docs/usage.md +0 -0
  56. {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/README.md +0 -0
  57. {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/c/Makefile +0 -0
  58. {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/c/hello.c +0 -0
  59. {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/go/go.mod +0 -0
  60. {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/go/hello.go +0 -0
  61. {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/hybrid-retrieval/README.md +0 -0
  62. {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/hybrid-retrieval/hybrid_retrieval.rs +0 -0
  63. {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/nodejs/hello.mjs +0 -0
  64. {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/python/hello.py +0 -0
  65. {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/rust/quickstart.rs +0 -0
  66. {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/wasm/Makefile +0 -0
  67. {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/wasm/index.html +0 -0
  68. {sqlrite-0.4.0 → sqlrite-0.5.1}/examples/wasm/server.mjs +0 -0
  69. {sqlrite-0.4.0 → sqlrite-0.5.1}/images/SQLRite - Desktop.png +0 -0
  70. {sqlrite-0.4.0 → sqlrite-0.5.1}/images/SQLRite Data Structures.png +0 -0
  71. {sqlrite-0.4.0 → sqlrite-0.5.1}/images/SQLRite Simple SQL Execution High Level Diagram.png +0 -0
  72. {sqlrite-0.4.0 → sqlrite-0.5.1}/images/SQLRite Simple SQL INSERT Execution High Level Diagram (Insert Row).png +0 -0
  73. {sqlrite-0.4.0 → sqlrite-0.5.1}/images/SQLRite Simple SQL INSERT Execution High Level Diagram.png +0 -0
  74. {sqlrite-0.4.0 → sqlrite-0.5.1}/images/SQLRite_logo.png +0 -0
  75. {sqlrite-0.4.0 → sqlrite-0.5.1}/images/architecture.png +0 -0
  76. {sqlrite-0.4.0 → sqlrite-0.5.1}/rust-toolchain.toml +0 -0
  77. {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/AST.delete.example +0 -0
  78. {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/AST.insert.exemple +0 -0
  79. {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/AST.select.example +0 -0
  80. {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/AST.update.example +0 -0
  81. {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/CREATE TABLE sqlrite_schema.sql +0 -0
  82. {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/CREATE_TABLE with duplicate.sql +0 -0
  83. {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/CREATE_TABLE.sql +0 -0
  84. {sqlrite-0.4.0 → sqlrite-0.5.1}/samples/INSERT.sql +0 -0
  85. {sqlrite-0.4.0 → sqlrite-0.5.1}/scripts/bump-version.sh +0 -0
  86. {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/README.md +0 -0
  87. {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/ask.go +0 -0
  88. {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/ask_test.go +0 -0
  89. {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/conn.go +0 -0
  90. {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/go.mod +0 -0
  91. {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/rows.go +0 -0
  92. {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/sqlrite.go +0 -0
  93. {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/sqlrite_test.go +0 -0
  94. {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/go/stmt.go +0 -0
  95. {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/python/README.md +0 -0
  96. {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/python/src/lib.rs +0 -0
  97. {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/python/tests/test_ask.py +0 -0
  98. {sqlrite-0.4.0 → sqlrite-0.5.1}/sdk/python/tests/test_sqlrite.py +0 -0
  99. {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/README.md +0 -0
  100. {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/src/lib.rs +0 -0
  101. {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/src/prompt.rs +0 -0
  102. {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/src/provider/anthropic.rs +0 -0
  103. {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/src/provider/mock.rs +0 -0
  104. {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/src/provider/mod.rs +0 -0
  105. {sqlrite-0.4.0 → sqlrite-0.5.1}/sqlrite-ask/tests/anthropic_http.rs +0 -0
  106. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/ask/mod.rs +0 -0
  107. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/ask/schema.rs +0 -0
  108. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/error.rs +0 -0
  109. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/lib.rs +0 -0
  110. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/main.rs +0 -0
  111. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/meta_command/mod.rs +0 -0
  112. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/repl/mod.rs +0 -0
  113. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/db/mod.rs +0 -0
  114. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/db/secondary_index.rs +0 -0
  115. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/db/table.rs +0 -0
  116. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/executor.rs +0 -0
  117. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/fts/bm25.rs +0 -0
  118. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/fts/mod.rs +0 -0
  119. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/fts/posting_list.rs +0 -0
  120. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/fts/tokenizer.rs +0 -0
  121. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/hnsw.rs +0 -0
  122. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/allocator.rs +0 -0
  123. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/cell.rs +0 -0
  124. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/file.rs +0 -0
  125. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/fts_cell.rs +0 -0
  126. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/header.rs +0 -0
  127. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/hnsw_cell.rs +0 -0
  128. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/index_cell.rs +0 -0
  129. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/interior_page.rs +0 -0
  130. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/page.rs +0 -0
  131. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/pager.rs +0 -0
  132. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/table_page.rs +0 -0
  133. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/varint.rs +0 -0
  134. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/pager/wal.rs +0 -0
  135. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/parser/create.rs +0 -0
  136. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/parser/insert.rs +0 -0
  137. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/parser/mod.rs +0 -0
  138. {sqlrite-0.4.0 → sqlrite-0.5.1}/src/sql/parser/select.rs +0 -0
  139. {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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.0", path = "sqlrite-ask", optional = true }
141
+ sqlrite-ask = { version = "0.5.1", path = "sqlrite-ask", optional = true }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlrite
3
- Version: 0.4.0
3
+ Version: 0.5.1
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sqlrite-desktop-frontend",
3
3
  "private": true,
4
- "version": "0.4.0",
4
+ "version": "0.5.1",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
@@ -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 doesn't shrink until [`VACUUM;`](#vacuum) compacts it.
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.4.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" }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sqlrite-python"
3
- version = "0.4.0"
3
+ version = "0.5.1"
4
4
  authors = ["Joao Henrique Machado Silva <joaoh82@gmail.com>"]
5
5
  edition = "2024"
6
6
  rust-version = "1.85"
@@ -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.4.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::*;