sqlrite 0.5.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.5.0 → sqlrite-0.5.1}/Cargo.lock +7 -7
  2. {sqlrite-0.5.0 → sqlrite-0.5.1}/Cargo.toml +2 -2
  3. {sqlrite-0.5.0 → sqlrite-0.5.1}/PKG-INFO +1 -1
  4. {sqlrite-0.5.0 → sqlrite-0.5.1}/desktop/package.json +1 -1
  5. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/file-format.md +2 -0
  6. {sqlrite-0.5.0 → sqlrite-0.5.1}/pyproject.toml +1 -1
  7. {sqlrite-0.5.0 → sqlrite-0.5.1}/sdk/python/Cargo.toml +1 -1
  8. {sqlrite-0.5.0 → sqlrite-0.5.1}/sqlrite-ask/Cargo.toml +1 -1
  9. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/mod.rs +195 -26
  10. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/overflow.rs +84 -2
  11. {sqlrite-0.5.0 → sqlrite-0.5.1}/.github/workflows/ci.yml +0 -0
  12. {sqlrite-0.5.0 → sqlrite-0.5.1}/.github/workflows/release-pr.yml +0 -0
  13. {sqlrite-0.5.0 → sqlrite-0.5.1}/.github/workflows/release.yml +0 -0
  14. {sqlrite-0.5.0 → sqlrite-0.5.1}/.github/workflows/rust.yml +0 -0
  15. {sqlrite-0.5.0 → sqlrite-0.5.1}/.gitignore +0 -0
  16. {sqlrite-0.5.0 → sqlrite-0.5.1}/CLAUDE.md +0 -0
  17. {sqlrite-0.5.0 → sqlrite-0.5.1}/CODE_OF_CONDUCT.md +0 -0
  18. {sqlrite-0.5.0 → sqlrite-0.5.1}/LICENSE +0 -0
  19. {sqlrite-0.5.0 → sqlrite-0.5.1}/MAINTAINERS +0 -0
  20. {sqlrite-0.5.0 → sqlrite-0.5.1}/Makefile +0 -0
  21. {sqlrite-0.5.0 → sqlrite-0.5.1}/README.md +0 -0
  22. {sqlrite-0.5.0 → sqlrite-0.5.1}/desktop/index.html +0 -0
  23. {sqlrite-0.5.0 → sqlrite-0.5.1}/desktop/package-lock.json +0 -0
  24. {sqlrite-0.5.0 → sqlrite-0.5.1}/desktop/src/App.svelte +0 -0
  25. {sqlrite-0.5.0 → sqlrite-0.5.1}/desktop/src/app.css +0 -0
  26. {sqlrite-0.5.0 → sqlrite-0.5.1}/desktop/src/main.ts +0 -0
  27. {sqlrite-0.5.0 → sqlrite-0.5.1}/desktop/src/vite-env.d.ts +0 -0
  28. {sqlrite-0.5.0 → sqlrite-0.5.1}/desktop/svelte.config.js +0 -0
  29. {sqlrite-0.5.0 → sqlrite-0.5.1}/desktop/tsconfig.json +0 -0
  30. {sqlrite-0.5.0 → sqlrite-0.5.1}/desktop/vite.config.ts +0 -0
  31. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/_index.md +0 -0
  32. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/architecture.md +0 -0
  33. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/ask-backend-examples.md +0 -0
  34. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/ask.md +0 -0
  35. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/design-decisions.md +0 -0
  36. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/desktop.md +0 -0
  37. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/embedding.md +0 -0
  38. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/fts.md +0 -0
  39. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/getting-started.md +0 -0
  40. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/mcp.md +0 -0
  41. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/pager.md +0 -0
  42. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/phase-7-plan.md +0 -0
  43. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/phase-8-plan.md +0 -0
  44. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/release-plan.md +0 -0
  45. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/release-secrets.md +0 -0
  46. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/roadmap.md +0 -0
  47. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/smoke-test.md +0 -0
  48. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/sql-engine.md +0 -0
  49. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/storage-model.md +0 -0
  50. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/supported-sql.md +0 -0
  51. {sqlrite-0.5.0 → sqlrite-0.5.1}/docs/usage.md +0 -0
  52. {sqlrite-0.5.0 → sqlrite-0.5.1}/examples/README.md +0 -0
  53. {sqlrite-0.5.0 → sqlrite-0.5.1}/examples/c/Makefile +0 -0
  54. {sqlrite-0.5.0 → sqlrite-0.5.1}/examples/c/hello.c +0 -0
  55. {sqlrite-0.5.0 → sqlrite-0.5.1}/examples/go/go.mod +0 -0
  56. {sqlrite-0.5.0 → sqlrite-0.5.1}/examples/go/hello.go +0 -0
  57. {sqlrite-0.5.0 → sqlrite-0.5.1}/examples/hybrid-retrieval/README.md +0 -0
  58. {sqlrite-0.5.0 → sqlrite-0.5.1}/examples/hybrid-retrieval/hybrid_retrieval.rs +0 -0
  59. {sqlrite-0.5.0 → sqlrite-0.5.1}/examples/nodejs/hello.mjs +0 -0
  60. {sqlrite-0.5.0 → sqlrite-0.5.1}/examples/python/hello.py +0 -0
  61. {sqlrite-0.5.0 → sqlrite-0.5.1}/examples/rust/quickstart.rs +0 -0
  62. {sqlrite-0.5.0 → sqlrite-0.5.1}/examples/wasm/Makefile +0 -0
  63. {sqlrite-0.5.0 → sqlrite-0.5.1}/examples/wasm/index.html +0 -0
  64. {sqlrite-0.5.0 → sqlrite-0.5.1}/examples/wasm/server.mjs +0 -0
  65. {sqlrite-0.5.0 → sqlrite-0.5.1}/images/SQLRite - Desktop.png +0 -0
  66. {sqlrite-0.5.0 → sqlrite-0.5.1}/images/SQLRite Data Structures.png +0 -0
  67. {sqlrite-0.5.0 → sqlrite-0.5.1}/images/SQLRite Simple SQL Execution High Level Diagram.png +0 -0
  68. {sqlrite-0.5.0 → sqlrite-0.5.1}/images/SQLRite Simple SQL INSERT Execution High Level Diagram (Insert Row).png +0 -0
  69. {sqlrite-0.5.0 → sqlrite-0.5.1}/images/SQLRite Simple SQL INSERT Execution High Level Diagram.png +0 -0
  70. {sqlrite-0.5.0 → sqlrite-0.5.1}/images/SQLRite_logo.png +0 -0
  71. {sqlrite-0.5.0 → sqlrite-0.5.1}/images/architecture.png +0 -0
  72. {sqlrite-0.5.0 → sqlrite-0.5.1}/rust-toolchain.toml +0 -0
  73. {sqlrite-0.5.0 → sqlrite-0.5.1}/samples/AST.delete.example +0 -0
  74. {sqlrite-0.5.0 → sqlrite-0.5.1}/samples/AST.insert.exemple +0 -0
  75. {sqlrite-0.5.0 → sqlrite-0.5.1}/samples/AST.select.example +0 -0
  76. {sqlrite-0.5.0 → sqlrite-0.5.1}/samples/AST.update.example +0 -0
  77. {sqlrite-0.5.0 → sqlrite-0.5.1}/samples/CREATE TABLE sqlrite_schema.sql +0 -0
  78. {sqlrite-0.5.0 → sqlrite-0.5.1}/samples/CREATE_TABLE with duplicate.sql +0 -0
  79. {sqlrite-0.5.0 → sqlrite-0.5.1}/samples/CREATE_TABLE.sql +0 -0
  80. {sqlrite-0.5.0 → sqlrite-0.5.1}/samples/INSERT.sql +0 -0
  81. {sqlrite-0.5.0 → sqlrite-0.5.1}/scripts/bump-version.sh +0 -0
  82. {sqlrite-0.5.0 → sqlrite-0.5.1}/sdk/go/README.md +0 -0
  83. {sqlrite-0.5.0 → sqlrite-0.5.1}/sdk/go/ask.go +0 -0
  84. {sqlrite-0.5.0 → sqlrite-0.5.1}/sdk/go/ask_test.go +0 -0
  85. {sqlrite-0.5.0 → sqlrite-0.5.1}/sdk/go/conn.go +0 -0
  86. {sqlrite-0.5.0 → sqlrite-0.5.1}/sdk/go/go.mod +0 -0
  87. {sqlrite-0.5.0 → sqlrite-0.5.1}/sdk/go/rows.go +0 -0
  88. {sqlrite-0.5.0 → sqlrite-0.5.1}/sdk/go/sqlrite.go +0 -0
  89. {sqlrite-0.5.0 → sqlrite-0.5.1}/sdk/go/sqlrite_test.go +0 -0
  90. {sqlrite-0.5.0 → sqlrite-0.5.1}/sdk/go/stmt.go +0 -0
  91. {sqlrite-0.5.0 → sqlrite-0.5.1}/sdk/python/README.md +0 -0
  92. {sqlrite-0.5.0 → sqlrite-0.5.1}/sdk/python/src/lib.rs +0 -0
  93. {sqlrite-0.5.0 → sqlrite-0.5.1}/sdk/python/tests/test_ask.py +0 -0
  94. {sqlrite-0.5.0 → sqlrite-0.5.1}/sdk/python/tests/test_sqlrite.py +0 -0
  95. {sqlrite-0.5.0 → sqlrite-0.5.1}/sqlrite-ask/README.md +0 -0
  96. {sqlrite-0.5.0 → sqlrite-0.5.1}/sqlrite-ask/src/lib.rs +0 -0
  97. {sqlrite-0.5.0 → sqlrite-0.5.1}/sqlrite-ask/src/prompt.rs +0 -0
  98. {sqlrite-0.5.0 → sqlrite-0.5.1}/sqlrite-ask/src/provider/anthropic.rs +0 -0
  99. {sqlrite-0.5.0 → sqlrite-0.5.1}/sqlrite-ask/src/provider/mock.rs +0 -0
  100. {sqlrite-0.5.0 → sqlrite-0.5.1}/sqlrite-ask/src/provider/mod.rs +0 -0
  101. {sqlrite-0.5.0 → sqlrite-0.5.1}/sqlrite-ask/tests/anthropic_http.rs +0 -0
  102. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/ask/mod.rs +0 -0
  103. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/ask/schema.rs +0 -0
  104. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/connection.rs +0 -0
  105. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/error.rs +0 -0
  106. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/lib.rs +0 -0
  107. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/main.rs +0 -0
  108. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/meta_command/mod.rs +0 -0
  109. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/repl/mod.rs +0 -0
  110. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/db/database.rs +0 -0
  111. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/db/mod.rs +0 -0
  112. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/db/secondary_index.rs +0 -0
  113. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/db/table.rs +0 -0
  114. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/executor.rs +0 -0
  115. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/fts/bm25.rs +0 -0
  116. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/fts/mod.rs +0 -0
  117. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/fts/posting_list.rs +0 -0
  118. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/fts/tokenizer.rs +0 -0
  119. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/hnsw.rs +0 -0
  120. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/mod.rs +0 -0
  121. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/allocator.rs +0 -0
  122. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/cell.rs +0 -0
  123. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/file.rs +0 -0
  124. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/freelist.rs +0 -0
  125. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/fts_cell.rs +0 -0
  126. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/header.rs +0 -0
  127. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/hnsw_cell.rs +0 -0
  128. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/index_cell.rs +0 -0
  129. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/interior_page.rs +0 -0
  130. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/page.rs +0 -0
  131. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/pager.rs +0 -0
  132. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/table_page.rs +0 -0
  133. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/varint.rs +0 -0
  134. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/pager/wal.rs +0 -0
  135. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/parser/create.rs +0 -0
  136. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/parser/insert.rs +0 -0
  137. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/parser/mod.rs +0 -0
  138. {sqlrite-0.5.0 → sqlrite-0.5.1}/src/sql/parser/select.rs +0 -0
  139. {sqlrite-0.5.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.5.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.5.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.5.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.5.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.5.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.5.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.5.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.5.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.5.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.5.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.5.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
  ```
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "sqlrite"
7
- version = "0.5.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.5.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.5.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"
@@ -267,6 +267,43 @@ fn save_database_with_mode(db: &mut Database, path: &Path, compact: bool) -> Res
267
267
  read_old_rootpages(&pager, old_header.schema_root_page)?
268
268
  };
269
269
 
270
+ // SQLR-1 — snapshot every prior B-Tree's page set NOW, before any
271
+ // staging starts. `Pager::read_page` shadows on-disk bytes with the
272
+ // current `staged` buffer, so if we deferred these walks until each
273
+ // object's turn in the staging loop, a *new* index added in this
274
+ // save would extend past the old high-water and overwrite the
275
+ // pages of any later-staged object whose old root sits in that
276
+ // range — including `sqlrite_master`, which is always staged last.
277
+ // The follow-up walk would then read the wrong B-Tree's bytes and
278
+ // either hand the allocator a bogus preferred pool or panic
279
+ // dispatching cells (a table-cell decoder vs. an index leaf, the
280
+ // shape of the original SQLR-1 panic). Walking up front pins each
281
+ // map to the committed bytes that were on disk before this save
282
+ // touched anything.
283
+ let old_preferred_pages: HashMap<(String, String), Vec<u32>> = if compact {
284
+ HashMap::new()
285
+ } else {
286
+ let mut map: HashMap<(String, String), Vec<u32>> = HashMap::new();
287
+ for ((kind, name), &root) in &old_rootpages {
288
+ // Tables can carry overflow chains; index/HNSW/FTS leaves
289
+ // never overflow in the current encoding, so the cheaper
290
+ // walk suffices for them.
291
+ let follow = kind == "table";
292
+ let pages = collect_pages_for_btree(&pager, root, follow)?;
293
+ map.insert((kind.clone(), name.clone()), pages);
294
+ }
295
+ map
296
+ };
297
+ let old_master_pages: Vec<u32> = if compact || old_header.schema_root_page == 0 {
298
+ Vec::new()
299
+ } else {
300
+ collect_pages_for_btree(
301
+ &pager,
302
+ old_header.schema_root_page,
303
+ /*follow_overflow=*/ true,
304
+ )?
305
+ };
306
+
270
307
  pager.clear_staged();
271
308
 
272
309
  // Allocator: in normal mode, seed with the old freelist; in compact
@@ -292,10 +329,8 @@ fn save_database_with_mode(db: &mut Database, path: &Path, compact: bool) -> Res
292
329
  )));
293
330
  }
294
331
  if !compact {
295
- if let Some(&prev_root) = old_rootpages.get(&("table".to_string(), name.to_string())) {
296
- let prev =
297
- collect_pages_for_btree(&pager, prev_root, /*follow_overflow=*/ true)?;
298
- alloc.set_preferred(prev);
332
+ if let Some(prev) = old_preferred_pages.get(&("table".to_string(), name.to_string())) {
333
+ alloc.set_preferred(prev.clone());
299
334
  }
300
335
  }
301
336
  let table = &db.tables[name];
@@ -322,12 +357,10 @@ fn save_database_with_mode(db: &mut Database, path: &Path, compact: bool) -> Res
322
357
  .sort_by(|(ta, ia), (tb, ib)| ta.tb_name.cmp(&tb.tb_name).then(ia.name.cmp(&ib.name)));
323
358
  for (_table, idx) in index_entries {
324
359
  if !compact {
325
- if let Some(&prev_root) =
326
- old_rootpages.get(&("index".to_string(), idx.name.to_string()))
360
+ if let Some(prev) =
361
+ old_preferred_pages.get(&("index".to_string(), idx.name.to_string()))
327
362
  {
328
- let prev =
329
- collect_pages_for_btree(&pager, prev_root, /*follow_overflow=*/ false)?;
330
- alloc.set_preferred(prev);
363
+ alloc.set_preferred(prev.clone());
331
364
  }
332
365
  }
333
366
  let rootpage = stage_index_btree(&mut pager, idx, &mut alloc)?;
@@ -359,12 +392,10 @@ fn save_database_with_mode(db: &mut Database, path: &Path, compact: bool) -> Res
359
392
  .sort_by(|(ta, ea), (tb, eb)| ta.tb_name.cmp(&tb.tb_name).then(ea.name.cmp(&eb.name)));
360
393
  for (table, entry) in hnsw_entries {
361
394
  if !compact {
362
- if let Some(&prev_root) =
363
- old_rootpages.get(&("index".to_string(), entry.name.to_string()))
395
+ if let Some(prev) =
396
+ old_preferred_pages.get(&("index".to_string(), entry.name.to_string()))
364
397
  {
365
- let prev =
366
- collect_pages_for_btree(&pager, prev_root, /*follow_overflow=*/ false)?;
367
- alloc.set_preferred(prev);
398
+ alloc.set_preferred(prev.clone());
368
399
  }
369
400
  }
370
401
  let rootpage = stage_hnsw_btree(&mut pager, &entry.index, &mut alloc)?;
@@ -401,12 +432,10 @@ fn save_database_with_mode(db: &mut Database, path: &Path, compact: bool) -> Res
401
432
  let any_fts = !fts_entries.is_empty();
402
433
  for (table, entry) in fts_entries {
403
434
  if !compact {
404
- if let Some(&prev_root) =
405
- old_rootpages.get(&("index".to_string(), entry.name.to_string()))
435
+ if let Some(prev) =
436
+ old_preferred_pages.get(&("index".to_string(), entry.name.to_string()))
406
437
  {
407
- let prev =
408
- collect_pages_for_btree(&pager, prev_root, /*follow_overflow=*/ false)?;
409
- alloc.set_preferred(prev);
438
+ alloc.set_preferred(prev.clone());
410
439
  }
411
440
  }
412
441
  let rootpage = stage_fts_btree(&mut pager, &entry.index, &mut alloc)?;
@@ -442,13 +471,11 @@ fn save_database_with_mode(db: &mut Database, path: &Path, compact: bool) -> Res
442
471
  ],
443
472
  )?;
444
473
  }
445
- if !compact && old_header.schema_root_page != 0 {
446
- let prev = collect_pages_for_btree(
447
- &pager,
448
- old_header.schema_root_page,
449
- /*follow_overflow=*/ true,
450
- )?;
451
- alloc.set_preferred(prev);
474
+ if !compact && !old_master_pages.is_empty() {
475
+ // Use the page list snapshotted before any staging touched
476
+ // disk; re-walking here would read whatever a new index
477
+ // already restaged on top of master's old root (SQLR-1).
478
+ alloc.set_preferred(old_master_pages.clone());
452
479
  }
453
480
  let master_root = stage_table_btree(&mut pager, &master, &mut alloc)?;
454
481
  alloc.finish_preferred();
@@ -2723,6 +2750,148 @@ mod tests {
2723
2750
  cleanup(&path);
2724
2751
  }
2725
2752
 
2753
+ /// SQLR-1 — `CREATE INDEX` on a wide table must round-trip when the
2754
+ /// index B-tree grows past one leaf and needs an interior level.
2755
+ /// Before the fix, the post-DDL auto-save panicked with
2756
+ /// `Internal("unknown paged-entry kind tag 0x4 …")` because a
2757
+ /// table-cell decoder was being run against an index leaf
2758
+ /// (`KIND_INDEX = 0x04`).
2759
+ ///
2760
+ /// 5 000 rows mirror the original repro from the issue and exceed
2761
+ /// every leaf-fanout cliff for the small `(rowid, value)` cells in
2762
+ /// a TEXT-keyed secondary index.
2763
+ #[test]
2764
+ fn secondary_index_with_interior_level_round_trips() {
2765
+ let path = tmp_path("sqlr1_wide_index");
2766
+ let mut db = Database::new("idx".to_string());
2767
+ db.source_path = Some(path.clone());
2768
+
2769
+ process_command(
2770
+ "CREATE TABLE bloat (id INTEGER PRIMARY KEY, payload TEXT);",
2771
+ &mut db,
2772
+ )
2773
+ .unwrap();
2774
+ // BEGIN/COMMIT collapses 5 000 inserts into one save (matches
2775
+ // `auto_vacuum_setup` and the issue's repro shape).
2776
+ process_command("BEGIN;", &mut db).unwrap();
2777
+ for i in 0..5000 {
2778
+ process_command(
2779
+ &format!("INSERT INTO bloat (payload) VALUES ('p-{i:08}');"),
2780
+ &mut db,
2781
+ )
2782
+ .unwrap();
2783
+ }
2784
+ process_command("COMMIT;", &mut db).unwrap();
2785
+
2786
+ // The DDL that used to panic.
2787
+ process_command("CREATE INDEX idx_p ON bloat (payload);", &mut db).unwrap();
2788
+
2789
+ // Reopen and verify lookups, plus that the index tree actually
2790
+ // grew an interior layer (otherwise this test wouldn't cover the
2791
+ // regression).
2792
+ drop(db);
2793
+ let loaded = open_database(&path, "idx".to_string()).unwrap();
2794
+ let bloat = loaded.get_table("bloat".to_string()).unwrap();
2795
+ let idx = bloat
2796
+ .index_by_name("idx_p")
2797
+ .expect("idx_p should survive close/reopen");
2798
+ assert!(!idx.is_unique);
2799
+
2800
+ // Spot-check the keyspace: first, middle, last value each map
2801
+ // back to exactly the row that carried them.
2802
+ for &(probe_i, expected_rowid) in &[(0i64, 1i64), (2500, 2501), (4999, 5000)] {
2803
+ let value = Value::Text(format!("p-{probe_i:08}"));
2804
+ let hits = idx.lookup(&value);
2805
+ assert_eq!(
2806
+ hits,
2807
+ vec![expected_rowid],
2808
+ "lookup({value:?}) should yield rowid {expected_rowid}",
2809
+ );
2810
+ }
2811
+
2812
+ // Confirm the index tree is multi-level (the regression's
2813
+ // necessary condition) — root must be an `InteriorNode` and
2814
+ // `find_leftmost_leaf` must reach a `TableLeaf` through it.
2815
+ let pager = loaded.pager.as_ref().unwrap();
2816
+ let mut master = build_empty_master_table();
2817
+ load_table_rows(pager, &mut master, pager.header().schema_root_page).unwrap();
2818
+ let idx_root = master
2819
+ .rowids()
2820
+ .into_iter()
2821
+ .find_map(
2822
+ |r| match (master.get_value("name", r), master.get_value("type", r)) {
2823
+ (Some(Value::Text(name)), Some(Value::Text(kind)))
2824
+ if name == "idx_p" && kind == "index" =>
2825
+ {
2826
+ match master.get_value("rootpage", r) {
2827
+ Some(Value::Integer(p)) => Some(p as u32),
2828
+ _ => None,
2829
+ }
2830
+ }
2831
+ _ => None,
2832
+ },
2833
+ )
2834
+ .expect("idx_p should appear in sqlrite_master");
2835
+ let root_buf = pager.read_page(idx_root).unwrap();
2836
+ assert_eq!(
2837
+ root_buf[0],
2838
+ PageType::InteriorNode as u8,
2839
+ "5 000-entry index must have an interior root — without one this test wouldn't cover SQLR-1",
2840
+ );
2841
+ let leaf = find_leftmost_leaf(pager, idx_root).unwrap();
2842
+ let leaf_buf = pager.read_page(leaf).unwrap();
2843
+ assert_eq!(leaf_buf[0], PageType::TableLeaf as u8);
2844
+
2845
+ cleanup(&path);
2846
+ }
2847
+
2848
+ /// SQLR-1 follow-on — the page-recycling path between two large
2849
+ /// versions of the same index name must not corrupt cell decoding.
2850
+ /// `DROP INDEX` returns its pages to the freelist; the next
2851
+ /// `CREATE INDEX` is free to reuse them. If the allocator hands an
2852
+ /// old index leaf to a *table* without zeroing it, an upstream
2853
+ /// table walk would see KIND_INDEX cells and panic.
2854
+ #[test]
2855
+ fn drop_then_recreate_wide_index_does_not_panic() {
2856
+ let path = tmp_path("sqlr1_drop_recreate");
2857
+ let mut db = Database::new("idx".to_string());
2858
+ db.source_path = Some(path.clone());
2859
+
2860
+ process_command(
2861
+ "CREATE TABLE bloat (id INTEGER PRIMARY KEY, payload TEXT);",
2862
+ &mut db,
2863
+ )
2864
+ .unwrap();
2865
+ process_command("BEGIN;", &mut db).unwrap();
2866
+ for i in 0..5000 {
2867
+ process_command(
2868
+ &format!("INSERT INTO bloat (payload) VALUES ('p-{i:08}');"),
2869
+ &mut db,
2870
+ )
2871
+ .unwrap();
2872
+ }
2873
+ process_command("COMMIT;", &mut db).unwrap();
2874
+
2875
+ process_command("CREATE INDEX idx_p ON bloat (payload);", &mut db).unwrap();
2876
+ process_command("DROP INDEX idx_p;", &mut db).unwrap();
2877
+ // Recreate from scratch — exercises the recycle path.
2878
+ process_command("CREATE INDEX idx_p ON bloat (payload);", &mut db).unwrap();
2879
+
2880
+ drop(db);
2881
+ let loaded = open_database(&path, "idx".to_string()).unwrap();
2882
+ let bloat = loaded.get_table("bloat".to_string()).unwrap();
2883
+ let idx = bloat
2884
+ .index_by_name("idx_p")
2885
+ .expect("idx_p should survive drop+recreate+reopen");
2886
+ assert_eq!(
2887
+ idx.lookup(&Value::Text("p-00002500".into())),
2888
+ vec![2501],
2889
+ "post-recycle lookup must still resolve correctly",
2890
+ );
2891
+
2892
+ cleanup(&path);
2893
+ }
2894
+
2726
2895
  #[test]
2727
2896
  fn deep_tree_round_trips() {
2728
2897
  // Force a 3-level tree by bypassing process_command (which prints
@@ -139,8 +139,17 @@ impl PagedEntry {
139
139
  }
140
140
 
141
141
  /// Dispatches on the kind tag and returns the appropriate variant.
142
+ ///
143
+ /// Only `KIND_LOCAL` and `KIND_OVERFLOW` are valid here — `PagedEntry`
144
+ /// is the table-leaf-cell type, so any other kind means a caller
145
+ /// pointed the wrong decoder at this page (the slot directory layout
146
+ /// is shared across leaf B-Trees, but secondary-index, HNSW, and
147
+ /// FTS leaves carry kind-specific cells decoded by `IndexCell::decode`,
148
+ /// `HnswNodeCell::decode`, and `FtsPostingCell::decode` respectively).
149
+ /// The named-kind error makes that mistake obvious next time.
142
150
  pub fn decode(buf: &[u8], pos: usize) -> Result<(PagedEntry, usize)> {
143
- match Cell::peek_kind(buf, pos)? {
151
+ let kind = Cell::peek_kind(buf, pos)?;
152
+ match kind {
144
153
  KIND_LOCAL => {
145
154
  let (c, n) = Cell::decode(buf, pos)?;
146
155
  Ok((PagedEntry::Local(c), n))
@@ -150,12 +159,41 @@ impl PagedEntry {
150
159
  Ok((PagedEntry::Overflow(r), n))
151
160
  }
152
161
  other => Err(SQLRiteError::Internal(format!(
153
- "unknown paged-entry kind tag {other:#x} at offset {pos}"
162
+ "PagedEntry::decode at offset {pos} got kind tag {other:#x} ({}); \
163
+ expected KIND_LOCAL (0x01) or KIND_OVERFLOW (0x02). \
164
+ The caller is reading a {} page with the table-leaf decoder.",
165
+ kind_name(other),
166
+ kind_btree_hint(other),
154
167
  ))),
155
168
  }
156
169
  }
157
170
  }
158
171
 
172
+ /// Human-readable label for a cell kind tag — used in error messages
173
+ /// to make wrong-decoder mistakes self-explanatory.
174
+ fn kind_name(tag: u8) -> &'static str {
175
+ match tag {
176
+ crate::sql::pager::cell::KIND_LOCAL => "KIND_LOCAL",
177
+ crate::sql::pager::cell::KIND_OVERFLOW => "KIND_OVERFLOW",
178
+ crate::sql::pager::cell::KIND_INTERIOR => "KIND_INTERIOR",
179
+ crate::sql::pager::cell::KIND_INDEX => "KIND_INDEX",
180
+ crate::sql::pager::cell::KIND_HNSW => "KIND_HNSW",
181
+ crate::sql::pager::cell::KIND_FTS_POSTING => "KIND_FTS_POSTING",
182
+ _ => "unknown kind",
183
+ }
184
+ }
185
+
186
+ /// Hint pointing at which B-Tree owns a cell of the given kind.
187
+ fn kind_btree_hint(tag: u8) -> &'static str {
188
+ match tag {
189
+ crate::sql::pager::cell::KIND_INTERIOR => "B-Tree interior",
190
+ crate::sql::pager::cell::KIND_INDEX => "secondary-index",
191
+ crate::sql::pager::cell::KIND_HNSW => "HNSW",
192
+ crate::sql::pager::cell::KIND_FTS_POSTING => "FTS",
193
+ _ => "non-table",
194
+ }
195
+ }
196
+
159
197
  /// Writes `bytes` into a chain of Overflow-typed pages, drawing each
160
198
  /// page number from the supplied [`PageAllocator`]. Returns the page
161
199
  /// number of the first link in the chain (the value to record in the
@@ -284,6 +322,50 @@ mod tests {
284
322
  assert_eq!(decoded, PagedEntry::Overflow(overflow));
285
323
  }
286
324
 
325
+ /// SQLR-1 — `PagedEntry::decode` is the table-leaf-cell decoder.
326
+ /// Pointing it at a secondary-index leaf used to surface as a
327
+ /// cryptic `unknown paged-entry kind tag 0x4`. The new error
328
+ /// message names the offending kind and the B-Tree the caller
329
+ /// is mistakenly walking.
330
+ #[test]
331
+ fn paged_entry_decode_rejects_index_kind_with_clear_error() {
332
+ use crate::sql::pager::index_cell::IndexCell;
333
+ let ic = IndexCell::new(42, Value::Text("alice".into()));
334
+ let bytes = ic.encode().unwrap();
335
+ let err = PagedEntry::decode(&bytes, 0).unwrap_err();
336
+ let msg = format!("{err}");
337
+ assert!(
338
+ msg.contains("KIND_INDEX"),
339
+ "expected error to name KIND_INDEX, got: {msg}",
340
+ );
341
+ assert!(
342
+ msg.contains("secondary-index"),
343
+ "expected error to identify the secondary-index B-Tree, got: {msg}",
344
+ );
345
+ }
346
+
347
+ /// Symmetric coverage for HNSW and FTS — same wrong-decoder shape,
348
+ /// same diagnostic guarantee.
349
+ #[test]
350
+ fn paged_entry_decode_rejects_hnsw_and_fts_kinds() {
351
+ use crate::sql::pager::cell::{KIND_FTS_POSTING, KIND_HNSW};
352
+ // Build minimal byte sequences carrying the right kind tag at
353
+ // the right offset. Body content past the kind tag doesn't
354
+ // matter — we expect the dispatch to short-circuit on the tag.
355
+ for (tag, hint) in [(KIND_HNSW, "HNSW"), (KIND_FTS_POSTING, "FTS")] {
356
+ // body_len declared as 1 (the kind tag itself); body is the
357
+ // tag and nothing else. Honest about what's in the buffer
358
+ // so a future tightening of `peek_kind` doesn't bite us.
359
+ let bytes = vec![/*body_len varint=*/ 1u8, tag];
360
+ let err = PagedEntry::decode(&bytes, 0).unwrap_err();
361
+ let msg = format!("{err}");
362
+ assert!(
363
+ msg.contains(hint),
364
+ "expected error to identify the {hint} B-Tree, got: {msg}",
365
+ );
366
+ }
367
+ }
368
+
287
369
  #[test]
288
370
  fn peek_rowid_works_for_both_kinds() {
289
371
  let local = Cell::new(99, vec![Some(Value::Integer(1))]);
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes