diff --git a/packages/kb-mail/tests/test_migration.py b/packages/kb-mail/tests/test_migration.py index e8c8961..f58a5d3 100644 --- a/packages/kb-mail/tests/test_migration.py +++ b/packages/kb-mail/tests/test_migration.py @@ -46,3 +46,44 @@ def test_migration_001_entities_has_default(): lines = [l for l in sql.splitlines() if "entities" in l.lower()] assert any("DEFAULT" in l.upper() for l in lines), \ "entities column must have a DEFAULT '[]'" + + +def test_migration_002_exists(): + assert (INIT_DIR / "002_chunks.sql").exists(), \ + f"Expected {INIT_DIR / '002_chunks.sql'} to exist" + + +def test_migration_002_does_not_touch_envelope(): + sql = (INIT_DIR / "002_chunks.sql").read_text() + assert "ALTER TABLE envelope" not in sql + assert "DROP TABLE envelope" not in sql.upper() + + +def test_migration_002_creates_document_chunk_table(): + sql = (INIT_DIR / "002_chunks.sql").read_text() + assert "CREATE TABLE IF NOT EXISTS document_chunk" in sql + + +def test_migration_002_references_envelope_with_cascade(): + sql = (INIT_DIR / "002_chunks.sql").read_text() + assert "REFERENCES envelope(id) ON DELETE CASCADE" in sql + + +def test_migration_002_embedding_dimension_matches_bge_m3(): + sql = (INIT_DIR / "002_chunks.sql").read_text() + # bge-m3 dense embedding dimension is 1024 (docs/kb/modules/05-faza2-plan.md ยง1.4) + assert "VECTOR(1024)" in sql.upper() + + +def test_migration_002_has_unique_envelope_chunk_index(): + sql = (INIT_DIR / "002_chunks.sql").read_text() + assert "UNIQUE (envelope_id, chunk_index)" in sql + + +def test_migration_002_uses_idempotent_ddl(): + sql = (INIT_DIR / "002_chunks.sql").read_text() + assert "CREATE TABLE IF NOT EXISTS" in sql + for line in sql.splitlines(): + if line.strip().upper().startswith("CREATE INDEX"): + assert "IF NOT EXISTS" in line.upper(), \ + f"CREATE INDEX must be idempotent: {line.strip()}" diff --git a/services/kb-postgres/init/002_chunks.sql b/services/kb-postgres/init/002_chunks.sql new file mode 100644 index 0000000..f0296ec --- /dev/null +++ b/services/kb-postgres/init/002_chunks.sql @@ -0,0 +1,18 @@ +-- KB spine: document_chunk โ€” chunk-level embeddings, 1:N against envelope +-- Additive: does not modify 001_envelope.sql. envelope stays untouched. +-- Version: 002 โ€” chunks (bge-m3 dense embeddings, dim=1024) + +CREATE TABLE IF NOT EXISTS document_chunk ( + id BIGSERIAL PRIMARY KEY, + envelope_id TEXT NOT NULL REFERENCES envelope(id) ON DELETE CASCADE, + chunk_index INT NOT NULL, -- order within envelope, 0-based + text TEXT NOT NULL, + embedding VECTOR(1024), -- bge-m3 dense; NULL until embedded + model TEXT NOT NULL, -- e.g. 'bge-m3' โ€” tracks re-index on model change + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (envelope_id, chunk_index) +); + +CREATE INDEX IF NOT EXISTS document_chunk_envelope_idx ON document_chunk (envelope_id); +CREATE INDEX IF NOT EXISTS document_chunk_embedding_hnsw_idx + ON document_chunk USING hnsw (embedding vector_cosine_ops);