Add incremental adapter compiler boundary
This commit is contained in:
parent
82b3b90521
commit
696b62f9f8
20 changed files with 1592 additions and 122 deletions
|
|
@ -13,7 +13,15 @@ from contextlib import contextmanager
|
|||
from pathlib import Path
|
||||
|
||||
from .errors import DocForgeError
|
||||
from .models import Edge, Node, ProjectService, ProjectSnapshot
|
||||
from .models import (
|
||||
BuildReportingProject,
|
||||
Edge,
|
||||
IncrementalStateProject,
|
||||
Node,
|
||||
ProjectService,
|
||||
ProjectSnapshot,
|
||||
ProjectState,
|
||||
)
|
||||
from .project import project_root_fingerprint
|
||||
|
||||
INDEX_SCHEMA_VERSION = 1
|
||||
|
|
@ -86,6 +94,11 @@ class ProjectIndex:
|
|||
def build(self) -> dict[str, object]:
|
||||
snapshot = self.project.load()
|
||||
status = _status(snapshot)
|
||||
build_report = (
|
||||
self.project.build_report() if isinstance(self.project, BuildReportingProject) else None
|
||||
)
|
||||
if build_report is not None and build_report.get("mode") != "incremental":
|
||||
build_report = None
|
||||
cache_root = snapshot.descriptor.cache_root
|
||||
cache_root.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
|
|
@ -183,9 +196,16 @@ class ProjectIndex:
|
|||
except Exception:
|
||||
temporary.unlink(missing_ok=True)
|
||||
raise
|
||||
return {**status, "database": str(self.path)}
|
||||
result: dict[str, object] = {**status, "database": str(self.path)}
|
||||
if build_report is not None:
|
||||
result["build"] = build_report
|
||||
return result
|
||||
|
||||
def check(self) -> dict[str, object]:
|
||||
if isinstance(self.project, IncrementalStateProject):
|
||||
state = self.project.incremental_state()
|
||||
if state is not None:
|
||||
return self._check_incremental_state(state)
|
||||
snapshot = self.project.load()
|
||||
expected = _status(snapshot)
|
||||
with _read_connection(self.path) as connection:
|
||||
|
|
@ -233,6 +253,64 @@ class ProjectIndex:
|
|||
raise DocForgeError("invalid_index", "Derived index rows do not match source")
|
||||
return {**expected, "database": str(self.path)}
|
||||
|
||||
def _check_incremental_state(self, state: ProjectState) -> dict[str, object]:
|
||||
"""Validate a published index against cheap current source identity."""
|
||||
|
||||
descriptor = self.project.descriptor
|
||||
identity = {
|
||||
"project_id": descriptor.project_id,
|
||||
"project_root_fingerprint": project_root_fingerprint(descriptor.root),
|
||||
"revision": state.revision,
|
||||
"source_hash": state.source_hash,
|
||||
"index_schema_version": INDEX_SCHEMA_VERSION,
|
||||
"adapter": descriptor.adapter,
|
||||
}
|
||||
with _read_connection(self.path) as connection:
|
||||
application_id = connection.execute("PRAGMA application_id").fetchone()[0]
|
||||
schema_version = connection.execute("PRAGMA user_version").fetchone()[0]
|
||||
if application_id != APPLICATION_ID or schema_version != INDEX_SCHEMA_VERSION:
|
||||
raise DocForgeError("invalid_index", "Derived index has an unsupported schema")
|
||||
metadata = dict(connection.execute("SELECT key, value FROM metadata"))
|
||||
for key, expected in identity.items():
|
||||
if metadata.get(key) != str(expected):
|
||||
raise DocForgeError(
|
||||
"stale_index", "Derived index does not match canonical source", field=key
|
||||
)
|
||||
integrity = connection.execute("PRAGMA integrity_check").fetchone()
|
||||
if integrity is None or integrity[0] != "ok":
|
||||
raise DocForgeError("invalid_index", "Derived index failed SQLite integrity check")
|
||||
indexed_nodes = tuple(
|
||||
_row_to_node(row)
|
||||
for row in connection.execute("SELECT * FROM nodes ORDER BY node_id")
|
||||
)
|
||||
indexed_edges = tuple(
|
||||
Edge(*row)
|
||||
for row in connection.execute(
|
||||
"SELECT source_id, relation, target_id FROM edges "
|
||||
"ORDER BY source_id, relation, target_id"
|
||||
)
|
||||
)
|
||||
node_hash = _node_hash(indexed_nodes)
|
||||
edge_hash = _edge_hash(indexed_edges)
|
||||
fts_count = connection.execute("SELECT COUNT(*) FROM node_fts").fetchone()[0]
|
||||
if (
|
||||
metadata.get("node_hash") != node_hash
|
||||
or metadata.get("edge_hash") != edge_hash
|
||||
or metadata.get("node_count") != str(len(indexed_nodes))
|
||||
or metadata.get("edge_count") != str(len(indexed_edges))
|
||||
or fts_count != len(indexed_nodes)
|
||||
):
|
||||
raise DocForgeError("invalid_index", "Derived index rows do not match metadata")
|
||||
return {
|
||||
**identity,
|
||||
"node_hash": node_hash,
|
||||
"node_count": len(indexed_nodes),
|
||||
"edge_hash": edge_hash,
|
||||
"edge_count": len(indexed_edges),
|
||||
"status": "ok",
|
||||
"database": str(self.path),
|
||||
}
|
||||
|
||||
def get_node(self, node_id: str) -> dict[str, object]:
|
||||
checked = self.check()
|
||||
with _read_connection(self.path) as connection:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue