sqlrite 0.1.13__tar.gz → 0.1.15__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 (110) hide show
  1. {sqlrite-0.1.13 → sqlrite-0.1.15}/Cargo.lock +5 -5
  2. {sqlrite-0.1.13 → sqlrite-0.1.15}/Cargo.toml +1 -1
  3. {sqlrite-0.1.13 → sqlrite-0.1.15}/PKG-INFO +1 -1
  4. {sqlrite-0.1.13 → sqlrite-0.1.15}/desktop/package.json +1 -1
  5. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/phase-7-plan.md +3 -3
  6. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/roadmap.md +1 -1
  7. {sqlrite-0.1.13 → sqlrite-0.1.15}/pyproject.toml +1 -1
  8. {sqlrite-0.1.13 → sqlrite-0.1.15}/sdk/python/Cargo.toml +1 -1
  9. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/db/table.rs +77 -1
  10. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/executor.rs +360 -29
  11. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/hnsw.rs +44 -0
  12. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/mod.rs +169 -0
  13. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/pager/cell.rs +15 -0
  14. sqlrite-0.1.15/src/sql/pager/hnsw_cell.rs +258 -0
  15. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/pager/mod.rs +453 -2
  16. {sqlrite-0.1.13 → sqlrite-0.1.15}/.github/workflows/ci.yml +0 -0
  17. {sqlrite-0.1.13 → sqlrite-0.1.15}/.github/workflows/release-pr.yml +0 -0
  18. {sqlrite-0.1.13 → sqlrite-0.1.15}/.github/workflows/release.yml +0 -0
  19. {sqlrite-0.1.13 → sqlrite-0.1.15}/.github/workflows/rust.yml +0 -0
  20. {sqlrite-0.1.13 → sqlrite-0.1.15}/.gitignore +0 -0
  21. {sqlrite-0.1.13 → sqlrite-0.1.15}/CODE_OF_CONDUCT.md +0 -0
  22. {sqlrite-0.1.13 → sqlrite-0.1.15}/LICENSE +0 -0
  23. {sqlrite-0.1.13 → sqlrite-0.1.15}/MAINTAINERS +0 -0
  24. {sqlrite-0.1.13 → sqlrite-0.1.15}/Makefile +0 -0
  25. {sqlrite-0.1.13 → sqlrite-0.1.15}/README.md +0 -0
  26. {sqlrite-0.1.13 → sqlrite-0.1.15}/desktop/index.html +0 -0
  27. {sqlrite-0.1.13 → sqlrite-0.1.15}/desktop/package-lock.json +0 -0
  28. {sqlrite-0.1.13 → sqlrite-0.1.15}/desktop/src/App.svelte +0 -0
  29. {sqlrite-0.1.13 → sqlrite-0.1.15}/desktop/src/app.css +0 -0
  30. {sqlrite-0.1.13 → sqlrite-0.1.15}/desktop/src/main.ts +0 -0
  31. {sqlrite-0.1.13 → sqlrite-0.1.15}/desktop/src/vite-env.d.ts +0 -0
  32. {sqlrite-0.1.13 → sqlrite-0.1.15}/desktop/svelte.config.js +0 -0
  33. {sqlrite-0.1.13 → sqlrite-0.1.15}/desktop/tsconfig.json +0 -0
  34. {sqlrite-0.1.13 → sqlrite-0.1.15}/desktop/vite.config.ts +0 -0
  35. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/_index.md +0 -0
  36. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/architecture.md +0 -0
  37. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/design-decisions.md +0 -0
  38. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/desktop.md +0 -0
  39. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/embedding.md +0 -0
  40. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/file-format.md +0 -0
  41. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/getting-started.md +0 -0
  42. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/pager.md +0 -0
  43. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/release-plan.md +0 -0
  44. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/release-secrets.md +0 -0
  45. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/smoke-test.md +0 -0
  46. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/sql-engine.md +0 -0
  47. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/storage-model.md +0 -0
  48. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/supported-sql.md +0 -0
  49. {sqlrite-0.1.13 → sqlrite-0.1.15}/docs/usage.md +0 -0
  50. {sqlrite-0.1.13 → sqlrite-0.1.15}/examples/README.md +0 -0
  51. {sqlrite-0.1.13 → sqlrite-0.1.15}/examples/c/Makefile +0 -0
  52. {sqlrite-0.1.13 → sqlrite-0.1.15}/examples/c/hello.c +0 -0
  53. {sqlrite-0.1.13 → sqlrite-0.1.15}/examples/go/go.mod +0 -0
  54. {sqlrite-0.1.13 → sqlrite-0.1.15}/examples/go/hello.go +0 -0
  55. {sqlrite-0.1.13 → sqlrite-0.1.15}/examples/nodejs/hello.mjs +0 -0
  56. {sqlrite-0.1.13 → sqlrite-0.1.15}/examples/python/hello.py +0 -0
  57. {sqlrite-0.1.13 → sqlrite-0.1.15}/examples/rust/quickstart.rs +0 -0
  58. {sqlrite-0.1.13 → sqlrite-0.1.15}/examples/wasm/Makefile +0 -0
  59. {sqlrite-0.1.13 → sqlrite-0.1.15}/examples/wasm/index.html +0 -0
  60. {sqlrite-0.1.13 → sqlrite-0.1.15}/images/SQLRite - Desktop.png +0 -0
  61. {sqlrite-0.1.13 → sqlrite-0.1.15}/images/SQLRite Data Structures.png +0 -0
  62. {sqlrite-0.1.13 → sqlrite-0.1.15}/images/SQLRite Simple SQL Execution High Level Diagram.png +0 -0
  63. {sqlrite-0.1.13 → sqlrite-0.1.15}/images/SQLRite Simple SQL INSERT Execution High Level Diagram (Insert Row).png +0 -0
  64. {sqlrite-0.1.13 → sqlrite-0.1.15}/images/SQLRite Simple SQL INSERT Execution High Level Diagram.png +0 -0
  65. {sqlrite-0.1.13 → sqlrite-0.1.15}/images/SQLRite_logo.png +0 -0
  66. {sqlrite-0.1.13 → sqlrite-0.1.15}/images/architecture.png +0 -0
  67. {sqlrite-0.1.13 → sqlrite-0.1.15}/rust-toolchain.toml +0 -0
  68. {sqlrite-0.1.13 → sqlrite-0.1.15}/samples/AST.delete.example +0 -0
  69. {sqlrite-0.1.13 → sqlrite-0.1.15}/samples/AST.insert.exemple +0 -0
  70. {sqlrite-0.1.13 → sqlrite-0.1.15}/samples/AST.select.example +0 -0
  71. {sqlrite-0.1.13 → sqlrite-0.1.15}/samples/AST.update.example +0 -0
  72. {sqlrite-0.1.13 → sqlrite-0.1.15}/samples/CREATE TABLE sqlrite_schema.sql +0 -0
  73. {sqlrite-0.1.13 → sqlrite-0.1.15}/samples/CREATE_TABLE with duplicate.sql +0 -0
  74. {sqlrite-0.1.13 → sqlrite-0.1.15}/samples/CREATE_TABLE.sql +0 -0
  75. {sqlrite-0.1.13 → sqlrite-0.1.15}/samples/INSERT.sql +0 -0
  76. {sqlrite-0.1.13 → sqlrite-0.1.15}/scripts/bump-version.sh +0 -0
  77. {sqlrite-0.1.13 → sqlrite-0.1.15}/sdk/go/README.md +0 -0
  78. {sqlrite-0.1.13 → sqlrite-0.1.15}/sdk/go/conn.go +0 -0
  79. {sqlrite-0.1.13 → sqlrite-0.1.15}/sdk/go/go.mod +0 -0
  80. {sqlrite-0.1.13 → sqlrite-0.1.15}/sdk/go/rows.go +0 -0
  81. {sqlrite-0.1.13 → sqlrite-0.1.15}/sdk/go/sqlrite.go +0 -0
  82. {sqlrite-0.1.13 → sqlrite-0.1.15}/sdk/go/sqlrite_test.go +0 -0
  83. {sqlrite-0.1.13 → sqlrite-0.1.15}/sdk/go/stmt.go +0 -0
  84. {sqlrite-0.1.13 → sqlrite-0.1.15}/sdk/python/README.md +0 -0
  85. {sqlrite-0.1.13 → sqlrite-0.1.15}/sdk/python/src/lib.rs +0 -0
  86. {sqlrite-0.1.13 → sqlrite-0.1.15}/sdk/python/tests/test_sqlrite.py +0 -0
  87. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/connection.rs +0 -0
  88. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/error.rs +0 -0
  89. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/lib.rs +0 -0
  90. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/main.rs +0 -0
  91. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/meta_command/mod.rs +0 -0
  92. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/repl/mod.rs +0 -0
  93. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/db/database.rs +0 -0
  94. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/db/mod.rs +0 -0
  95. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/db/secondary_index.rs +0 -0
  96. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/pager/file.rs +0 -0
  97. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/pager/header.rs +0 -0
  98. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/pager/index_cell.rs +0 -0
  99. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/pager/interior_page.rs +0 -0
  100. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/pager/overflow.rs +0 -0
  101. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/pager/page.rs +0 -0
  102. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/pager/pager.rs +0 -0
  103. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/pager/table_page.rs +0 -0
  104. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/pager/varint.rs +0 -0
  105. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/pager/wal.rs +0 -0
  106. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/parser/create.rs +0 -0
  107. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/parser/insert.rs +0 -0
  108. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/parser/mod.rs +0 -0
  109. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/parser/select.rs +0 -0
  110. {sqlrite-0.1.13 → sqlrite-0.1.15}/src/sql/tokenizer.rs +0 -0
@@ -3736,7 +3736,7 @@ dependencies = [
3736
3736
 
3737
3737
  [[package]]
3738
3738
  name = "sqlrite-desktop"
3739
- version = "0.1.13"
3739
+ version = "0.1.15"
3740
3740
  dependencies = [
3741
3741
  "serde",
3742
3742
  "serde_json",
@@ -3748,7 +3748,7 @@ dependencies = [
3748
3748
 
3749
3749
  [[package]]
3750
3750
  name = "sqlrite-engine"
3751
- version = "0.1.13"
3751
+ version = "0.1.15"
3752
3752
  dependencies = [
3753
3753
  "clap",
3754
3754
  "env_logger",
@@ -3763,7 +3763,7 @@ dependencies = [
3763
3763
 
3764
3764
  [[package]]
3765
3765
  name = "sqlrite-ffi"
3766
- version = "0.1.13"
3766
+ version = "0.1.15"
3767
3767
  dependencies = [
3768
3768
  "cbindgen",
3769
3769
  "sqlrite-engine",
@@ -3771,7 +3771,7 @@ dependencies = [
3771
3771
 
3772
3772
  [[package]]
3773
3773
  name = "sqlrite-nodejs"
3774
- version = "0.1.13"
3774
+ version = "0.1.15"
3775
3775
  dependencies = [
3776
3776
  "napi",
3777
3777
  "napi-build",
@@ -3781,7 +3781,7 @@ dependencies = [
3781
3781
 
3782
3782
  [[package]]
3783
3783
  name = "sqlrite-python"
3784
- version = "0.1.13"
3784
+ version = "0.1.15"
3785
3785
  dependencies = [
3786
3786
  "pyo3",
3787
3787
  "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.1.13"
30
+ version = "0.1.15"
31
31
  authors = ["Joao Henrique Machado Silva <joaoh82@gmail.com>"]
32
32
  edition = "2024"
33
33
  rust-version = "1.85"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlrite
3
- Version: 0.1.13
3
+ Version: 0.1.15
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.1.13",
4
+ "version": "0.1.15",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
@@ -160,9 +160,9 @@ SELECT id, title FROM docs ORDER BY embedding <-> [0.1, ...] LIMIT 10;
160
160
 
161
161
  > **Scope correction (2026-04-27, post-7c):** Re-scoping during implementation showed 7d works out to ~1300 LOC across three logical chunks, more than the original ~700-900 estimate and too much for one reviewable PR. Splitting into three:
162
162
  >
163
- > - **7d.1 — Pure HNSW algorithm** *(~700 LOC).* `src/sql/hnsw.rs` standalone module: insert + search + layer assignment + beam search per layer + L2/cosine/dot distance dispatch. No SQL integration yet — vectors are passed in via a `get_vec` closure so the algorithm doesn't depend on table types. Tests verify recall@k ≥ 0.95 vs brute-force on randomly-generated vector sets; deterministic via a fixed RNG seed.
164
- > - **7d.2 — SQL integration** *(~400 LOC).* `CREATE INDEX … USING hnsw (col)` parser + engine, INSERT wiring (also calls `hnsw.insert()`), query optimizer hook (recognizes `ORDER BY vec_distance_*(col, literal) LIMIT k` and probes the HNSW instead of full-scanning). HNSW lives in memory only at this point — gets rebuilt on every database open.
165
- > - **7d.3 — Persistence** *(~300 LOC).* Wire HNSW into the cell format: new `KIND_HNSW` cell tag, page-tree storage parallel to secondary indexes, save/reopen round-trip.
163
+ > - **✅ 7d.1 — Pure HNSW algorithm** *(~700 LOC, shipped in v0.1.13).* `src/sql/hnsw.rs` standalone module: insert + search + layer assignment + beam search per layer + L2/cosine/dot distance dispatch. No SQL integration yet — vectors are passed in via a `get_vec` closure so the algorithm doesn't depend on table types. Tests verify recall@k ≥ 0.95 vs brute-force on randomly-generated vector sets; deterministic via a fixed RNG seed.
164
+ > - **✅ 7d.2 — SQL integration** *(~500 LOC).* `CREATE INDEX … USING hnsw (col)` parser + engine, INSERT wiring (also calls `hnsw.insert()` incrementally), query optimizer hook (recognizes `ORDER BY vec_distance_l2(col, literal) LIMIT k` and probes the HNSW instead of full-scanning). HNSW lives in memory only at this point; the **CREATE INDEX SQL persists in `sqlrite_master` and reopen rebuilds the graph from current rows** partial persistence ahead of 7d.3. DELETE/UPDATE on HNSW-indexed tables refused with helpful error pointing at 7d.3.
165
+ > - **✅ 7d.3 — Persistence** *(~600 LOC).* New `KIND_HNSW` cell tag and `HnswNodeCell` encoding (varint node_id + per-layer neighbor lists). Each HNSW index gets its own page tree parallel to secondary indexes. Open path loads cells directly into `HnswIndex::from_persisted_nodes` — no algorithm runs, exact bit-for-bit reproduction. Also unblocks DELETE / UPDATE on HNSW-indexed tables: those mark the index `needs_rebuild`, save rebuilds from current rows before staging. ~2× the original 300-LOC estimate because the cell encoding + tests + rebuild path together added more than expected.
166
166
  >
167
167
  > Each 7d.x ships as its own PR + release wave. The user-facing value lands at 7d.2; 7d.3 closes the persistence loop. 7d.1 is foundational but ships a tested algorithmic primitive on its own — useful as documentation of the engine's "from scratch" theme.
168
168
 
@@ -473,7 +473,7 @@ Approved sub-phases (Q1–Q10 resolved):
473
473
  - **✅ 7a — `VECTOR(N)` column type** *(v0.1.10)* — dense fixed-dimension f32 storage via the existing cell encoding; format bumped to v4. Bracket-array literal syntax `[0.1, 0.2, …]` (Q7).
474
474
  - **✅ 7b — Distance functions** *(v0.1.11)* — `vec_distance_l2/cosine/dot`, plus the ORDER BY-expressions parser change so KNN queries work end-to-end. Operators (`<->` `<=>` `<#>`) deferred to **7b.1** — sqlparser doesn't parse them natively, contradicting Q6's "tiny parser change" assumption.
475
475
  - **✅ 7c — Brute-force KNN executor optimization** — bounded `BinaryHeap` of size k for `ORDER BY <expr> LIMIT k`. ~1.8× faster than full-sort at N=10k for cheap keys; bigger gains on expensive keys like `vec_distance_l2`.
476
- - **7d — HNSW ANN index** — `CREATE INDEX … USING hnsw (col)`; persisted as cell-encoded graph. Fixed defaults `M=16, ef_construction=200, ef_search=50` (Q2).
476
+ - **✅ 7d — HNSW ANN index** — three PRs: 7d.1 (algorithm w/ recall@10 ≥ 0.95), 7d.2 (SQL integration + query optimizer), 7d.3 (persistence + DELETE/UPDATE rebuild). `CREATE INDEX … USING hnsw (col)`; fixed defaults `M=16, ef_construction=200, ef_search=50` (Q2). New `KIND_HNSW` cell tag.
477
477
  - **7e — JSON column type + path queries** — `JSON` data type stored as bincoded `serde_json::Value` (Q3); `json_extract` / `json_array_length` / `json_object_keys` / `json_type`.
478
478
  - **7f — ~~Full-text search with BM25~~** — **deferred to Phase 8** (Q1).
479
479
  - **7g — `ask()` API across the product surface** — natural-language → SQL via Anthropic API (Q4), Anthropic-first then OpenAI + Ollama follow-ups. Foundational 7g.1 introduces a new `sqlrite-ask` crate (Q10 — separate crate, not a feature flag). Thin per-product adapters in 7g.2-7g.8 cover REPL, desktop, Python, Node.js, Go, WASM (JS-callback shape per Q9), and the MCP `ask` tool.
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "sqlrite"
7
- version = "0.1.13"
7
+ version = "0.1.15"
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.1.13"
3
+ version = "0.1.15"
4
4
  authors = ["Joao Henrique Machado Silva <joaoh82@gmail.com>"]
5
5
  edition = "2024"
6
6
  rust-version = "1.85"
@@ -1,5 +1,6 @@
1
1
  use crate::error::{Result, SQLRiteError};
2
2
  use crate::sql::db::secondary_index::{IndexOrigin, SecondaryIndex};
3
+ use crate::sql::hnsw::HnswIndex;
3
4
  use crate::sql::parser::create::CreateQuery;
4
5
  use std::collections::{BTreeMap, HashMap};
5
6
  use std::fmt;
@@ -118,12 +119,37 @@ pub struct Table {
118
119
  /// add more. Looking up an index: iterate by column name, or by index
119
120
  /// name via `Table::index_by_name`.
120
121
  pub secondary_indexes: Vec<SecondaryIndex>,
122
+ /// HNSW indexes on VECTOR columns (Phase 7d.2). Maintained in lockstep
123
+ /// with row storage on INSERT (incremental); rebuilt on open from the
124
+ /// persisted CREATE INDEX SQL. The graph itself is NOT yet persisted —
125
+ /// see Phase 7d.3 for cell-encoded graph storage.
126
+ pub hnsw_indexes: Vec<HnswIndexEntry>,
121
127
  /// ROWID of most recent insert.
122
128
  pub last_rowid: i64,
123
129
  /// PRIMARY KEY column name, or "-1" if the table has no PRIMARY KEY.
124
130
  pub primary_key: String,
125
131
  }
126
132
 
133
+ /// One HNSW index attached to a table. Phase 7d.2 only supports L2
134
+ /// distance; cosine and dot are 7d.x follow-ups (would require either
135
+ /// distinct USING methods like `hnsw_cosine` or a `WITH (metric = …)`
136
+ /// clause — see `docs/phase-7-plan.md` for the deferred decision).
137
+ #[derive(Debug, Clone)]
138
+ pub struct HnswIndexEntry {
139
+ /// User-supplied name from `CREATE INDEX <name> …`. Unique across
140
+ /// both `secondary_indexes` and `hnsw_indexes` on a given table.
141
+ pub name: String,
142
+ /// The VECTOR column this index covers.
143
+ pub column_name: String,
144
+ /// The graph itself.
145
+ pub index: HnswIndex,
146
+ /// Phase 7d.3 — true iff a DELETE or UPDATE-on-vector-col has
147
+ /// invalidated the graph since the last rebuild. INSERT maintains
148
+ /// the graph incrementally and leaves this false. The next save
149
+ /// rebuilds dirty indexes from current rows before serializing.
150
+ pub needs_rebuild: bool,
151
+ }
152
+
127
153
  impl Table {
128
154
  pub fn new(create_query: CreateQuery) -> Self {
129
155
  let table_name = create_query.table_name;
@@ -194,6 +220,11 @@ impl Table {
194
220
  columns: table_cols,
195
221
  rows: table_rows,
196
222
  secondary_indexes,
223
+ // HNSW indexes only land via explicit CREATE INDEX … USING hnsw
224
+ // statements (Phase 7d.2); never auto-created at CREATE TABLE
225
+ // time, because there's no UNIQUE-style constraint that
226
+ // implies a vector index.
227
+ hnsw_indexes: Vec::new(),
197
228
  last_rowid: 0,
198
229
  primary_key,
199
230
  }
@@ -217,6 +248,10 @@ impl Table {
217
248
  columns: self.columns.clone(),
218
249
  rows: Arc::new(Mutex::new(cloned_rows)),
219
250
  secondary_indexes: self.secondary_indexes.clone(),
251
+ // HnswIndexEntry derives Clone, so the snapshot owns its own
252
+ // graph copy. Phase 4f's snapshot-rollback semantics require
253
+ // the snapshot to be fully decoupled from live state.
254
+ hnsw_indexes: self.hnsw_indexes.clone(),
220
255
  last_rowid: self.last_rowid,
221
256
  primary_key: self.primary_key.clone(),
222
257
  }
@@ -813,16 +848,57 @@ impl Table {
813
848
 
814
849
  // Step 2: maintain the secondary index (if any). insert() is a
815
850
  // no-op for Value::Null and cheap for other value kinds.
816
- if let Some(v) = typed_value {
851
+ if let Some(v) = typed_value.clone() {
817
852
  if let Some(idx) = self.index_for_column_mut(key) {
818
853
  idx.insert(&v, next_rowid)?;
819
854
  }
820
855
  }
856
+
857
+ // Step 3 (Phase 7d.2): maintain any HNSW indexes on this column.
858
+ // The HNSW algorithm needs access to other rows' vectors when
859
+ // wiring up neighbor edges, so build a get_vec closure that
860
+ // pulls from the table's row storage (which we *just* updated
861
+ // with the new value).
862
+ if let Some(Value::Vector(new_vec)) = typed_value {
863
+ self.maintain_hnsw_on_insert(key, next_rowid, &new_vec);
864
+ }
821
865
  }
822
866
  self.last_rowid = next_rowid;
823
867
  Ok(())
824
868
  }
825
869
 
870
+ /// After a row insert, push the new (rowid, vector) into every HNSW
871
+ /// index whose column matches `column`. Split out of `insert_row` so
872
+ /// the borrowing dance — we need both `&self.rows` (read other
873
+ /// vectors) and `&mut self.hnsw_indexes` (insert into the graph) —
874
+ /// stays localized.
875
+ fn maintain_hnsw_on_insert(&mut self, column: &str, rowid: i64, new_vec: &[f32]) {
876
+ // Snapshot the current vector storage so the get_vec closure
877
+ // doesn't fight with `&mut self.hnsw_indexes`. For a typical
878
+ // HNSW insert we touch ef_construction × log(N) other vectors,
879
+ // so the snapshot cost is small relative to the graph wiring.
880
+ let mut vec_snapshot: HashMap<i64, Vec<f32>> = HashMap::new();
881
+ {
882
+ let row_data = self.rows.lock().expect("rows mutex poisoned");
883
+ if let Some(Row::Vector(map)) = row_data.get(column) {
884
+ for (id, v) in map.iter() {
885
+ vec_snapshot.insert(*id, v.clone());
886
+ }
887
+ }
888
+ }
889
+ // The new row was just written into row storage — make sure the
890
+ // snapshot reflects it (it should, but defensive).
891
+ vec_snapshot.insert(rowid, new_vec.to_vec());
892
+
893
+ for entry in &mut self.hnsw_indexes {
894
+ if entry.column_name == column {
895
+ entry.index.insert(rowid, new_vec, |id| {
896
+ vec_snapshot.get(&id).cloned().unwrap_or_default()
897
+ });
898
+ }
899
+ }
900
+ }
901
+
826
902
  /// Print the table schema to standard output in a pretty formatted way.
827
903
  ///
828
904
  /// # Example