cjm-capability-graph-sqlite 0.0.26__tar.gz → 0.0.28__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.26/cjm_capability_graph_sqlite.egg-info → cjm_capability_graph_sqlite-0.0.28}/PKG-INFO +1 -1
- cjm_capability_graph_sqlite-0.0.28/cjm_capability_graph_sqlite/__init__.py +1 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.28}/cjm_capability_graph_sqlite/capability.py +45 -7
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.28}/cjm_capability_graph_sqlite/query_translation.py +7 -3
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.28/cjm_capability_graph_sqlite.egg-info}/PKG-INFO +1 -1
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.28}/tests/test_capability.py +44 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.28}/tests/test_query_translation.py +12 -0
- cjm_capability_graph_sqlite-0.0.26/cjm_capability_graph_sqlite/__init__.py +0 -1
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.28}/LICENSE +0 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.28}/MANIFEST.in +0 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.28}/README.md +0 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.28}/cjm_capability_graph_sqlite.egg-info/SOURCES.txt +0 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.28}/cjm_capability_graph_sqlite.egg-info/dependency_links.txt +0 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.28}/cjm_capability_graph_sqlite.egg-info/requires.txt +0 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.28}/cjm_capability_graph_sqlite.egg-info/top_level.txt +0 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.28}/pyproject.toml +0 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.28}/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.28
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.28"
|
|
@@ -82,6 +82,12 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
82
82
|
|
|
83
83
|
config_class = SQLiteGraphCapabilityConfig
|
|
84
84
|
|
|
85
|
+
# Substrate observability class (DEC 8bf656c0 as manifest DATA, finding 0d886ffe B):
|
|
86
|
+
# graph-storage ops are compute-light, re-derivable work — routine per-op journal
|
|
87
|
+
# rows and worker-lifecycle rows are noise (they filled 97% of journal.db); failures
|
|
88
|
+
# still journal. `cjm-ctl regenerate-manifest` records this on the manifest.
|
|
89
|
+
observability_class = "ambient"
|
|
90
|
+
|
|
85
91
|
def __init__(self):
|
|
86
92
|
self.logger = logging.getLogger(f"{__name__}.{type(self).__name__}")
|
|
87
93
|
self.config: SQLiteGraphCapabilityConfig = None
|
|
@@ -291,17 +297,23 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
291
297
|
self,
|
|
292
298
|
nodes: List[GraphNode] # Nodes to create
|
|
293
299
|
) -> List[str]: # Created node IDs
|
|
294
|
-
"""Bulk create nodes.
|
|
300
|
+
"""Bulk create nodes.
|
|
301
|
+
|
|
302
|
+
A node arriving with created_at/updated_at already set keeps them —
|
|
303
|
+
journal REPLAY restores temporal provenance this way (finding 0d50b921:
|
|
304
|
+
stamping now() here clamped every rebuilt node to rebuild time)."""
|
|
295
305
|
ids = []
|
|
296
306
|
now = time.time()
|
|
297
307
|
with self._connect() as con:
|
|
298
308
|
for n in nodes:
|
|
299
309
|
sources_json = json.dumps([s.to_dict() for s in n.sources])
|
|
300
310
|
props_json = json.dumps(n.properties)
|
|
311
|
+
created = n.created_at if n.created_at is not None else now
|
|
312
|
+
updated = n.updated_at if n.updated_at is not None else created
|
|
301
313
|
try:
|
|
302
314
|
con.execute(
|
|
303
315
|
"INSERT INTO nodes (id, label, properties, sources, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
304
|
-
(n.id, n.label, props_json, sources_json,
|
|
316
|
+
(n.id, n.label, props_json, sources_json, created, updated)
|
|
305
317
|
)
|
|
306
318
|
ids.append(n.id)
|
|
307
319
|
except sqlite3.IntegrityError:
|
|
@@ -312,21 +324,40 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
312
324
|
self,
|
|
313
325
|
edges: List[GraphEdge] # Edges to create
|
|
314
326
|
) -> List[str]: # Created edge IDs
|
|
315
|
-
"""Bulk create edges.
|
|
327
|
+
"""Bulk create edges.
|
|
328
|
+
|
|
329
|
+
Carried created_at/updated_at are honored like add_nodes (0d50b921).
|
|
330
|
+
|
|
331
|
+
Missing-endpoint edges (FOREIGN KEY failures) are AGGREGATED into one warning
|
|
332
|
+
per call — count + a bounded sample — never one line per edge: a rebuild replays
|
|
333
|
+
every journaled edge whose endpoint is absent, and per-edge logging flooded
|
|
334
|
+
diagnostics.db to 1.5M rows / 633 MB (finding 0d886ffe). The skipped count is
|
|
335
|
+
recoverable by the caller as `len(edges) - len(ids)`."""
|
|
316
336
|
ids = []
|
|
317
337
|
now = time.time()
|
|
338
|
+
missing: List[Tuple[str, str, str]] = [] # (source_id, target_id, relation_type) of FK-skipped edges
|
|
318
339
|
with self._connect() as con:
|
|
319
340
|
con.execute("PRAGMA foreign_keys = ON;")
|
|
320
341
|
for e in edges:
|
|
321
342
|
props_json = json.dumps(e.properties)
|
|
343
|
+
created = e.created_at if e.created_at is not None else now
|
|
344
|
+
updated = e.updated_at if e.updated_at is not None else created
|
|
322
345
|
try:
|
|
323
346
|
con.execute(
|
|
324
347
|
"INSERT INTO edges (id, source_id, target_id, relation_type, properties, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
325
|
-
(e.id, e.source_id, e.target_id, e.relation_type, props_json,
|
|
348
|
+
(e.id, e.source_id, e.target_id, e.relation_type, props_json, created, updated)
|
|
326
349
|
)
|
|
327
350
|
ids.append(e.id)
|
|
328
351
|
except sqlite3.IntegrityError as err:
|
|
329
|
-
|
|
352
|
+
if "FOREIGN KEY" in str(err):
|
|
353
|
+
missing.append((e.source_id, e.target_id, e.relation_type))
|
|
354
|
+
else:
|
|
355
|
+
self.logger.warning(f"Edge creation error ({e.id}): {err}")
|
|
356
|
+
if missing:
|
|
357
|
+
sample = "; ".join(f"{s[:8]}->{t[:8]} {r}" for s, t, r in missing[:3])
|
|
358
|
+
self.logger.warning(
|
|
359
|
+
f"add_edges: {len(missing)} of {len(edges)} edge(s) skipped — endpoint node missing "
|
|
360
|
+
f"(FOREIGN KEY); sample: {sample}")
|
|
330
361
|
return ids
|
|
331
362
|
|
|
332
363
|
# -------------------------------------------------------------------------
|
|
@@ -490,7 +521,14 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
490
521
|
node_id: str, # UUID of node to update
|
|
491
522
|
properties: Dict[str, Any] # Properties to merge/update
|
|
492
523
|
) -> bool: # True if successful
|
|
493
|
-
"""Partial update of node properties.
|
|
524
|
+
"""Partial update of node properties.
|
|
525
|
+
|
|
526
|
+
A reserved `updated_at` key in `properties` sets the COLUMN instead of
|
|
527
|
+
now() and never lands in the JSON blob — journal REPLAY restores a STATE
|
|
528
|
+
op's true time this way (0d50b921 residual: later-STATE nodes clamped
|
|
529
|
+
to rebuild time)."""
|
|
530
|
+
properties = dict(properties)
|
|
531
|
+
stamp = properties.pop("updated_at", None)
|
|
494
532
|
with self._connect() as con:
|
|
495
533
|
# Fetch existing to merge
|
|
496
534
|
cur = con.execute("SELECT properties FROM nodes WHERE id = ?", (node_id,))
|
|
@@ -503,7 +541,7 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
503
541
|
|
|
504
542
|
con.execute(
|
|
505
543
|
"UPDATE nodes SET properties = ?, updated_at = ? WHERE id = ?",
|
|
506
|
-
(json.dumps(existing), time.time(), node_id)
|
|
544
|
+
(json.dumps(existing), stamp if stamp is not None else time.time(), node_id)
|
|
507
545
|
)
|
|
508
546
|
return True
|
|
509
547
|
|
|
@@ -22,9 +22,10 @@ Translation rules:
|
|
|
22
22
|
does Python-side locator equality; a recurring need here is promotion
|
|
23
23
|
evidence).
|
|
24
24
|
- **Modes**: `count` > `project` > full (mirrors the result DTO contract).
|
|
25
|
-
Projected node rows always carry `id` (+ `label`
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
Projected node rows always carry `id` (+ `label`/`created_at`/`updated_at`
|
|
26
|
+
projectable structurally — the storage stamps, never shadowed by a
|
|
27
|
+
same-named property; `sources` projectable as parsed ref dicts); edge rows
|
|
28
|
+
always carry `id`/`source_id`/`target_id` (+ `relation_type` projectable)."""
|
|
28
29
|
|
|
29
30
|
import json
|
|
30
31
|
import re
|
|
@@ -230,6 +231,9 @@ def translate_node_query(
|
|
|
230
231
|
if name == "label":
|
|
231
232
|
select_parts.append("n.label")
|
|
232
233
|
keys.append("label")
|
|
234
|
+
elif name in ("created_at", "updated_at"):
|
|
235
|
+
select_parts.append("n." + name) # storage stamp, not a property
|
|
236
|
+
keys.append(name)
|
|
233
237
|
elif name == "sources":
|
|
234
238
|
select_parts.append("n.sources")
|
|
235
239
|
keys.append("sources")
|
|
@@ -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.28
|
|
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
|
{cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.28}/tests/test_capability.py
RENAMED
|
@@ -177,3 +177,47 @@ def test_typed_surface_smoke(cap):
|
|
|
177
177
|
ctx = cap.export_graph(NodeQuery(label="Segment",
|
|
178
178
|
related=RelationPredicate("PART_OF", node_id=doc)))
|
|
179
179
|
assert len(ctx.nodes) == 3 and len(ctx.edges) == 2 # NEXT only (PART_OF targets the doc)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_update_node_reserved_updated_at_sets_column(cap):
|
|
183
|
+
# 0d50b921 residual: a reserved `updated_at` key in the update payload sets
|
|
184
|
+
# the COLUMN (journal replay restores a STATE op's true time) and never
|
|
185
|
+
# lands in the JSON properties blob; without it, now()-stamping stands.
|
|
186
|
+
import json
|
|
187
|
+
|
|
188
|
+
nid = str(uuid.uuid4())
|
|
189
|
+
cap.add_nodes([GraphNode(id=nid, label="Person", properties={"name": "Ada"})])
|
|
190
|
+
assert cap.update_node(nid, {"role": "host", "updated_at": 1234.5})
|
|
191
|
+
con = sqlite3.connect(cap._db_path)
|
|
192
|
+
row = con.execute("SELECT properties, updated_at FROM nodes WHERE id = ?",
|
|
193
|
+
(nid,)).fetchone()
|
|
194
|
+
con.close()
|
|
195
|
+
props = json.loads(row[0])
|
|
196
|
+
assert props["role"] == "host" and "updated_at" not in props
|
|
197
|
+
assert row[1] == 1234.5
|
|
198
|
+
# Without the reserved key the column re-stamps to now().
|
|
199
|
+
assert cap.update_node(nid, {"role": "guest"})
|
|
200
|
+
con = sqlite3.connect(cap._db_path)
|
|
201
|
+
row2 = con.execute("SELECT updated_at FROM nodes WHERE id = ?", (nid,)).fetchone()
|
|
202
|
+
con.close()
|
|
203
|
+
assert row2[0] > 1234.5
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_add_edges_aggregates_missing_endpoint_warnings(cap, caplog):
|
|
207
|
+
"""Finding 0d886ffe: FK-skipped edges log ONE warning per call (count + sample),
|
|
208
|
+
never one line per edge — per-edge logging flooded diagnostics.db during rebuilds."""
|
|
209
|
+
import logging
|
|
210
|
+
from cjm_context_graph_primitives.graph import GraphEdge, GraphNode
|
|
211
|
+
cap.add_nodes([GraphNode(id="n-live", label="T", properties={}, sources=[])])
|
|
212
|
+
edges = [GraphEdge(id=f"e{i}", source_id="n-live", target_id=f"n-missing-{i}",
|
|
213
|
+
relation_type="REL", properties={}) for i in range(25)]
|
|
214
|
+
edges.append(GraphEdge(id="e-ok", source_id="n-live", target_id="n-live",
|
|
215
|
+
relation_type="SELF", properties={}))
|
|
216
|
+
with caplog.at_level(logging.WARNING):
|
|
217
|
+
ids = cap.add_edges(edges)
|
|
218
|
+
assert ids == ["e-ok"] # skipped count recoverable as len(edges) - len(ids)
|
|
219
|
+
fk_lines = [r for r in caplog.records if "endpoint node missing" in r.getMessage()]
|
|
220
|
+
assert len(fk_lines) == 1
|
|
221
|
+
msg = fk_lines[0].getMessage()
|
|
222
|
+
assert "25 of 26" in msg and "sample:" in msg
|
|
223
|
+
assert not any("Edge creation error (likely missing node)" in r.getMessage() for r in caplog.records)
|
|
@@ -85,6 +85,18 @@ def test_spine_read_ordered_projection(con):
|
|
|
85
85
|
assert json.loads(rows[1]["sources"])[0]["content_hash"] == "sha256:ab"
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def test_structural_timestamp_projection(con):
|
|
89
|
+
# created_at/updated_at project from the storage COLUMNS (the stamps),
|
|
90
|
+
# not json_extract over properties — no Segment carries them as props.
|
|
91
|
+
rows = run(con, NodeQuery(label="Segment",
|
|
92
|
+
related=RelationPredicate("PART_OF", node_id="doc-1"),
|
|
93
|
+
order_by=OrderBy(prop="index"),
|
|
94
|
+
project=["index", "created_at", "updated_at"]))
|
|
95
|
+
assert [r["created_at"] for r in rows] == [1.0, 1.0, 1.0, 1.0]
|
|
96
|
+
assert rows[0]["updated_at"] == 1.0
|
|
97
|
+
assert rows[0]["id"] == "s0"
|
|
98
|
+
|
|
99
|
+
|
|
88
100
|
def test_empty_filter_or_case_two_counts(con):
|
|
89
101
|
empty_eq = run(con, NodeQuery(label="Segment", where=[PropertyPredicate("text", "eq", "")],
|
|
90
102
|
related=RelationPredicate("PART_OF", node_id="doc-1"), count=True))
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.26"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|