cjm-capability-graph-sqlite 0.0.30__tar.gz → 0.0.31__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.
Files changed (17) hide show
  1. {cjm_capability_graph_sqlite-0.0.30/cjm_capability_graph_sqlite.egg-info → cjm_capability_graph_sqlite-0.0.31}/PKG-INFO +1 -1
  2. cjm_capability_graph_sqlite-0.0.31/cjm_capability_graph_sqlite/__init__.py +1 -0
  3. {cjm_capability_graph_sqlite-0.0.30 → cjm_capability_graph_sqlite-0.0.31}/cjm_capability_graph_sqlite/capability.py +34 -8
  4. {cjm_capability_graph_sqlite-0.0.30 → cjm_capability_graph_sqlite-0.0.31/cjm_capability_graph_sqlite.egg-info}/PKG-INFO +1 -1
  5. {cjm_capability_graph_sqlite-0.0.30 → cjm_capability_graph_sqlite-0.0.31}/tests/test_capability.py +24 -2
  6. cjm_capability_graph_sqlite-0.0.30/cjm_capability_graph_sqlite/__init__.py +0 -1
  7. {cjm_capability_graph_sqlite-0.0.30 → cjm_capability_graph_sqlite-0.0.31}/LICENSE +0 -0
  8. {cjm_capability_graph_sqlite-0.0.30 → cjm_capability_graph_sqlite-0.0.31}/MANIFEST.in +0 -0
  9. {cjm_capability_graph_sqlite-0.0.30 → cjm_capability_graph_sqlite-0.0.31}/README.md +0 -0
  10. {cjm_capability_graph_sqlite-0.0.30 → cjm_capability_graph_sqlite-0.0.31}/cjm_capability_graph_sqlite/query_translation.py +0 -0
  11. {cjm_capability_graph_sqlite-0.0.30 → cjm_capability_graph_sqlite-0.0.31}/cjm_capability_graph_sqlite.egg-info/SOURCES.txt +0 -0
  12. {cjm_capability_graph_sqlite-0.0.30 → cjm_capability_graph_sqlite-0.0.31}/cjm_capability_graph_sqlite.egg-info/dependency_links.txt +0 -0
  13. {cjm_capability_graph_sqlite-0.0.30 → cjm_capability_graph_sqlite-0.0.31}/cjm_capability_graph_sqlite.egg-info/requires.txt +0 -0
  14. {cjm_capability_graph_sqlite-0.0.30 → cjm_capability_graph_sqlite-0.0.31}/cjm_capability_graph_sqlite.egg-info/top_level.txt +0 -0
  15. {cjm_capability_graph_sqlite-0.0.30 → cjm_capability_graph_sqlite-0.0.31}/pyproject.toml +0 -0
  16. {cjm_capability_graph_sqlite-0.0.30 → cjm_capability_graph_sqlite-0.0.31}/setup.cfg +0 -0
  17. {cjm_capability_graph_sqlite-0.0.30 → cjm_capability_graph_sqlite-0.0.31}/tests/test_query_translation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjm-capability-graph-sqlite
3
- Version: 0.0.30
3
+ Version: 0.0.31
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.31"
@@ -235,7 +235,7 @@ class SQLiteGraphCapability(ToolCapability):
235
235
  # correlated-probe plans pick the WRONG composite index (the
236
236
  # target-side probe scans a whole document's PART_OF rows per
237
237
  # outer row: 5s; source-side probe: 8ms at corpus scale). They are
238
- # kept by `_writing`'s PRAGMA optimize at the close of every write,
238
+ # kept by `_writing`'s statistics rule at the close of every write,
239
239
  # this one included (a new index is analyzed there) -- no longer a
240
240
  # full ANALYZE per open, O(db) on every read (finding ff3ede3d).
241
241
 
@@ -261,22 +261,48 @@ class SQLiteGraphCapability(ToolCapability):
261
261
  error), then keep the planner's statistics current, then close.
262
262
 
263
263
  Statistics are written on the paths that change data, never on an open (finding
264
- ff3ede3d: a read paid a full ANALYZE and took the write lock). PRAGMA optimize
265
- analyzes a table only when it was never analyzed, gained an index, or moved ~10x
266
- in size since, so a steady-state write pays a no-op check and a fresh rebuild db
267
- gains statistics as it fills. Guarded: statistics are an OPTIMIZATION — a corrupted
268
- DB must still answer integrity_check (the designed G3 gate)."""
264
+ ff3ede3d: a read paid a full ANALYZE and took the write lock): `_refresh_statistics`
265
+ analyzes a table only when its statistics are missing or stale, so a steady-state write
266
+ pays a ~0.01 ms check and a fresh rebuild db gains statistics as it fills. Guarded:
267
+ statistics are an OPTIMIZATION — a corrupted DB must still answer integrity_check (the
268
+ designed G3 gate)."""
269
269
  con = self._connect()
270
270
  try:
271
271
  with con:
272
272
  yield con
273
273
  try:
274
- con.execute("PRAGMA optimize=0x10002;")
274
+ self._refresh_statistics(con)
275
275
  except sqlite3.DatabaseError as e:
276
- logging.warning(f"PRAGMA optimize skipped (run integrity_check): {e}")
276
+ logging.warning(f"statistics skipped (run integrity_check): {e}")
277
277
  finally:
278
278
  con.close()
279
279
 
280
+ def _refresh_statistics(
281
+ self,
282
+ con: sqlite3.Connection, # A write connection, after its unit of work committed
283
+ ) -> None:
284
+ """Analyze each graph table whose planner statistics are missing or stale: an index
285
+ with no sqlite_stat1 row (never analyzed, or created since), or a size ~10x the
286
+ analyzed one (max(rowid), an O(log n) estimate). One rule on every SQLite: PRAGMA
287
+ optimize analyzes a never-analyzed table only from SQLite 3.46 on, so on 3.45 (CI's
288
+ Ubuntu 24.04) the write path kept no statistics at all -- the guard of the 162 s
289
+ planner regression gone."""
290
+ for table in ("nodes", "edges"):
291
+ rows = con.execute(f"SELECT max(rowid) FROM {table}").fetchone()[0] or 0
292
+ if not rows:
293
+ continue
294
+ indexes = {r[0] for r in con.execute(
295
+ "SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = ?", (table,))}
296
+ try:
297
+ stats = dict(con.execute(
298
+ "SELECT idx, stat FROM sqlite_stat1 WHERE tbl = ?", (table,)).fetchall())
299
+ except sqlite3.OperationalError: # no sqlite_stat1 yet: never analyzed
300
+ stats = {}
301
+ analyzed = max((int(str(s).split()[0]) for s in stats.values() if s), default=0)
302
+ if indexes - set(stats) or rows >= 10 * max(analyzed, 1):
303
+ con.execute(f"ANALYZE {table};")
304
+ con.commit()
305
+
280
306
  # -------------------------------------------------------------------------
281
307
  # Helpers
282
308
  # -------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjm-capability-graph-sqlite
3
- Version: 0.0.30
3
+ Version: 0.0.31
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
@@ -140,8 +140,8 @@ def test_sg41_wal_query_guard_merge_readonly(tmp_path):
140
140
 
141
141
 
142
142
  def test_reads_and_opens_never_write_writes_keep_statistics(tmp_path):
143
- # Finding ff3ede3d: statistics are written by the paths that change data (PRAGMA
144
- # optimize at the close of each write), so an open or a read commits nothing -- an
143
+ # Finding ff3ede3d: statistics are written by the paths that change data (the
144
+ # statistics rule at the close of each write), so an open or a read commits nothing -- an
145
145
  # observer's data_version moves only when ANOTHER connection commits.
146
146
  db = str(tmp_path / "stats.db")
147
147
  w = SQLiteGraphCapability()
@@ -167,6 +167,28 @@ def test_reads_and_opens_never_write_writes_keep_statistics(tmp_path):
167
167
  observer.close()
168
168
 
169
169
 
170
+ def test_statistics_follow_growth_on_every_sqlite(tmp_path):
171
+ # The write path's own rule, never PRAGMA optimize's version-specific one (SQLite < 3.46
172
+ # never analyzed a fresh table, so CI kept no statistics): the first write analyzes, a
173
+ # write that leaves the table ~10x its analyzed size re-analyzes, a small one does not.
174
+ db = str(tmp_path / "grow.db")
175
+ cap = SQLiteGraphCapability()
176
+ cap.initialize({"db_path": db})
177
+
178
+ def analyzed_rows():
179
+ stats = sqlite3.connect(db).execute(
180
+ "SELECT stat FROM sqlite_stat1 WHERE tbl = 'nodes' AND idx = 'idx_nodes_label_id'").fetchone()
181
+ return int(stats[0].split()[0])
182
+
183
+ cap.add_nodes([GraphNode(id=f"a{i:03d}", label="Doc", properties={}) for i in range(5)])
184
+ assert analyzed_rows() == 5
185
+ cap.add_nodes([GraphNode(id=f"b{i:03d}", label="Doc", properties={}) for i in range(10)])
186
+ assert analyzed_rows() == 5 # 15 rows: under 10x, the statistics stand
187
+ cap.add_nodes([GraphNode(id=f"c{i:03d}", label="Doc", properties={}) for i in range(40)])
188
+ assert analyzed_rows() == 55 # 55 rows: 10x the analyzed 5, re-analyzed
189
+ cap.cleanup()
190
+
191
+
170
192
  def test_typed_surface_smoke(cap):
171
193
  doc = str(uuid.uuid4())
172
194
  segs = [str(uuid.uuid4()) for _ in range(3)]
@@ -1 +0,0 @@
1
- __version__ = "0.0.30"