cjm-capability-graph-sqlite 0.0.26__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.26/cjm_capability_graph_sqlite.egg-info → cjm_capability_graph_sqlite-0.0.29}/PKG-INFO +5 -1
- {cjm_capability_graph_sqlite-0.0.26 → 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.26 → cjm_capability_graph_sqlite-0.0.29}/cjm_capability_graph_sqlite/capability.py +125 -20
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.29}/cjm_capability_graph_sqlite/query_translation.py +31 -5
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.29/cjm_capability_graph_sqlite.egg-info}/PKG-INFO +5 -1
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.29}/tests/test_capability.py +108 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.29}/tests/test_query_translation.py +33 -2
- 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.29}/LICENSE +0 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.29}/MANIFEST.in +0 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.29}/cjm_capability_graph_sqlite.egg-info/SOURCES.txt +0 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.29}/cjm_capability_graph_sqlite.egg-info/dependency_links.txt +0 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.29}/cjm_capability_graph_sqlite.egg-info/requires.txt +0 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.29}/cjm_capability_graph_sqlite.egg-info/top_level.txt +0 -0
- {cjm_capability_graph_sqlite-0.0.26 → cjm_capability_graph_sqlite-0.0.29}/pyproject.toml +0 -0
- {cjm_capability_graph_sqlite-0.0.26 → 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,10 +82,21 @@ 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
|
|
84
93
|
|
|
94
|
+
# Substrate observability class (DEC 8bf656c0 as manifest DATA, finding 0d886ffe B):
|
|
95
|
+
# graph-storage ops are compute-light, re-derivable work — routine per-op journal
|
|
96
|
+
# rows and worker-lifecycle rows are noise (they filled 97% of journal.db); failures
|
|
97
|
+
# still journal. `cjm-ctl regenerate-manifest` records this on the manifest.
|
|
98
|
+
observability_class = "ambient"
|
|
99
|
+
|
|
85
100
|
def __init__(self):
|
|
86
101
|
self.logger = logging.getLogger(f"{__name__}.{type(self).__name__}")
|
|
87
102
|
self.config: SQLiteGraphCapabilityConfig = None
|
|
@@ -201,6 +216,14 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
201
216
|
# them up on the next initialize.
|
|
202
217
|
con.execute("CREATE INDEX IF NOT EXISTS idx_edges_source_type ON edges(source_id, relation_type);")
|
|
203
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
|
+
""")
|
|
204
227
|
# H7 companion: planner statistics. Without sqlite_stat1 the
|
|
205
228
|
# correlated-probe plans pick the WRONG composite index (the
|
|
206
229
|
# target-side probe scans a whole document's PART_OF rows per
|
|
@@ -291,17 +314,23 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
291
314
|
self,
|
|
292
315
|
nodes: List[GraphNode] # Nodes to create
|
|
293
316
|
) -> List[str]: # Created node IDs
|
|
294
|
-
"""Bulk create nodes.
|
|
317
|
+
"""Bulk create nodes.
|
|
318
|
+
|
|
319
|
+
A node arriving with created_at/updated_at already set keeps them —
|
|
320
|
+
journal REPLAY restores temporal provenance this way (finding 0d50b921:
|
|
321
|
+
stamping now() here clamped every rebuilt node to rebuild time)."""
|
|
295
322
|
ids = []
|
|
296
323
|
now = time.time()
|
|
297
324
|
with self._connect() as con:
|
|
298
325
|
for n in nodes:
|
|
299
326
|
sources_json = json.dumps([s.to_dict() for s in n.sources])
|
|
300
327
|
props_json = json.dumps(n.properties)
|
|
328
|
+
created = n.created_at if n.created_at is not None else now
|
|
329
|
+
updated = n.updated_at if n.updated_at is not None else created
|
|
301
330
|
try:
|
|
302
331
|
con.execute(
|
|
303
332
|
"INSERT INTO nodes (id, label, properties, sources, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
304
|
-
(n.id, n.label, props_json, sources_json,
|
|
333
|
+
(n.id, n.label, props_json, sources_json, created, updated)
|
|
305
334
|
)
|
|
306
335
|
ids.append(n.id)
|
|
307
336
|
except sqlite3.IntegrityError:
|
|
@@ -312,21 +341,40 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
312
341
|
self,
|
|
313
342
|
edges: List[GraphEdge] # Edges to create
|
|
314
343
|
) -> List[str]: # Created edge IDs
|
|
315
|
-
"""Bulk create edges.
|
|
344
|
+
"""Bulk create edges.
|
|
345
|
+
|
|
346
|
+
Carried created_at/updated_at are honored like add_nodes (0d50b921).
|
|
347
|
+
|
|
348
|
+
Missing-endpoint edges (FOREIGN KEY failures) are AGGREGATED into one warning
|
|
349
|
+
per call — count + a bounded sample — never one line per edge: a rebuild replays
|
|
350
|
+
every journaled edge whose endpoint is absent, and per-edge logging flooded
|
|
351
|
+
diagnostics.db to 1.5M rows / 633 MB (finding 0d886ffe). The skipped count is
|
|
352
|
+
recoverable by the caller as `len(edges) - len(ids)`."""
|
|
316
353
|
ids = []
|
|
317
354
|
now = time.time()
|
|
355
|
+
missing: List[Tuple[str, str, str]] = [] # (source_id, target_id, relation_type) of FK-skipped edges
|
|
318
356
|
with self._connect() as con:
|
|
319
357
|
con.execute("PRAGMA foreign_keys = ON;")
|
|
320
358
|
for e in edges:
|
|
321
359
|
props_json = json.dumps(e.properties)
|
|
360
|
+
created = e.created_at if e.created_at is not None else now
|
|
361
|
+
updated = e.updated_at if e.updated_at is not None else created
|
|
322
362
|
try:
|
|
323
363
|
con.execute(
|
|
324
364
|
"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,
|
|
365
|
+
(e.id, e.source_id, e.target_id, e.relation_type, props_json, created, updated)
|
|
326
366
|
)
|
|
327
367
|
ids.append(e.id)
|
|
328
368
|
except sqlite3.IntegrityError as err:
|
|
329
|
-
|
|
369
|
+
if "FOREIGN KEY" in str(err):
|
|
370
|
+
missing.append((e.source_id, e.target_id, e.relation_type))
|
|
371
|
+
else:
|
|
372
|
+
self.logger.warning(f"Edge creation error ({e.id}): {err}")
|
|
373
|
+
if missing:
|
|
374
|
+
sample = "; ".join(f"{s[:8]}->{t[:8]} {r}" for s, t, r in missing[:3])
|
|
375
|
+
self.logger.warning(
|
|
376
|
+
f"add_edges: {len(missing)} of {len(edges)} edge(s) skipped — endpoint node missing "
|
|
377
|
+
f"(FOREIGN KEY); sample: {sample}")
|
|
330
378
|
return ids
|
|
331
379
|
|
|
332
380
|
# -------------------------------------------------------------------------
|
|
@@ -490,7 +538,14 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
490
538
|
node_id: str, # UUID of node to update
|
|
491
539
|
properties: Dict[str, Any] # Properties to merge/update
|
|
492
540
|
) -> bool: # True if successful
|
|
493
|
-
"""Partial update of node properties.
|
|
541
|
+
"""Partial update of node properties.
|
|
542
|
+
|
|
543
|
+
A reserved `updated_at` key in `properties` sets the COLUMN instead of
|
|
544
|
+
now() and never lands in the JSON blob — journal REPLAY restores a STATE
|
|
545
|
+
op's true time this way (0d50b921 residual: later-STATE nodes clamped
|
|
546
|
+
to rebuild time)."""
|
|
547
|
+
properties = dict(properties)
|
|
548
|
+
stamp = properties.pop("updated_at", None)
|
|
494
549
|
with self._connect() as con:
|
|
495
550
|
# Fetch existing to merge
|
|
496
551
|
cur = con.execute("SELECT properties FROM nodes WHERE id = ?", (node_id,))
|
|
@@ -503,7 +558,7 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
503
558
|
|
|
504
559
|
con.execute(
|
|
505
560
|
"UPDATE nodes SET properties = ?, updated_at = ? WHERE id = ?",
|
|
506
|
-
(json.dumps(existing), time.time(), node_id)
|
|
561
|
+
(json.dumps(existing), stamp if stamp is not None else time.time(), node_id)
|
|
507
562
|
)
|
|
508
563
|
return True
|
|
509
564
|
|
|
@@ -512,7 +567,13 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
512
567
|
edge_id: str, # UUID of edge to update
|
|
513
568
|
properties: Dict[str, Any] # Properties to merge/update
|
|
514
569
|
) -> bool: # True if successful
|
|
515
|
-
"""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)
|
|
516
577
|
with self._connect() as con:
|
|
517
578
|
cur = con.execute("SELECT properties FROM edges WHERE id = ?", (edge_id,))
|
|
518
579
|
row = cur.fetchone()
|
|
@@ -524,7 +585,7 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
524
585
|
|
|
525
586
|
con.execute(
|
|
526
587
|
"UPDATE edges SET properties = ?, updated_at = ? WHERE id = ?",
|
|
527
|
-
(json.dumps(existing), time.time(), edge_id)
|
|
588
|
+
(json.dumps(existing), stamp if stamp is not None else time.time(), edge_id)
|
|
528
589
|
)
|
|
529
590
|
return True
|
|
530
591
|
|
|
@@ -613,7 +674,8 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
613
674
|
nodes: List[GraphNode], # Nodes to import
|
|
614
675
|
merge_strategy: str # "overwrite" | "skip" | "merge"
|
|
615
676
|
) -> int: # Count of rows written (inserted or updated)
|
|
616
|
-
"""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)."""
|
|
617
679
|
now = time.time()
|
|
618
680
|
written = 0
|
|
619
681
|
with self._connect() as con:
|
|
@@ -624,7 +686,10 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
624
686
|
if row is None:
|
|
625
687
|
con.execute(
|
|
626
688
|
"INSERT INTO nodes (id, label, properties, sources, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
627
|
-
(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)),
|
|
628
693
|
)
|
|
629
694
|
written += 1
|
|
630
695
|
elif merge_strategy == "skip":
|
|
@@ -632,7 +697,8 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
632
697
|
elif merge_strategy == "overwrite":
|
|
633
698
|
con.execute(
|
|
634
699
|
"UPDATE nodes SET label = ?, properties = ?, sources = ?, updated_at = ? WHERE id = ?",
|
|
635
|
-
(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),
|
|
636
702
|
)
|
|
637
703
|
written += 1
|
|
638
704
|
else: # merge
|
|
@@ -648,7 +714,8 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
648
714
|
seen.add(key)
|
|
649
715
|
con.execute(
|
|
650
716
|
"UPDATE nodes SET label = ?, properties = ?, sources = ?, updated_at = ? WHERE id = ?",
|
|
651
|
-
(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),
|
|
652
719
|
)
|
|
653
720
|
written += 1
|
|
654
721
|
return written
|
|
@@ -658,7 +725,8 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
658
725
|
edges: List[GraphEdge], # Edges to import
|
|
659
726
|
merge_strategy: str # "overwrite" | "skip" | "merge"
|
|
660
727
|
) -> int: # Count of rows written (inserted or updated)
|
|
661
|
-
"""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)."""
|
|
662
730
|
now = time.time()
|
|
663
731
|
written = 0
|
|
664
732
|
with self._connect() as con:
|
|
@@ -669,7 +737,10 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
669
737
|
try:
|
|
670
738
|
con.execute(
|
|
671
739
|
"INSERT INTO edges (id, source_id, target_id, relation_type, properties, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
672
|
-
(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)),
|
|
673
744
|
)
|
|
674
745
|
written += 1
|
|
675
746
|
except sqlite3.IntegrityError as err:
|
|
@@ -679,7 +750,8 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
679
750
|
elif merge_strategy == "overwrite":
|
|
680
751
|
con.execute(
|
|
681
752
|
"UPDATE edges SET source_id = ?, target_id = ?, relation_type = ?, properties = ?, updated_at = ? WHERE id = ?",
|
|
682
|
-
(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),
|
|
683
755
|
)
|
|
684
756
|
written += 1
|
|
685
757
|
else: # merge
|
|
@@ -687,7 +759,8 @@ class SQLiteGraphCapability(ToolCapability):
|
|
|
687
759
|
merged.update(edge.properties)
|
|
688
760
|
con.execute(
|
|
689
761
|
"UPDATE edges SET properties = ?, updated_at = ? WHERE id = ?",
|
|
690
|
-
(json.dumps(merged),
|
|
762
|
+
(json.dumps(merged), edge.updated_at if edge.updated_at is not None else now,
|
|
763
|
+
edge.id),
|
|
691
764
|
)
|
|
692
765
|
written += 1
|
|
693
766
|
return written
|
|
@@ -782,7 +855,7 @@ def query_nodes(
|
|
|
782
855
|
return NodeQueryResult(nodes=[self._row_to_node(r) for r in rows])
|
|
783
856
|
out = []
|
|
784
857
|
for r in rows:
|
|
785
|
-
d =
|
|
858
|
+
d = decode_projected_row(keys, r, NODE_STRUCTURAL_KEYS) # properties keep their JSON types
|
|
786
859
|
if "sources" in d and d["sources"]:
|
|
787
860
|
d["sources"] = json.loads(d["sources"]) # ref dicts (CR-19 shape)
|
|
788
861
|
out.append(d)
|
|
@@ -805,7 +878,7 @@ def query_edges(
|
|
|
805
878
|
con.close()
|
|
806
879
|
if mode == "full":
|
|
807
880
|
return EdgeQueryResult(edges=[self._row_to_edge(r) for r in rows])
|
|
808
|
-
return EdgeQueryResult(rows=[
|
|
881
|
+
return EdgeQueryResult(rows=[decode_projected_row(keys, r, EDGE_STRUCTURAL_KEYS) for r in rows])
|
|
809
882
|
|
|
810
883
|
|
|
811
884
|
def raw_query(
|
|
@@ -843,7 +916,39 @@ def integrity_check(self) -> Dict[str, Any]: # {"ok": bool, "errors": [...], "b
|
|
|
843
916
|
return {"ok": ok, "errors": [] if ok else rows, "backend": "sqlite"}
|
|
844
917
|
|
|
845
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
|
+
|
|
846
949
|
SQLiteGraphCapability.query_nodes = query_nodes
|
|
847
950
|
SQLiteGraphCapability.query_edges = query_edges
|
|
848
951
|
SQLiteGraphCapability.raw_query = raw_query
|
|
849
952
|
SQLiteGraphCapability.integrity_check = integrity_check
|
|
953
|
+
SQLiteGraphCapability.record_ingest_sources = record_ingest_sources
|
|
954
|
+
SQLiteGraphCapability.ingest_sources = ingest_sources
|
|
@@ -22,9 +22,14 @@ 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).
|
|
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)."""
|
|
28
33
|
|
|
29
34
|
import json
|
|
30
35
|
import re
|
|
@@ -230,11 +235,14 @@ def translate_node_query(
|
|
|
230
235
|
if name == "label":
|
|
231
236
|
select_parts.append("n.label")
|
|
232
237
|
keys.append("label")
|
|
238
|
+
elif name in ("created_at", "updated_at"):
|
|
239
|
+
select_parts.append("n." + name) # storage stamp, not a property
|
|
240
|
+
keys.append(name)
|
|
233
241
|
elif name == "sources":
|
|
234
242
|
select_parts.append("n.sources")
|
|
235
243
|
keys.append("sources")
|
|
236
244
|
else:
|
|
237
|
-
select_parts.append(f"
|
|
245
|
+
select_parts.append(f"n.properties -> {_json_path(name)}")
|
|
238
246
|
keys.append(name)
|
|
239
247
|
tail = _order_limit_sql(q, "n.properties", params)
|
|
240
248
|
return (f"SELECT {', '.join(select_parts)} FROM nodes AS n{where_sql}{tail}",
|
|
@@ -302,7 +310,7 @@ def translate_edge_query(
|
|
|
302
310
|
select_parts.append("e.relation_type")
|
|
303
311
|
keys.append("relation_type")
|
|
304
312
|
else:
|
|
305
|
-
select_parts.append(f"
|
|
313
|
+
select_parts.append(f"e.properties -> {_json_path(name)}")
|
|
306
314
|
keys.append(name)
|
|
307
315
|
tail = _order_limit_sql(q, "e.properties", params)
|
|
308
316
|
return (f"SELECT {', '.join(select_parts)} FROM edges AS e{where_sql}{tail}",
|
|
@@ -311,3 +319,21 @@ def translate_edge_query(
|
|
|
311
319
|
tail = _order_limit_sql(q, "e.properties", params)
|
|
312
320
|
return (f"SELECT {EDGE_FULL_COLUMNS} FROM edges AS e{where_sql}{tail}",
|
|
313
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.26 → cjm_capability_graph_sqlite-0.0.29}/tests/test_capability.py
RENAMED
|
@@ -177,3 +177,111 @@ 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)
|
|
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,33 @@ 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
|
+
|
|
107
|
+
def test_structural_timestamp_projection(con):
|
|
108
|
+
# created_at/updated_at project from the storage COLUMNS (the stamps),
|
|
109
|
+
# not json_extract over properties — no Segment carries them as props.
|
|
110
|
+
rows = run(con, NodeQuery(label="Segment",
|
|
111
|
+
related=RelationPredicate("PART_OF", node_id="doc-1"),
|
|
112
|
+
order_by=OrderBy(prop="index"),
|
|
113
|
+
project=["index", "created_at", "updated_at"]))
|
|
114
|
+
assert [r["created_at"] for r in rows] == [1.0, 1.0, 1.0, 1.0]
|
|
115
|
+
assert rows[0]["updated_at"] == 1.0
|
|
116
|
+
assert rows[0]["id"] == "s0"
|
|
117
|
+
|
|
118
|
+
|
|
88
119
|
def test_empty_filter_or_case_two_counts(con):
|
|
89
120
|
empty_eq = run(con, NodeQuery(label="Segment", where=[PropertyPredicate("text", "eq", "")],
|
|
90
121
|
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
|