Repository navigation
contextdb: add table declarations and write rules that enforce keys, uniqueness, references and defaults - #23
Conversation
…uniqueness, references and defaults
|
Hosted run: https://github.com/context-graph-ai/contextdb/actions/runs/43248584430 failed. lint and minimum-rust pass. Two tests fail, one on each runner: on macOS, |
|
Hosted run on |
Adds the data rules of the new engine under
v2/: what a table may declare and what every write must obey. Nothing that builds fromcrates/changes; the rootCargo.tomlandCargo.lockare untouched. One commit ondev. The query language (joins, grouping, graph patterns, ranked search, unions, subqueries) is the next pull request; the store's on-disk layout is its own pull request after it.What is in it
UNIQUErules on one column or several;REFERENCESandFOREIGN KEYonto a primary key or aUNIQUErule, single or composite;IMMUTABLEon a table or a column;STATE MACHINE,DAG,PROPAGATEalong references and along edges,PROPAGATE ... EXCLUDE VECTOR;MAINTAIN COUNT;CONTEXT_ID,SCOPE_LABELandACL REFERENCES; and a table with no primary key at all, whose rows are kept under a hidden key. A declaration prints back as it was written: Context Graph's 118 table declarations are all accepted and 93 of the 93 that were written out come back unchanged.NOT NULL, the key, everyUNIQUErule, references, the state machine, immutability and the graph's no-cycle rule are checked before anything changes; a statement that breaks one is refused and changes nothing. References and the cycle rule are judged when the transaction commits. A key is judged when the statement ends, so anUPDATEthat swaps two keys or shifts a run of them succeeds.unique constraint violation: users (id) = (1);foreign key violation: orders (user_id) = (99) has no row in users;foreign key violation: users is still referenced by orders (user_id) = (1);invalid state transition of tasks.status: new -> shipped. A long text is cut at 64 bytes at a whole character and followed by its length. The value shown is always the one the writer gave, so a narrowed handle learns nothing about a row it cannot see.ALTER TABLEadds a column (earlier rows read itsDEFAULT), drops one (RESTRICT, orCASCADEwhich reports the indexes it dropped), renames one, replacesRETAIN,HISTORY,SYNC,SYNC CONFLICTandMAINTAIN COUNT, and changes a vector column withSET MAX_PARTITIONS,SET SEARCH_MODE,SET AUTO_INDEX_ATandSET CONSOLIDATION(each withDEFAULTto put the default back). A column the table is declared by (its primary key, anIMMUTABLEcolumn, a state machine's column, aMAINTAIN COUNTcolumn, a vector partition key, the edge columns of aDAG, a column a rank policy reads) is neither dropped nor renamed, and the refusal names the rule.DROP TABLErefuses a table another table references;IF EXISTSmakes a missing table or index a no-op.INSERT ... ON CONFLICT (columns) DO UPDATE,UPDATE,DELETEandSELECTover one table, withINlists,COUNT,SUM,MIN,MAX,ORDER BYandLIMIT. AnINSERTinto a table with an index on a JSON path is accepted; the index is stored and printed and the query language fills and reads it.warning: there is already a transaction in progress/there is no transaction in progress) whenBEGINcomes inside a transaction orCOMMITorROLLBACKoutside one.set_memory_limiton a narrowed handle is refused withAdminRequired, as the statement is..github/workflows/check.ymlstates 21,000 shipped lines (it stated 15,500), 380 tests (it stated 260) and, new in this change, 38,000 test lines with a step that fails when they pass it.What is refused, and what to write instead
WITH (quantization = 'SQ4')not part of this library: write 'SQ8' to store one byte a component, or leave the clause out to store each vector as givenIVF (...)orSEARCH (...), in a declaration or afterALTER COLUMN ... SETnot part of this library: the index sizes its own groups and measures how far a search scans; for the true nearest rows write USE VECTOR EXACT or SEARCH_MODE EXACT, and to move the size from which AUTO uses the index write AUTO_INDEX_ATEXPIRESEXPIRES is not supported in this version of the library(nothing expires yet;RETAINis accepted and printed back)CREATE UNIQUE INDEXUNIQUEon the column or in the tableADD COLUMN ... NOT NULLwith noDEFAULTon a table that has rowsDEFAULTINSERT,SETorDO UPDATE SETplan error: ... is named twice as a column ... is written inNOT NULLcolumn to NULL, onUPDATEorDO UPDATEColumnNotNullableUNIQUErule or an index naming more than 32 columnsSchemaInvalid: name fewer__pk_,__unique_,__fk_or__graph_ReservedIndexName: choose another nameDROP COLUMNof a column an index namesCASCADEINTEGERcolumn referencing aUUIDkeyJSONcolumnDROP COLUMNof a state machine's columnSchemaInvalidnaming theSTATE MACHINEJOIN,GROUP BY,HAVING,DISTINCT,LIKE,BETWEEN,OFFSET,UNION,WITH, subqueries, graph patterns andUSE RANKare still refused by name,... is not supported in this version of the library; they come with the query language.What changed on purpose from the old engine
Seventeen rows of the Postgres-differences list (80 to 96) come from running 978 statements of the old engine's SQL scripts against both engines; three more (77 to 79) are the refused vector clauses above. Among them:
UPDATEthat swaps two primary keys or shifts a run of them succeeds (row 96). The old engine refuses it as a unique violation. Postgres decides by the order it visits rows unless the key isDEFERRABLE. Here a key is judged when the statement ends, as the SQL standard says; two rows left under one key are still refused.ADD COLUMN ... DEFAULTfills earlier rows with the default (the old engine left them NULL);ADD COLUMN ... NOT NULLwith no default on a table with rows is refused (the old engine wrote NULL into aNOT NULLcolumn).DROP TABLEof a table that is not there answerstable_not_found(the old engine answered "corrupt store"); a column named twice,NOT NULLset to NULL, anINTEGERcolumn referencing aUUIDkey are refused (the old engine accepted them).RENAME COLUMNof aUNIQUEcolumn a reference names renames it and keeps the reference.UNIQUEcolumn are two rows inON CONFLICT(the old engine updated the first).PROPAGATEruns for every key type; the old engine ran it only for rows keyed by a UUID, so withINTEGERkeys a declared rule silently never ran.JSONcolumn is parsed and kept as a value, and--jsonprints it as an object, not a string. A consumer that read the old string sees the change.Measured at this commit
By
v2/numbers.shon this code: a release build on one machine with nothing else running; one query at a time on one thread, a read session at 1 GiB, 95th percentile of 100 questions. "devbefore" is the same table taken ondevwithout this change. A figure is marked worse when it is worse thandevbefore by more than the spread the file prints for it, or by any amount where no spread is printed.devbeforeSELECT SUM)cargo testThe scan is 18.4 ms slower than
devbefore. Two costs of this change were removed (13 ms and 8 to 10 ms of it); about 20 ms are left, mostly the hand-off of each row in code shared with the query path, and the read and decode of the store alone is the same 148 ms asdevbefore. Fifteen keys of the numbers script are marked worse, the largest being the sightings indexed find (30.35 to 33.73 ms) and the fresh-clone build.Store sizes
Context Graph's declared schema, loaded the same way into both engines, 500 rows to a commit (the old engine at its current commit on the same machine).
Both stores are above twice what was put in, and 4.5 and 7.6 times smaller than the old engine's for the same rows. The cause is measured and belongs to the store's layout, not to the data rules: the storage library leaves half-empty index pages when an index is written in runs, 62.1 MiB of the passages store. That is the layout pull request. The old engine's figure for the anchors was measured beside an earlier run of this engine (672.4 MiB); this run's 560.6 MiB follows a later cap on how much of a long text an index entry keeps.
Old engine against this one, statement by statement
978 statements of 20 SQL scripts, run against both engines: 778 answer the same, 18 are the new form (a vector search written the Postgres way), 44 differ on purpose and are written down, 88 follow from those, 28 differ only in wording, 22 are old-engine defects this change corrects, 0 are defects of this change, and 0 are unread.
Verification
check.ymlas written, on the commit: format, lint with warnings as errors, 339 of 339 tests, the documentation tests, the storage library's own 202.c955c0253: the run is linked in a comment below when it finishes.Not in this change
LIKE, the typed equality on a JSON field,USE RANK, theCREATE INDEXline of.explain, a statement that cannot overflow the stack and aSUMover a filter that passes only some rows are the next pull request.RETAINis accepted and printed back; no row expires yet.Known limits of this version
devbefore.NOT NULLcolumn names the column, not the row.