sqlrite 0.1.13__tar.gz → 0.1.14__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 (109) hide show
  1. {sqlrite-0.1.13 → sqlrite-0.1.14}/Cargo.lock +5 -5
  2. {sqlrite-0.1.13 → sqlrite-0.1.14}/Cargo.toml +1 -1
  3. {sqlrite-0.1.13 → sqlrite-0.1.14}/PKG-INFO +1 -1
  4. {sqlrite-0.1.13 → sqlrite-0.1.14}/desktop/package.json +1 -1
  5. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/phase-7-plan.md +3 -3
  6. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/roadmap.md +1 -1
  7. {sqlrite-0.1.13 → sqlrite-0.1.14}/pyproject.toml +1 -1
  8. {sqlrite-0.1.13 → sqlrite-0.1.14}/sdk/python/Cargo.toml +1 -1
  9. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/db/table.rs +72 -1
  10. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/executor.rs +370 -29
  11. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/mod.rs +158 -0
  12. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/pager/mod.rs +143 -2
  13. {sqlrite-0.1.13 → sqlrite-0.1.14}/.github/workflows/ci.yml +0 -0
  14. {sqlrite-0.1.13 → sqlrite-0.1.14}/.github/workflows/release-pr.yml +0 -0
  15. {sqlrite-0.1.13 → sqlrite-0.1.14}/.github/workflows/release.yml +0 -0
  16. {sqlrite-0.1.13 → sqlrite-0.1.14}/.github/workflows/rust.yml +0 -0
  17. {sqlrite-0.1.13 → sqlrite-0.1.14}/.gitignore +0 -0
  18. {sqlrite-0.1.13 → sqlrite-0.1.14}/CODE_OF_CONDUCT.md +0 -0
  19. {sqlrite-0.1.13 → sqlrite-0.1.14}/LICENSE +0 -0
  20. {sqlrite-0.1.13 → sqlrite-0.1.14}/MAINTAINERS +0 -0
  21. {sqlrite-0.1.13 → sqlrite-0.1.14}/Makefile +0 -0
  22. {sqlrite-0.1.13 → sqlrite-0.1.14}/README.md +0 -0
  23. {sqlrite-0.1.13 → sqlrite-0.1.14}/desktop/index.html +0 -0
  24. {sqlrite-0.1.13 → sqlrite-0.1.14}/desktop/package-lock.json +0 -0
  25. {sqlrite-0.1.13 → sqlrite-0.1.14}/desktop/src/App.svelte +0 -0
  26. {sqlrite-0.1.13 → sqlrite-0.1.14}/desktop/src/app.css +0 -0
  27. {sqlrite-0.1.13 → sqlrite-0.1.14}/desktop/src/main.ts +0 -0
  28. {sqlrite-0.1.13 → sqlrite-0.1.14}/desktop/src/vite-env.d.ts +0 -0
  29. {sqlrite-0.1.13 → sqlrite-0.1.14}/desktop/svelte.config.js +0 -0
  30. {sqlrite-0.1.13 → sqlrite-0.1.14}/desktop/tsconfig.json +0 -0
  31. {sqlrite-0.1.13 → sqlrite-0.1.14}/desktop/vite.config.ts +0 -0
  32. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/_index.md +0 -0
  33. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/architecture.md +0 -0
  34. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/design-decisions.md +0 -0
  35. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/desktop.md +0 -0
  36. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/embedding.md +0 -0
  37. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/file-format.md +0 -0
  38. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/getting-started.md +0 -0
  39. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/pager.md +0 -0
  40. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/release-plan.md +0 -0
  41. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/release-secrets.md +0 -0
  42. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/smoke-test.md +0 -0
  43. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/sql-engine.md +0 -0
  44. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/storage-model.md +0 -0
  45. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/supported-sql.md +0 -0
  46. {sqlrite-0.1.13 → sqlrite-0.1.14}/docs/usage.md +0 -0
  47. {sqlrite-0.1.13 → sqlrite-0.1.14}/examples/README.md +0 -0
  48. {sqlrite-0.1.13 → sqlrite-0.1.14}/examples/c/Makefile +0 -0
  49. {sqlrite-0.1.13 → sqlrite-0.1.14}/examples/c/hello.c +0 -0
  50. {sqlrite-0.1.13 → sqlrite-0.1.14}/examples/go/go.mod +0 -0
  51. {sqlrite-0.1.13 → sqlrite-0.1.14}/examples/go/hello.go +0 -0
  52. {sqlrite-0.1.13 → sqlrite-0.1.14}/examples/nodejs/hello.mjs +0 -0
  53. {sqlrite-0.1.13 → sqlrite-0.1.14}/examples/python/hello.py +0 -0
  54. {sqlrite-0.1.13 → sqlrite-0.1.14}/examples/rust/quickstart.rs +0 -0
  55. {sqlrite-0.1.13 → sqlrite-0.1.14}/examples/wasm/Makefile +0 -0
  56. {sqlrite-0.1.13 → sqlrite-0.1.14}/examples/wasm/index.html +0 -0
  57. {sqlrite-0.1.13 → sqlrite-0.1.14}/images/SQLRite - Desktop.png +0 -0
  58. {sqlrite-0.1.13 → sqlrite-0.1.14}/images/SQLRite Data Structures.png +0 -0
  59. {sqlrite-0.1.13 → sqlrite-0.1.14}/images/SQLRite Simple SQL Execution High Level Diagram.png +0 -0
  60. {sqlrite-0.1.13 → sqlrite-0.1.14}/images/SQLRite Simple SQL INSERT Execution High Level Diagram (Insert Row).png +0 -0
  61. {sqlrite-0.1.13 → sqlrite-0.1.14}/images/SQLRite Simple SQL INSERT Execution High Level Diagram.png +0 -0
  62. {sqlrite-0.1.13 → sqlrite-0.1.14}/images/SQLRite_logo.png +0 -0
  63. {sqlrite-0.1.13 → sqlrite-0.1.14}/images/architecture.png +0 -0
  64. {sqlrite-0.1.13 → sqlrite-0.1.14}/rust-toolchain.toml +0 -0
  65. {sqlrite-0.1.13 → sqlrite-0.1.14}/samples/AST.delete.example +0 -0
  66. {sqlrite-0.1.13 → sqlrite-0.1.14}/samples/AST.insert.exemple +0 -0
  67. {sqlrite-0.1.13 → sqlrite-0.1.14}/samples/AST.select.example +0 -0
  68. {sqlrite-0.1.13 → sqlrite-0.1.14}/samples/AST.update.example +0 -0
  69. {sqlrite-0.1.13 → sqlrite-0.1.14}/samples/CREATE TABLE sqlrite_schema.sql +0 -0
  70. {sqlrite-0.1.13 → sqlrite-0.1.14}/samples/CREATE_TABLE with duplicate.sql +0 -0
  71. {sqlrite-0.1.13 → sqlrite-0.1.14}/samples/CREATE_TABLE.sql +0 -0
  72. {sqlrite-0.1.13 → sqlrite-0.1.14}/samples/INSERT.sql +0 -0
  73. {sqlrite-0.1.13 → sqlrite-0.1.14}/scripts/bump-version.sh +0 -0
  74. {sqlrite-0.1.13 → sqlrite-0.1.14}/sdk/go/README.md +0 -0
  75. {sqlrite-0.1.13 → sqlrite-0.1.14}/sdk/go/conn.go +0 -0
  76. {sqlrite-0.1.13 → sqlrite-0.1.14}/sdk/go/go.mod +0 -0
  77. {sqlrite-0.1.13 → sqlrite-0.1.14}/sdk/go/rows.go +0 -0
  78. {sqlrite-0.1.13 → sqlrite-0.1.14}/sdk/go/sqlrite.go +0 -0
  79. {sqlrite-0.1.13 → sqlrite-0.1.14}/sdk/go/sqlrite_test.go +0 -0
  80. {sqlrite-0.1.13 → sqlrite-0.1.14}/sdk/go/stmt.go +0 -0
  81. {sqlrite-0.1.13 → sqlrite-0.1.14}/sdk/python/README.md +0 -0
  82. {sqlrite-0.1.13 → sqlrite-0.1.14}/sdk/python/src/lib.rs +0 -0
  83. {sqlrite-0.1.13 → sqlrite-0.1.14}/sdk/python/tests/test_sqlrite.py +0 -0
  84. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/connection.rs +0 -0
  85. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/error.rs +0 -0
  86. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/lib.rs +0 -0
  87. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/main.rs +0 -0
  88. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/meta_command/mod.rs +0 -0
  89. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/repl/mod.rs +0 -0
  90. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/db/database.rs +0 -0
  91. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/db/mod.rs +0 -0
  92. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/db/secondary_index.rs +0 -0
  93. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/hnsw.rs +0 -0
  94. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/pager/cell.rs +0 -0
  95. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/pager/file.rs +0 -0
  96. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/pager/header.rs +0 -0
  97. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/pager/index_cell.rs +0 -0
  98. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/pager/interior_page.rs +0 -0
  99. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/pager/overflow.rs +0 -0
  100. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/pager/page.rs +0 -0
  101. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/pager/pager.rs +0 -0
  102. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/pager/table_page.rs +0 -0
  103. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/pager/varint.rs +0 -0
  104. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/pager/wal.rs +0 -0
  105. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/parser/create.rs +0 -0
  106. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/parser/insert.rs +0 -0
  107. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/parser/mod.rs +0 -0
  108. {sqlrite-0.1.13 → sqlrite-0.1.14}/src/sql/parser/select.rs +0 -0
  109. {sqlrite-0.1.13 → sqlrite-0.1.14}/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.14"
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.14"
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.14"
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.14"
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.14"
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.14"
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.14
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.14",
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** *(~300 LOC).* Wire HNSW into the cell format: new `KIND_HNSW` cell tag, page-tree storage parallel to secondary indexes, save/reopen round-trip without rebuild. Also adds DELETE/UPDATE support since the persisted form gives us a natural rebuild trigger.
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** — split into 7d.1 (✅ algorithm), 7d.2 (✅ SQL integration), 7d.3 (persistence). `CREATE INDEX … USING hnsw (col)`; fixed defaults `M=16, ef_construction=200, ef_search=50` (Q2).
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.14"
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.14"
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,32 @@ 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
+ }
147
+
127
148
  impl Table {
128
149
  pub fn new(create_query: CreateQuery) -> Self {
129
150
  let table_name = create_query.table_name;
@@ -194,6 +215,11 @@ impl Table {
194
215
  columns: table_cols,
195
216
  rows: table_rows,
196
217
  secondary_indexes,
218
+ // HNSW indexes only land via explicit CREATE INDEX … USING hnsw
219
+ // statements (Phase 7d.2); never auto-created at CREATE TABLE
220
+ // time, because there's no UNIQUE-style constraint that
221
+ // implies a vector index.
222
+ hnsw_indexes: Vec::new(),
197
223
  last_rowid: 0,
198
224
  primary_key,
199
225
  }
@@ -217,6 +243,10 @@ impl Table {
217
243
  columns: self.columns.clone(),
218
244
  rows: Arc::new(Mutex::new(cloned_rows)),
219
245
  secondary_indexes: self.secondary_indexes.clone(),
246
+ // HnswIndexEntry derives Clone, so the snapshot owns its own
247
+ // graph copy. Phase 4f's snapshot-rollback semantics require
248
+ // the snapshot to be fully decoupled from live state.
249
+ hnsw_indexes: self.hnsw_indexes.clone(),
220
250
  last_rowid: self.last_rowid,
221
251
  primary_key: self.primary_key.clone(),
222
252
  }
@@ -813,16 +843,57 @@ impl Table {
813
843
 
814
844
  // Step 2: maintain the secondary index (if any). insert() is a
815
845
  // no-op for Value::Null and cheap for other value kinds.
816
- if let Some(v) = typed_value {
846
+ if let Some(v) = typed_value.clone() {
817
847
  if let Some(idx) = self.index_for_column_mut(key) {
818
848
  idx.insert(&v, next_rowid)?;
819
849
  }
820
850
  }
851
+
852
+ // Step 3 (Phase 7d.2): maintain any HNSW indexes on this column.
853
+ // The HNSW algorithm needs access to other rows' vectors when
854
+ // wiring up neighbor edges, so build a get_vec closure that
855
+ // pulls from the table's row storage (which we *just* updated
856
+ // with the new value).
857
+ if let Some(Value::Vector(new_vec)) = typed_value {
858
+ self.maintain_hnsw_on_insert(key, next_rowid, &new_vec);
859
+ }
821
860
  }
822
861
  self.last_rowid = next_rowid;
823
862
  Ok(())
824
863
  }
825
864
 
865
+ /// After a row insert, push the new (rowid, vector) into every HNSW
866
+ /// index whose column matches `column`. Split out of `insert_row` so
867
+ /// the borrowing dance — we need both `&self.rows` (read other
868
+ /// vectors) and `&mut self.hnsw_indexes` (insert into the graph) —
869
+ /// stays localized.
870
+ fn maintain_hnsw_on_insert(&mut self, column: &str, rowid: i64, new_vec: &[f32]) {
871
+ // Snapshot the current vector storage so the get_vec closure
872
+ // doesn't fight with `&mut self.hnsw_indexes`. For a typical
873
+ // HNSW insert we touch ef_construction × log(N) other vectors,
874
+ // so the snapshot cost is small relative to the graph wiring.
875
+ let mut vec_snapshot: HashMap<i64, Vec<f32>> = HashMap::new();
876
+ {
877
+ let row_data = self.rows.lock().expect("rows mutex poisoned");
878
+ if let Some(Row::Vector(map)) = row_data.get(column) {
879
+ for (id, v) in map.iter() {
880
+ vec_snapshot.insert(*id, v.clone());
881
+ }
882
+ }
883
+ }
884
+ // The new row was just written into row storage — make sure the
885
+ // snapshot reflects it (it should, but defensive).
886
+ vec_snapshot.insert(rowid, new_vec.to_vec());
887
+
888
+ for entry in &mut self.hnsw_indexes {
889
+ if entry.column_name == column {
890
+ entry.index.insert(rowid, new_vec, |id| {
891
+ vec_snapshot.get(&id).cloned().unwrap_or_default()
892
+ });
893
+ }
894
+ }
895
+ }
896
+
826
897
  /// Print the table schema to standard output in a pretty formatted way.
827
898
  ///
828
899
  /// # Example