Skip to content

Commit a2a0272

Browse files
fix: harden embeddings sync/rewrite scripts after #979 review
Skip empty vectors instead of inserting [], make SQLite rewrite idempotent/rerun-safe, fail hard if local Postgres never becomes ready. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 01d3a4b commit a2a0272

5 files changed

Lines changed: 257 additions & 59 deletions

File tree

Makefile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,15 @@ docker-postgres:
5757
-p 5432:5432 \
5858
"$$wanted"; \
5959
fi; \
60-
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do \
61-
docker exec cre-postgres pg_isready -U cre -d cre >/dev/null 2>&1 && break; \
60+
ready=0; \
61+
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30; do \
62+
if docker exec cre-postgres pg_isready -U cre -d cre >/dev/null 2>&1; then ready=1; break; fi; \
6263
sleep 1; \
6364
done; \
65+
if [ "$$ready" != "1" ]; then \
66+
echo "error: cre-postgres did not become ready within 30s" >&2; \
67+
exit 1; \
68+
fi; \
6469
docker exec cre-postgres psql -U cre -d cre -c "CREATE EXTENSION IF NOT EXISTS vector;" >/dev/null
6570

6671
start-containers: docker-neo4j docker-redis
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""Unit tests for embeddings sync / SQLite rewrite helpers."""
2+
3+
from __future__ import annotations
4+
5+
import sqlite3
6+
import tempfile
7+
import unittest
8+
from pathlib import Path
9+
10+
from scripts.rewrite_sqlite_embeddings_to_vec import rewrite
11+
from scripts.sync_embeddings_table import _as_vec_literal, _fetch_sqlite_embeddings
12+
13+
14+
class AsVecLiteralTest(unittest.TestCase):
15+
def test_none_and_empty_return_none(self) -> None:
16+
self.assertIsNone(_as_vec_literal(None))
17+
self.assertIsNone(_as_vec_literal(""))
18+
self.assertIsNone(_as_vec_literal("[]"))
19+
self.assertIsNone(_as_vec_literal(" [ ] "))
20+
21+
def test_literal_and_csv(self) -> None:
22+
self.assertEqual(_as_vec_literal("[1.0,2.0]"), "[1.0,2.0]")
23+
self.assertEqual(_as_vec_literal("1.0,2.0"), "[1.0,2.0]")
24+
25+
26+
class SyncSqliteFetchTest(unittest.TestCase):
27+
def test_skips_empty_vectors(self) -> None:
28+
with tempfile.TemporaryDirectory() as td:
29+
path = Path(td) / "t.sqlite"
30+
conn = sqlite3.connect(path)
31+
conn.execute(
32+
"""
33+
CREATE TABLE embeddings (
34+
embedding_vec TEXT,
35+
doc_type TEXT,
36+
cre_id TEXT,
37+
node_id TEXT,
38+
embeddings_content TEXT,
39+
embeddings_url TEXT
40+
)
41+
"""
42+
)
43+
conn.executemany(
44+
"INSERT INTO embeddings VALUES (?,?,?,?,?,?)",
45+
[
46+
("[0.1,0.2]", "CRE", "a", None, "x", None),
47+
(None, "CRE", "b", None, "y", None),
48+
("[]", "CRE", "c", None, "z", None),
49+
],
50+
)
51+
conn.commit()
52+
conn.close()
53+
_cols, rows, skipped = _fetch_sqlite_embeddings(str(path))
54+
self.assertEqual(len(rows), 1)
55+
self.assertEqual(skipped, 2)
56+
self.assertEqual(rows[0][0], "[0.1,0.2]")
57+
58+
59+
class RewriteSqliteTest(unittest.TestCase):
60+
def test_idempotent_and_no_double_brackets(self) -> None:
61+
with tempfile.TemporaryDirectory() as td:
62+
path = Path(td) / "t.sqlite"
63+
conn = sqlite3.connect(path)
64+
conn.execute(
65+
"""
66+
CREATE TABLE embeddings (
67+
id TEXT,
68+
doc_type TEXT NOT NULL,
69+
cre_id TEXT,
70+
node_id TEXT,
71+
embeddings TEXT NOT NULL,
72+
embeddings_content TEXT,
73+
embeddings_url TEXT
74+
)
75+
"""
76+
)
77+
conn.execute(
78+
"INSERT INTO embeddings VALUES (?,?,?,?,?,?,?)",
79+
("1", "CRE", "a", None, "0.1,0.2", "t", None),
80+
)
81+
conn.execute(
82+
"INSERT INTO embeddings VALUES (?,?,?,?,?,?,?)",
83+
("2", "CRE", "b", None, "[0.3,0.4]", "t2", None),
84+
)
85+
conn.commit()
86+
conn.close()
87+
88+
self.assertEqual(rewrite(str(path)), 0)
89+
self.assertEqual(rewrite(str(path)), 0) # already vec-only
90+
91+
conn = sqlite3.connect(path)
92+
cols = {r[1] for r in conn.execute("PRAGMA table_info(embeddings)")}
93+
self.assertIn("embedding_vec", cols)
94+
self.assertNotIn("embeddings", cols)
95+
vecs = [
96+
r[0]
97+
for r in conn.execute(
98+
"SELECT embedding_vec FROM embeddings ORDER BY id"
99+
)
100+
]
101+
self.assertEqual(vecs[0], "[0.1,0.2]")
102+
self.assertEqual(vecs[1], "[0.3,0.4]") # not [[0.3,0.4]]
103+
conn.close()
104+
105+
def test_rerun_safe_after_leftover_embeddings_new(self) -> None:
106+
with tempfile.TemporaryDirectory() as td:
107+
path = Path(td) / "t.sqlite"
108+
conn = sqlite3.connect(path)
109+
conn.execute(
110+
"""
111+
CREATE TABLE embeddings (
112+
id TEXT,
113+
doc_type TEXT NOT NULL,
114+
cre_id TEXT,
115+
node_id TEXT,
116+
embeddings TEXT NOT NULL,
117+
embeddings_content TEXT,
118+
embeddings_url TEXT
119+
)
120+
"""
121+
)
122+
conn.execute(
123+
"INSERT INTO embeddings VALUES (?,?,?,?,?,?,?)",
124+
("1", "CRE", "a", None, "1,2", None, None),
125+
)
126+
# Simulate interrupted prior run.
127+
conn.execute(
128+
"CREATE TABLE embeddings_new (id TEXT, doc_type TEXT, embedding_vec TEXT)"
129+
)
130+
conn.commit()
131+
conn.close()
132+
self.assertEqual(rewrite(str(path)), 0)
133+
134+
135+
if __name__ == "__main__":
136+
unittest.main()

scripts/link_pci_dss_cre.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def main() -> int:
3838

3939
from application.cmd import cre_main
4040
from application.database import db
41+
from application.database.pgvector_utils import parse_stored_embedding_vec
4142
from application.defs import cre_defs as defs
4243
from application.prompt_client import prompt_client
4344

@@ -90,8 +91,6 @@ def main() -> int:
9091
skipped_no_match += 1
9192
continue
9293

93-
from application.database.pgvector_utils import parse_stored_embedding_vec
94-
9594
control_embeddings = parse_stored_embedding_vec(emb_row.embedding_vec)
9695
if not control_embeddings:
9796
logger.warning(

scripts/rewrite_sqlite_embeddings_to_vec.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
Old ``standards_cache.sqlite`` files still have a CSV ``embeddings`` column —
77
the ORM will refuse them. This script converts in place:
88
9-
* ADD ``embedding_vec`` TEXT
10-
* copy ``'[' || embeddings || ']'``
11-
* DROP ``embeddings``
9+
* ADD ``embedding_vec`` TEXT (if missing)
10+
* copy CSV → ``[…]`` literal without double-bracketing
11+
* DROP ``embeddings`` via table recreate (rerun-safe)
1212
1313
Usage::
1414
@@ -22,6 +22,13 @@
2222
import sys
2323

2424

25+
def _csv_to_literal(csv: str) -> str:
26+
s = (csv or "").strip()
27+
if s.startswith("[") and s.endswith("]"):
28+
return s
29+
return f"[{s}]"
30+
31+
2532
def rewrite(path: str) -> int:
2633
conn = sqlite3.connect(path)
2734
try:
@@ -32,19 +39,36 @@ def rewrite(path: str) -> int:
3239
if "embedding_vec" in cols and "embeddings" not in cols:
3340
print(f"{path}: already embedding_vec-only; nothing to do")
3441
return 0
42+
if "embeddings" not in cols:
43+
print(
44+
f"error: {path} has embedding_vec but no CSV embeddings column "
45+
"and is incomplete; refusing to rewrite",
46+
file=sys.stderr,
47+
)
48+
return 2
3549
if "embedding_vec" not in cols:
3650
conn.execute("ALTER TABLE embeddings ADD COLUMN embedding_vec TEXT")
37-
conn.execute(
51+
52+
# Copy only rows still missing a vector; never wrap an already-bracketed
53+
# CSV twice (some caches may already store ``[1,2]`` in the CSV column).
54+
rows = conn.execute(
3855
"""
39-
UPDATE embeddings
40-
SET embedding_vec = '[' || embeddings || ']'
56+
SELECT rowid, embeddings, embedding_vec
57+
FROM embeddings
4158
WHERE embeddings IS NOT NULL
4259
AND trim(embeddings) <> ''
4360
AND (embedding_vec IS NULL OR trim(embedding_vec) = '')
4461
"""
45-
)
62+
).fetchall()
63+
for rowid, csv, _existing in rows:
64+
conn.execute(
65+
"UPDATE embeddings SET embedding_vec = ? WHERE rowid = ?",
66+
(_csv_to_literal(str(csv)), rowid),
67+
)
68+
4669
nulls = conn.execute(
47-
"SELECT COUNT(*) FROM embeddings WHERE embedding_vec IS NULL OR trim(embedding_vec) = ''"
70+
"SELECT COUNT(*) FROM embeddings "
71+
"WHERE embedding_vec IS NULL OR trim(embedding_vec) = ''"
4872
).fetchone()[0]
4973
if nulls:
5074
print(
@@ -55,7 +79,10 @@ def rewrite(path: str) -> int:
5579
conn.rollback()
5680
return 3
5781
conn.commit()
82+
5883
# SQLite lacks reliable DROP COLUMN on older builds; recreate table.
84+
# Drop any leftover embeddings_new from a prior interrupted run.
85+
conn.execute("DROP TABLE IF EXISTS embeddings_new")
5986
src_cols = {
6087
r[1] for r in conn.execute("PRAGMA table_info(embeddings)").fetchall()
6188
}

0 commit comments

Comments
 (0)