cjm-capability-graph-sqlite 0.0.28__tar.gz → 0.0.29__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.
- {cjm_capability_graph_sqlite-0.0.28/cjm_capability_graph_sqlite.egg-info → cjm_capability_graph_sqlite-0.0.29}/PKG-INFO +5 -1
- {cjm_capability_graph_sqlite-0.0.28 → cjm_capability_graph_sqlite-0.0.29}/README.md +4 -0
- cjm_capability_graph_sqlite-0.0.29/cjm_capability_graph_sqlite/__init__.py +1 -0
- {cjm_capability_graph_sqlite-0.0.28 → cjm_capability_graph_sqlite-0.0.29}/cjm_capability_graph_sqlite/capability.py +80 -13
- {cjm_capability_graph_sqlite-0.0.28 → cjm_capability_graph_sqlite-0.0.29}/cjm_capability_graph_sqlite/query_translation.py +25 -3
- {cjm_capability_graph_sqlite-0.0.28 → cjm_capability_graph_sqlite-0.0.29/cjm_capability_graph_sqlite.egg-info}/PKG-INFO +5 -1
- {cjm_capability_graph_sqlite-0.0.28 → cjm_capability_graph_sqlite-0.0.29}/tests/test_capability.py +64 -0
- {cjm_capability_graph_sqlite-0.0.28 → cjm_capability_graph_sqlite-0.0.29}/tests/test_query_translation.py +21 -2
- cjm_capability_graph_sqlite-0.0.28/cjm_capability_graph_sqlite/__init__.py +0 -1
- {cjm_capability_graph_sqlite-0.0.28 → cjm_capability_graph_sqlite-0.0.29}/LICENSE +0 -0
- {cjm_capability_graph_sqlite-0.0.28 → cjm_capability_graph_sqlite-0.0.29}/MANIFEST.in +0 -0
- {cjm_capability_graph_sqlite-0.0.28 → cjm_capability_graph_sqlite-0.0.29}/cjm_capability_graph_sqlite.egg-info/SOURCES.txt +0 -0
- {cjm_capability_graph_sqlite-0.0.28 → cjm_capability_graph_sqlite-0.0.29}/cjm_capability_graph_sqlite.egg-info/dependency_links.txt +0 -0
- {cjm_capability_graph_sqlite-0.0.28 → cjm_capability_graph_sqlite-0.0.29}/cjm_capability_graph_sqlite.egg-info/requires.txt +0 -0
- {cjm_capability_graph_sqlite-0.0.28 → cjm_capability_graph_sqlite-0.0.29}/cjm_capability_graph_sqlite.egg-info/top_level.txt +0 -0
- {cjm_capability_graph_sqlite-0.0.28 → cjm_capability_graph_sqlite-0.0.29}/pyproject.toml +0 -0
- {cjm_capability_graph_sqlite-0.0.28 → cjm_capability_graph_sqlite-0.0.29}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cjm-capability-graph-sqlite
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.29
|
|
4
4
|
Summary: A local, file-backed context-graph storage capability for the cjm-substrate runtime that implements graph storage, traversal, and querying using SQLite.
|
|
5
5
|
Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -26,6 +26,7 @@ A local, file-backed context-graph storage capability for the cjm-substrate runt
|
|
|
26
26
|
|
|
27
27
|
## Modules
|
|
28
28
|
|
|
29
|
+
- **`cjm_capability_graph_sqlite.__init__`**
|
|
29
30
|
- **`cjm_capability_graph_sqlite.capability`** — Capability implementation for Context Graph using SQLite.
|
|
30
31
|
- **`cjm_capability_graph_sqlite.query_translation`** — The per-backend translation of the typed query expressions (pass-2 Thread 5; stage 4): NodeQuery/EdgeQuery -> parameterized SQLite SQL over the nodes/edges schema. THIS module is what makes the typed surface portable - the expression is domain- and backend-neutral; every backend tool owns a translation like this one (the ratified stage-4 split: backend owns translation; the adapter stays generic). Pure functions, unit-tested against an in-memory DB with the production schema - no capability runtime needed.
|
|
31
32
|
|
|
@@ -35,13 +36,16 @@ A local, file-backed context-graph storage capability for the cjm-substrate runt
|
|
|
35
36
|
|
|
36
37
|
- `SQLiteGraphCapability` _class_ — Local, file-backed Context Graph implementation using SQLite.
|
|
37
38
|
- `SQLiteGraphCapabilityConfig` _class_ — Configuration for SQLite Graph Capability.
|
|
39
|
+
- `ingest_sources` _function_ — The source HEADs this db was ingested from, on a fresh read-only connection. A db built
|
|
38
40
|
- `integrity_check` _function_ — Backend self-check (stage 4; the G3 corruption find institutionalized):
|
|
39
41
|
- `query_edges` _function_ — Execute a typed edge query (stage 4) — same contract as `query_nodes`.
|
|
40
42
|
- `query_nodes` _function_ — Execute a typed node query (stage 4) — translated to parameterized SQL
|
|
41
43
|
- `raw_query` _function_ — Execute the raw escape — refuses backend mismatches (non-portable by
|
|
44
|
+
- `record_ingest_sources` _function_ — Record the source HEADs this db was ingested from, REPLACING any earlier record
|
|
42
45
|
|
|
43
46
|
### `cjm_capability_graph_sqlite.query_translation`
|
|
44
47
|
|
|
48
|
+
- `decode_projected_row` _function_ — Zip a projected row, decoding every property column from its JSON representation.
|
|
45
49
|
- `translate_edge_query` _function_ — Translate an `EdgeQuery` to parameterized SQLite SQL.
|
|
46
50
|
- `translate_node_query` _function_ — Translate a `NodeQuery` to parameterized SQLite SQL.
|
|
47
51
|
|
|
@@ -6,6 +6,7 @@ A local, file-backed context-graph storage capability for the cjm-substrate runt
|
|
|
6
6
|
|
|
7
7
|
## Modules
|
|
8
8
|
|
|
9
|
+
- **`cjm_capability_graph_sqlite.__init__`**
|
|
9
10
|
- **`cjm_capability_graph_sqlite.capability`** — Capability implementation for Context Graph using SQLite.
|
|
10
11
|
- **`cjm_capability_graph_sqlite.query_translation`** — The per-backend translation of the typed query expressions (pass-2 Thread 5; stage 4): NodeQuery/EdgeQuery -> parameterized SQLite SQL over the nodes/edges schema. THIS module is what makes the typed surface portable - the expression is domain- and backend-neutral; every backend tool owns a translation like this one (the ratified stage-4 split: backend owns translation; the adapter stays generic). Pure functions, unit-tested against an in-memory DB with the production schema - no capability runtime needed.
|
|
11
12
|
|
|
@@ -15,13 +16,16 @@ A local, file-backed context-graph storage capability for the cjm-substrate runt
|
|
|
15
16
|
|
|
16
17
|
- `SQLiteGraphCapability` _class_ — Local, file-backed Context Graph implementation using SQLite.
|
|
17
18
|
- `SQLiteGraphCapabilityConfig` _class_ — Configuration for SQLite Graph Capability.
|
|
19
|
+
- `ingest_sources` _function_ — The source HEADs this db was ingested from, on a fresh read-only connection. A db built
|
|
18
20
|
- `integrity_check` _function_ — Backend self-check (stage 4; the G3 corruption find institutionalized):
|
|
19
21
|
- `query_edges` _function_ — Execute a typed edge query (stage 4) — same contract as `query_nodes`.
|
|
20
22
|
- `query_nodes` _function_ — Execute a typed node query (stage 4) — translated to parameterized SQL
|
|
21
23
|
- `raw_query` _function_ — Execute the raw escape — refuses backend mismatches (non-portable by
|
|
24
|
+
- `record_ingest_sources` _function_ — Record the source HEADs this db was ingested from, REPLACING any earlier record
|
|
22
25
|
|
|
23
26
|
### `cjm_capability_graph_sqlite.query_translation`
|
|
24
27
|
|
|
28
|
+
- `decode_projected_row` _function_ — Zip a projected row, decoding every property column from its JSON representation.
|
|
25
29
|
- `translate_edge_query` _function_ — Translate an `EdgeQuery` to parameterized SQLite SQL.
|
|
26
30
|
- `translate_node_query` _function_ — Translate a `NodeQuery` to parameterized SQLite SQL.
|
|
27
31
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.29"
|
|
@@ -22,7 +22,11 @@ import time
|
|
|
22
22
|
from dataclasses import dataclass, field
|
|
23
23
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
24
24
|
|
|
25
|
-
from cjm_capability_graph_sqlite.query_translation import
|
|
25
|
+
from cjm_capability_graph_sqlite.query_translation import (decode_projected_row,
|
|
26
|
+
EDGE_STRUCTURAL_KEYS,
|
|
27
|
+
NODE_STRUCTURAL_KEYS,
|
|
28
|
+
translate_edge_query,
|
|
29
|
+
translate_node_query)
|
|
26
30
|
from cjm_context_graph_primitives.graph import GraphContext, GraphEdge, GraphNode
|
|
27
31
|
from cjm_context_graph_primitives.provenance import SourceRef
|
|
28
32
|
from cjm_context_graph_primitives.query import (EdgeQuery, EdgeQueryResult, NodeQuery,
|
|
@@ -78,6 +82,11 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
78
82
|
FOREIGN KEY(source_id) REFERENCES nodes(id) ON DELETE CASCADE,
|
|
79
83
|
FOREIGN KEY(target_id) REFERENCES nodes(id) ON DELETE CASCADE
|
|
80
84
|
);
|
|
85
|
+
|
|
86
|
+
CREATE TABLE ingest_sources ( -- the source HEADs the rebuild ingested (DEC a9176261)
|
|
87
|
+
source TEXT PRIMARY KEY,
|
|
88
|
+
head TEXT NOT NULL
|
|
89
|
+
);
|
|
81
90
|
"""
|
|
82
91
|
|
|
83
92
|
config_class = SQLiteGraphCapabilityConfig
|
|
@@ -207,6 +216,14 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
207
216
|
# them up on the next initialize.
|
|
208
217
|
con.execute("CREATE INDEX IF NOT EXISTS idx_edges_source_type ON edges(source_id, relation_type);")
|
|
209
218
|
con.execute("CREATE INDEX IF NOT EXISTS idx_edges_target_type ON edges(target_id, relation_type);")
|
|
219
|
+
# The ingest record (DEC a9176261): the source HEADs this db was ingested from —
|
|
220
|
+
# provenance about the db itself, beside the graph and never in it.
|
|
221
|
+
con.execute("""
|
|
222
|
+
CREATE TABLE IF NOT EXISTS ingest_sources (
|
|
223
|
+
source TEXT PRIMARY KEY,
|
|
224
|
+
head TEXT NOT NULL
|
|
225
|
+
)
|
|
226
|
+
""")
|
|
210
227
|
# H7 companion: planner statistics. Without sqlite_stat1 the
|
|
211
228
|
# correlated-probe plans pick the WRONG composite index (the
|
|
212
229
|
# target-side probe scans a whole document's PART_OF rows per
|
|
@@ -550,7 +567,13 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
550
567
|
edge_id: str, # UUID of edge to update
|
|
551
568
|
properties: Dict[str, Any] # Properties to merge/update
|
|
552
569
|
) -> bool: # True if successful
|
|
553
|
-
"""Partial update of edge properties.
|
|
570
|
+
"""Partial update of edge properties.
|
|
571
|
+
|
|
572
|
+
A reserved `updated_at` key in `properties` sets the COLUMN instead of now()
|
|
573
|
+
and never lands in the JSON blob — the op clock's stamp (design 8f6f2343),
|
|
574
|
+
exactly as `update_node` takes it."""
|
|
575
|
+
properties = dict(properties)
|
|
576
|
+
stamp = properties.pop("updated_at", None)
|
|
554
577
|
with self._connect() as con:
|
|
555
578
|
cur = con.execute("SELECT properties FROM edges WHERE id = ?", (edge_id,))
|
|
556
579
|
row = cur.fetchone()
|
|
@@ -562,7 +585,7 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
562
585
|
|
|
563
586
|
con.execute(
|
|
564
587
|
"UPDATE edges SET properties = ?, updated_at = ? WHERE id = ?",
|
|
565
|
-
(json.dumps(existing), time.time(), edge_id)
|
|
588
|
+
(json.dumps(existing), stamp if stamp is not None else time.time(), edge_id)
|
|
566
589
|
)
|
|
567
590
|
return True
|
|
568
591
|
|
|
@@ -651,7 +674,8 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
651
674
|
nodes: List[GraphNode], # Nodes to import
|
|
652
675
|
merge_strategy: str # "overwrite" | "skip" | "merge"
|
|
653
676
|
) -> int: # Count of rows written (inserted or updated)
|
|
654
|
-
"""Import nodes honoring merge_strategy (see import_graph).
|
|
677
|
+
"""Import nodes honoring merge_strategy (see import_graph). Carried
|
|
678
|
+
created_at / updated_at are honored like add_nodes (0d50b921, design 8f6f2343)."""
|
|
655
679
|
now = time.time()
|
|
656
680
|
written = 0
|
|
657
681
|
with self._connect() as con:
|
|
@@ -662,7 +686,10 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
662
686
|
if row is None:
|
|
663
687
|
con.execute(
|
|
664
688
|
"INSERT INTO nodes (id, label, properties, sources, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
665
|
-
(node.id, node.label, props_json, sources_json,
|
|
689
|
+
(node.id, node.label, props_json, sources_json,
|
|
690
|
+
node.created_at if node.created_at is not None else now,
|
|
691
|
+
node.updated_at if node.updated_at is not None
|
|
692
|
+
else (node.created_at if node.created_at is not None else now)),
|
|
666
693
|
)
|
|
667
694
|
written += 1
|
|
668
695
|
elif merge_strategy == "skip":
|
|
@@ -670,7 +697,8 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
670
697
|
elif merge_strategy == "overwrite":
|
|
671
698
|
con.execute(
|
|
672
699
|
"UPDATE nodes SET label = ?, properties = ?, sources = ?, updated_at = ? WHERE id = ?",
|
|
673
|
-
(node.label, props_json, sources_json,
|
|
700
|
+
(node.label, props_json, sources_json,
|
|
701
|
+
node.updated_at if node.updated_at is not None else now, node.id),
|
|
674
702
|
)
|
|
675
703
|
written += 1
|
|
676
704
|
else: # merge
|
|
@@ -686,7 +714,8 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
686
714
|
seen.add(key)
|
|
687
715
|
con.execute(
|
|
688
716
|
"UPDATE nodes SET label = ?, properties = ?, sources = ?, updated_at = ? WHERE id = ?",
|
|
689
|
-
(node.label, json.dumps(merged_props), json.dumps(existing_sources),
|
|
717
|
+
(node.label, json.dumps(merged_props), json.dumps(existing_sources),
|
|
718
|
+
node.updated_at if node.updated_at is not None else now, node.id),
|
|
690
719
|
)
|
|
691
720
|
written += 1
|
|
692
721
|
return written
|
|
@@ -696,7 +725,8 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
696
725
|
edges: List[GraphEdge], # Edges to import
|
|
697
726
|
merge_strategy: str # "overwrite" | "skip" | "merge"
|
|
698
727
|
) -> int: # Count of rows written (inserted or updated)
|
|
699
|
-
"""Import edges honoring merge_strategy (see import_graph).
|
|
728
|
+
"""Import edges honoring merge_strategy (see import_graph). Carried
|
|
729
|
+
created_at / updated_at are honored like add_edges (0d50b921, design 8f6f2343)."""
|
|
700
730
|
now = time.time()
|
|
701
731
|
written = 0
|
|
702
732
|
with self._connect() as con:
|
|
@@ -707,7 +737,10 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
707
737
|
try:
|
|
708
738
|
con.execute(
|
|
709
739
|
"INSERT INTO edges (id, source_id, target_id, relation_type, properties, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
710
|
-
(edge.id, edge.source_id, edge.target_id, edge.relation_type, props_json,
|
|
740
|
+
(edge.id, edge.source_id, edge.target_id, edge.relation_type, props_json,
|
|
741
|
+
edge.created_at if edge.created_at is not None else now,
|
|
742
|
+
edge.updated_at if edge.updated_at is not None
|
|
743
|
+
else (edge.created_at if edge.created_at is not None else now)),
|
|
711
744
|
)
|
|
712
745
|
written += 1
|
|
713
746
|
except sqlite3.IntegrityError as err:
|
|
@@ -717,7 +750,8 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
717
750
|
elif merge_strategy == "overwrite":
|
|
718
751
|
con.execute(
|
|
719
752
|
"UPDATE edges SET source_id = ?, target_id = ?, relation_type = ?, properties = ?, updated_at = ? WHERE id = ?",
|
|
720
|
-
(edge.source_id, edge.target_id, edge.relation_type, props_json,
|
|
753
|
+
(edge.source_id, edge.target_id, edge.relation_type, props_json,
|
|
754
|
+
edge.updated_at if edge.updated_at is not None else now, edge.id),
|
|
721
755
|
)
|
|
722
756
|
written += 1
|
|
723
757
|
else: # merge
|
|
@@ -725,7 +759,8 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
725
759
|
merged.update(edge.properties)
|
|
726
760
|
con.execute(
|
|
727
761
|
"UPDATE edges SET properties = ?, updated_at = ? WHERE id = ?",
|
|
728
|
-
(json.dumps(merged),
|
|
762
|
+
(json.dumps(merged), edge.updated_at if edge.updated_at is not None else now,
|
|
763
|
+
edge.id),
|
|
729
764
|
)
|
|
730
765
|
written += 1
|
|
731
766
|
return written
|
|
@@ -820,7 +855,7 @@ def query_nodes(
|
|
|
820
855
|
return NodeQueryResult(nodes=[self._row_to_node(r) for r in rows])
|
|
821
856
|
out = []
|
|
822
857
|
for r in rows:
|
|
823
|
-
d =
|
|
858
|
+
d = decode_projected_row(keys, r, NODE_STRUCTURAL_KEYS) # properties keep their JSON types
|
|
824
859
|
if "sources" in d and d["sources"]:
|
|
825
860
|
d["sources"] = json.loads(d["sources"]) # ref dicts (CR-19 shape)
|
|
826
861
|
out.append(d)
|
|
@@ -843,7 +878,7 @@ def query_edges(
|
|
|
843
878
|
con.close()
|
|
844
879
|
if mode == "full":
|
|
845
880
|
return EdgeQueryResult(edges=[self._row_to_edge(r) for r in rows])
|
|
846
|
-
return EdgeQueryResult(rows=[
|
|
881
|
+
return EdgeQueryResult(rows=[decode_projected_row(keys, r, EDGE_STRUCTURAL_KEYS) for r in rows])
|
|
847
882
|
|
|
848
883
|
|
|
849
884
|
def raw_query(
|
|
@@ -881,7 +916,39 @@ def integrity_check(self) -> Dict[str, Any]: # {"ok": bool, "errors": [...], "b
|
|
|
881
916
|
return {"ok": ok, "errors": [] if ok else rows, "backend": "sqlite"}
|
|
882
917
|
|
|
883
918
|
|
|
919
|
+
def record_ingest_sources(
|
|
920
|
+
self,
|
|
921
|
+
sources: Dict[str, str], # source id -> the HEAD commit the ingest read
|
|
922
|
+
) -> int: # Sources recorded
|
|
923
|
+
"""Record the source HEADs this db was ingested from, REPLACING any earlier record
|
|
924
|
+
(DEC a9176261). The rebuild writes it into the fresh db it builds, so the record and the
|
|
925
|
+
rows it describes swap in together; it is provenance about the db, not a graph row, so
|
|
926
|
+
it is neither stamped by the op clock nor seen by the write observer."""
|
|
927
|
+
rows = sorted((str(k), str(v)) for k, v in dict(sources).items())
|
|
928
|
+
with self._connect() as con:
|
|
929
|
+
con.execute("DELETE FROM ingest_sources")
|
|
930
|
+
con.executemany("INSERT INTO ingest_sources (source, head) VALUES (?, ?)", rows)
|
|
931
|
+
return len(rows)
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def ingest_sources(self) -> Dict[str, str]: # source id -> HEAD; {} when the db holds no record
|
|
935
|
+
"""The source HEADs this db was ingested from, on a fresh read-only connection. A db built
|
|
936
|
+
before the record existed has no table: it reads as no record, never as an error."""
|
|
937
|
+
con = sqlite3.connect(f"file:{self._db_path}?mode=ro", uri=True)
|
|
938
|
+
try:
|
|
939
|
+
rows = con.execute("SELECT source, head FROM ingest_sources ORDER BY source").fetchall()
|
|
940
|
+
except sqlite3.OperationalError as e:
|
|
941
|
+
if "no such table" not in str(e):
|
|
942
|
+
raise
|
|
943
|
+
rows = []
|
|
944
|
+
finally:
|
|
945
|
+
con.close()
|
|
946
|
+
return {s: h for s, h in rows}
|
|
947
|
+
|
|
948
|
+
|
|
884
949
|
SQLiteGraphCapability.query_nodes = query_nodes
|
|
885
950
|
SQLiteGraphCapability.query_edges = query_edges
|
|
886
951
|
SQLiteGraphCapability.raw_query = raw_query
|
|
887
952
|
SQLiteGraphCapability.integrity_check = integrity_check
|
|
953
|
+
SQLiteGraphCapability.record_ingest_sources = record_ingest_sources
|
|
954
|
+
SQLiteGraphCapability.ingest_sources = ingest_sources
|
|
@@ -25,7 +25,11 @@ Translation rules:
|
|
|
25
25
|
Projected node rows always carry `id` (+ `label`/`created_at`/`updated_at`
|
|
26
26
|
projectable structurally — the storage stamps, never shadowed by a
|
|
27
27
|
same-named property; `sources` projectable as parsed ref dicts); edge rows
|
|
28
|
-
always carry `id`/`source_id`/`target_id` (+ `relation_type` projectable).
|
|
28
|
+
always carry `id`/`source_id`/`target_id` (+ `relation_type` projectable).
|
|
29
|
+
A projected PROPERTY is selected as its JSON representation (`->`) and decoded
|
|
30
|
+
by the capability, so it keeps its type — a list stays a list, an object an
|
|
31
|
+
object (`json_extract` returned arrays and objects as JSON TEXT: a projected
|
|
32
|
+
`calls` list read as a string, the live code fold's index keyed characters)."""
|
|
29
33
|
|
|
30
34
|
import json
|
|
31
35
|
import re
|
|
@@ -238,7 +242,7 @@ def translate_node_query(
|
|
|
238
242
|
select_parts.append("n.sources")
|
|
239
243
|
keys.append("sources")
|
|
240
244
|
else:
|
|
241
|
-
select_parts.append(f"
|
|
245
|
+
select_parts.append(f"n.properties -> {_json_path(name)}")
|
|
242
246
|
keys.append(name)
|
|
243
247
|
tail = _order_limit_sql(q, "n.properties", params)
|
|
244
248
|
return (f"SELECT {', '.join(select_parts)} FROM nodes AS n{where_sql}{tail}",
|
|
@@ -306,7 +310,7 @@ def translate_edge_query(
|
|
|
306
310
|
select_parts.append("e.relation_type")
|
|
307
311
|
keys.append("relation_type")
|
|
308
312
|
else:
|
|
309
|
-
select_parts.append(f"
|
|
313
|
+
select_parts.append(f"e.properties -> {_json_path(name)}")
|
|
310
314
|
keys.append(name)
|
|
311
315
|
tail = _order_limit_sql(q, "e.properties", params)
|
|
312
316
|
return (f"SELECT {', '.join(select_parts)} FROM edges AS e{where_sql}{tail}",
|
|
@@ -315,3 +319,21 @@ def translate_edge_query(
|
|
|
315
319
|
tail = _order_limit_sql(q, "e.properties", params)
|
|
316
320
|
return (f"SELECT {EDGE_FULL_COLUMNS} FROM edges AS e{where_sql}{tail}",
|
|
317
321
|
params, "full", None)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# The structural columns a projection selects as-is; every other projected key is a property,
|
|
325
|
+
# selected as JSON (`->`) and decoded by `decode_projected_row`.
|
|
326
|
+
NODE_STRUCTURAL_KEYS = frozenset({"id", "label", "created_at", "updated_at", "sources"})
|
|
327
|
+
EDGE_STRUCTURAL_KEYS = frozenset({"id", "source_id", "target_id", "relation_type"})
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def decode_projected_row(
|
|
331
|
+
keys: List[str], # The projected keys, in select order
|
|
332
|
+
values: Any, # One cursor row
|
|
333
|
+
structural: frozenset, # The keys selected as plain columns
|
|
334
|
+
) -> dict: # The row: structural columns as-is, each property decoded to its JSON type (absent -> None)
|
|
335
|
+
"""Zip a projected row, decoding every property column from its JSON representation."""
|
|
336
|
+
out = {}
|
|
337
|
+
for k, v in zip(keys, values):
|
|
338
|
+
out[k] = v if k in structural or v is None else json.loads(v)
|
|
339
|
+
return out
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cjm-capability-graph-sqlite
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.29
|
|
4
4
|
Summary: A local, file-backed context-graph storage capability for the cjm-substrate runtime that implements graph storage, traversal, and querying using SQLite.
|
|
5
5
|
Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -26,6 +26,7 @@ A local, file-backed context-graph storage capability for the cjm-substrate runt
|
|
|
26
26
|
|
|
27
27
|
## Modules
|
|
28
28
|
|
|
29
|
+
- **`cjm_capability_graph_sqlite.__init__`**
|
|
29
30
|
- **`cjm_capability_graph_sqlite.capability`** — Capability implementation for Context Graph using SQLite.
|
|
30
31
|
- **`cjm_capability_graph_sqlite.query_translation`** — The per-backend translation of the typed query expressions (pass-2 Thread 5; stage 4): NodeQuery/EdgeQuery -> parameterized SQLite SQL over the nodes/edges schema. THIS module is what makes the typed surface portable - the expression is domain- and backend-neutral; every backend tool owns a translation like this one (the ratified stage-4 split: backend owns translation; the adapter stays generic). Pure functions, unit-tested against an in-memory DB with the production schema - no capability runtime needed.
|
|
31
32
|
|
|
@@ -35,13 +36,16 @@ A local, file-backed context-graph storage capability for the cjm-substrate runt
|
|
|
35
36
|
|
|
36
37
|
- `SQLiteGraphCapability` _class_ — Local, file-backed Context Graph implementation using SQLite.
|
|
37
38
|
- `SQLiteGraphCapabilityConfig` _class_ — Configuration for SQLite Graph Capability.
|
|
39
|
+
- `ingest_sources` _function_ — The source HEADs this db was ingested from, on a fresh read-only connection. A db built
|
|
38
40
|
- `integrity_check` _function_ — Backend self-check (stage 4; the G3 corruption find institutionalized):
|
|
39
41
|
- `query_edges` _function_ — Execute a typed edge query (stage 4) — same contract as `query_nodes`.
|
|
40
42
|
- `query_nodes` _function_ — Execute a typed node query (stage 4) — translated to parameterized SQL
|
|
41
43
|
- `raw_query` _function_ — Execute the raw escape — refuses backend mismatches (non-portable by
|
|
44
|
+
- `record_ingest_sources` _function_ — Record the source HEADs this db was ingested from, REPLACING any earlier record
|
|
42
45
|
|
|
43
46
|
### `cjm_capability_graph_sqlite.query_translation`
|
|
44
47
|
|
|
48
|
+
- `decode_projected_row` _function_ — Zip a projected row, decoding every property column from its JSON representation.
|
|
45
49
|
- `translate_edge_query` _function_ — Translate an `EdgeQuery` to parameterized SQLite SQL.
|
|
46
50
|
- `translate_node_query` _function_ — Translate a `NodeQuery` to parameterized SQLite SQL.
|
|
47
51
|
|
{cjm_capability_graph_sqlite-0.0.28 → cjm_capability_graph_sqlite-0.0.29}/tests/test_capability.py
RENAMED
|
@@ -221,3 +221,67 @@ def test_add_edges_aggregates_missing_endpoint_warnings(cap, caplog):
|
|
|
221
221
|
msg = fk_lines[0].getMessage()
|
|
222
222
|
assert "25 of 26" in msg and "sample:" in msg
|
|
223
223
|
assert not any("Edge creation error (likely missing node)" in r.getMessage() for r in caplog.records)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_update_edge_reserved_updated_at_sets_column(cap):
|
|
227
|
+
# The op clock (design 8f6f2343): update_edge takes the reserved `updated_at` exactly as
|
|
228
|
+
# update_node does — the COLUMN, never the JSON blob; without it, now()-stamping stands.
|
|
229
|
+
import json
|
|
230
|
+
|
|
231
|
+
a, b = str(uuid.uuid4()), str(uuid.uuid4())
|
|
232
|
+
cap.add_nodes([GraphNode(id=a, label="Person"), GraphNode(id=b, label="Person")])
|
|
233
|
+
eid = str(uuid.uuid4())
|
|
234
|
+
cap.add_edges([GraphEdge(id=eid, source_id=a, target_id=b, relation_type="KNOWS")])
|
|
235
|
+
assert cap.update_edge(eid, {"since": 1999, "updated_at": 1234.5})
|
|
236
|
+
con = sqlite3.connect(cap._db_path)
|
|
237
|
+
props, stamp = con.execute("SELECT properties, updated_at FROM edges WHERE id = ?",
|
|
238
|
+
(eid,)).fetchone()
|
|
239
|
+
con.close()
|
|
240
|
+
assert json.loads(props) == {"since": 1999} and stamp == 1234.5
|
|
241
|
+
assert cap.update_edge(eid, {"since": 2000})
|
|
242
|
+
con = sqlite3.connect(cap._db_path)
|
|
243
|
+
(stamp2,) = con.execute("SELECT updated_at FROM edges WHERE id = ?", (eid,)).fetchone()
|
|
244
|
+
con.close()
|
|
245
|
+
assert stamp2 > 1234.5
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_import_honors_carried_times(cap):
|
|
249
|
+
# import_graph keeps carried created_at / updated_at like add_nodes / add_edges.
|
|
250
|
+
a, b = str(uuid.uuid4()), str(uuid.uuid4())
|
|
251
|
+
eid = str(uuid.uuid4())
|
|
252
|
+
cap.import_graph(GraphContext(
|
|
253
|
+
nodes=[GraphNode(id=a, label="P", created_at=10.0, updated_at=20.0),
|
|
254
|
+
GraphNode(id=b, label="P", created_at=30.0)],
|
|
255
|
+
edges=[GraphEdge(id=eid, source_id=a, target_id=b, relation_type="R", created_at=40.0)]))
|
|
256
|
+
con = sqlite3.connect(cap._db_path)
|
|
257
|
+
rows = dict((r[0], r[1:]) for r in con.execute("SELECT id, created_at, updated_at FROM nodes"))
|
|
258
|
+
erow = con.execute("SELECT created_at, updated_at FROM edges WHERE id = ?", (eid,)).fetchone()
|
|
259
|
+
con.close()
|
|
260
|
+
assert rows[a] == (10.0, 20.0) and rows[b] == (30.0, 30.0) and erow == (40.0, 40.0)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_ingest_record_replaces_reads_readonly_and_tolerates_a_legacy_db(cap, tmp_path):
|
|
264
|
+
"""The ingest record (DEC a9176261): a replace, never a merge; beside the graph (no row,
|
|
265
|
+
no op-clock stamp); readable through a read-only open; a db built before the table
|
|
266
|
+
existed reads as no record."""
|
|
267
|
+
assert cap.ingest_sources() == {}
|
|
268
|
+
assert cap.record_ingest_sources({"archive:/site/posts": "a" * 40, "repo:/r/x": "b" * 40}) == 2
|
|
269
|
+
assert cap.record_ingest_sources({"repo:/r/x": "c" * 40}) == 1
|
|
270
|
+
assert cap.ingest_sources() == {"repo:/r/x": "c" * 40}
|
|
271
|
+
assert cap.get_schema()["counts"] == {} # beside the graph, never in it
|
|
272
|
+
|
|
273
|
+
ro = SQLiteGraphCapability()
|
|
274
|
+
ro.initialize({"db_path": cap._db_path, "readonly": True})
|
|
275
|
+
assert ro.ingest_sources() == {"repo:/r/x": "c" * 40}
|
|
276
|
+
ro.cleanup()
|
|
277
|
+
|
|
278
|
+
legacy = str(tmp_path / "legacy.db")
|
|
279
|
+
con = sqlite3.connect(legacy)
|
|
280
|
+
con.execute("CREATE TABLE nodes (id TEXT PRIMARY KEY, label TEXT NOT NULL, properties JSON, "
|
|
281
|
+
"sources JSON, created_at REAL, updated_at REAL)")
|
|
282
|
+
con.commit()
|
|
283
|
+
con.close()
|
|
284
|
+
old = SQLiteGraphCapability()
|
|
285
|
+
old.initialize({"db_path": legacy, "readonly": True})
|
|
286
|
+
assert old.ingest_sources() == {}
|
|
287
|
+
old.cleanup()
|
|
@@ -8,7 +8,10 @@ import sqlite3
|
|
|
8
8
|
|
|
9
9
|
import pytest
|
|
10
10
|
|
|
11
|
-
from cjm_capability_graph_sqlite.query_translation import (
|
|
11
|
+
from cjm_capability_graph_sqlite.query_translation import (EDGE_STRUCTURAL_KEYS,
|
|
12
|
+
NODE_STRUCTURAL_KEYS,
|
|
13
|
+
decode_projected_row,
|
|
14
|
+
translate_edge_query,
|
|
12
15
|
translate_node_query)
|
|
13
16
|
from cjm_context_graph_primitives.locators import FileRef
|
|
14
17
|
from cjm_context_graph_primitives.query import (EdgeQuery, NodeQuery, OrderBy,
|
|
@@ -71,7 +74,8 @@ def run(con, q):
|
|
|
71
74
|
return cur.fetchone()[0]
|
|
72
75
|
rows = cur.fetchall()
|
|
73
76
|
if mode == "rows":
|
|
74
|
-
|
|
77
|
+
structural = NODE_STRUCTURAL_KEYS if isinstance(q, NodeQuery) else EDGE_STRUCTURAL_KEYS
|
|
78
|
+
return [decode_projected_row(keys, r, structural) for r in rows]
|
|
75
79
|
return rows
|
|
76
80
|
|
|
77
81
|
|
|
@@ -85,6 +89,21 @@ def test_spine_read_ordered_projection(con):
|
|
|
85
89
|
assert json.loads(rows[1]["sources"])[0]["content_hash"] == "sha256:ab"
|
|
86
90
|
|
|
87
91
|
|
|
92
|
+
def test_projected_properties_keep_their_json_types(con):
|
|
93
|
+
# A projected object stays an object and a missing key is None (json_extract used to
|
|
94
|
+
# hand arrays and objects back as JSON TEXT — a projected list read as a string).
|
|
95
|
+
rows = run(con, NodeQuery(label="Correction", project=["payload", "correction_type"]))
|
|
96
|
+
by = {r["id"]: r for r in rows}
|
|
97
|
+
assert by["c1"]["payload"] == {"document_id": "doc-1", "segment_id": "s1"}
|
|
98
|
+
assert by["c2"]["correction_type"] == "text_content"
|
|
99
|
+
rows = run(con, NodeQuery(label="Segment", related=RelationPredicate("PART_OF", node_id="doc-1"),
|
|
100
|
+
order_by=OrderBy(prop="index"), project=["text", "start_time"]))
|
|
101
|
+
assert [r["text"] for r in rows] == ["It's 100% real_deal", "hello world", "", None]
|
|
102
|
+
assert rows[1]["start_time"] == 1.0
|
|
103
|
+
rows = run(con, EdgeQuery(relation_type="REVIEWED", project=["decision"]))
|
|
104
|
+
assert rows == [{"id": "rev", "source_id": "sess-1", "target_id": "s0", "decision": "corrected"}]
|
|
105
|
+
|
|
106
|
+
|
|
88
107
|
def test_structural_timestamp_projection(con):
|
|
89
108
|
# created_at/updated_at project from the storage COLUMNS (the stamps),
|
|
90
109
|
# not json_extract over properties — no Segment carries them as props.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.28"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|