relationship persistence, context entities, idempotent asset creation, audit/version handling for relationship changes

This commit is contained in:
2026-05-06 02:09:23 +02:00
parent bf59087073
commit 286ebc3cb6
12 changed files with 651 additions and 24 deletions

View File

@@ -12,10 +12,14 @@ from kontextual_engine.core import (
AssetRepresentation,
AssetVersion,
AuditEvent,
ContextEntity,
CoreRelationship,
IdempotencyRecord,
KnowledgeAsset,
LifecycleState,
MetadataRecord,
RepresentationKind,
RelationshipTargetKind,
)
from kontextual_engine.errors import NotFoundError, ValidationError
@@ -183,6 +187,90 @@ class SQLiteAssetRegistryRepository:
self.get_asset(asset_id)
return [MetadataRecord.from_dict(_loads(row["payload"])) for row in rows]
def save_context_entity(self, entity: ContextEntity) -> ContextEntity:
with self._connect() as conn:
conn.execute(
"""
insert into context_entities (id, entity_type, name, payload)
values (?, ?, ?, ?)
on conflict(id) do update set
entity_type=excluded.entity_type,
name=excluded.name,
payload=excluded.payload
""",
(entity.entity_id, entity.entity_type.value, entity.name, _json(entity.to_dict())),
)
return entity
def get_context_entity(self, entity_id: str) -> ContextEntity:
row = self._one("select payload from context_entities where id = ?", (entity_id,))
if row is None:
raise NotFoundError("Context entity not found", details={"entity_id": entity_id})
return ContextEntity.from_dict(_loads(row["payload"]))
def list_context_entities(self) -> list[ContextEntity]:
rows = self._all("select payload from context_entities order by entity_type, name, id", ())
return [ContextEntity.from_dict(_loads(row["payload"])) for row in rows]
def save_relationship(self, relationship: CoreRelationship) -> CoreRelationship:
self.get_asset(relationship.source_id)
if relationship.target_kind == RelationshipTargetKind.ASSET:
self.get_asset(relationship.target_id)
else:
self.get_context_entity(relationship.target_id)
with self._connect() as conn:
conn.execute(
"""
insert into core_relationships (id, source_id, target_id, target_kind, predicate, payload)
values (?, ?, ?, ?, ?, ?)
on conflict(id) do update set
source_id=excluded.source_id,
target_id=excluded.target_id,
target_kind=excluded.target_kind,
predicate=excluded.predicate,
payload=excluded.payload
""",
(
relationship.relationship_id,
relationship.source_id,
relationship.target_id,
relationship.target_kind.value,
relationship.predicate,
_json(relationship.to_dict()),
),
)
return relationship
def get_relationship(self, relationship_id: str) -> CoreRelationship:
row = self._one("select payload from core_relationships where id = ?", (relationship_id,))
if row is None:
raise NotFoundError(
"Relationship not found",
details={"relationship_id": relationship_id},
)
return CoreRelationship.from_dict(_loads(row["payload"]))
def list_relationships(
self,
*,
source_id: str | None = None,
target_id: str | None = None,
) -> list[CoreRelationship]:
clauses = []
params: list[Any] = []
if source_id is not None:
clauses.append("source_id = ?")
params.append(source_id)
if target_id is not None:
clauses.append("target_id = ?")
params.append(target_id)
where = f" where {' and '.join(clauses)}" if clauses else ""
rows = self._all(
f"select payload from core_relationships{where} order by source_id, target_id, predicate, id",
tuple(params),
)
return [CoreRelationship.from_dict(_loads(row["payload"])) for row in rows]
def save_version(self, version: AssetVersion) -> AssetVersion:
try:
with self._connect() as conn:
@@ -241,6 +329,12 @@ class SQLiteAssetRegistryRepository:
)
return event
def get_audit_event(self, event_id: str) -> AuditEvent:
row = self._one("select payload from audit_events where id = ?", (event_id,))
if row is None:
raise NotFoundError("Audit event not found", details={"event_id": event_id})
return AuditEvent.from_dict(_loads(row["payload"]))
def list_audit_events(
self,
*,
@@ -259,6 +353,34 @@ class SQLiteAssetRegistryRepository:
rows = self._all(f"select payload from audit_events{where} order by occurred_at, id", tuple(params))
return [AuditEvent.from_dict(_loads(row["payload"])) for row in rows]
def save_idempotency_record(self, record: IdempotencyRecord) -> IdempotencyRecord:
with self._connect() as conn:
conn.execute(
"""
insert into idempotency_records (key, operation, request_hash, status, payload)
values (?, ?, ?, ?, ?)
on conflict(key) do update set
operation=excluded.operation,
request_hash=excluded.request_hash,
status=excluded.status,
payload=excluded.payload
""",
(
record.key,
record.operation,
record.request_hash,
record.status.value,
_json(record.to_dict()),
),
)
return record
def get_idempotency_record(self, key: str) -> IdempotencyRecord | None:
row = self._one("select payload from idempotency_records where key = ?", (key,))
if row is None:
return None
return IdempotencyRecord.from_dict(_loads(row["payload"]))
def _initialize(self) -> None:
with self._connect() as conn:
conn.executescript(
@@ -288,6 +410,20 @@ class SQLiteAssetRegistryRepository:
key text not null,
payload text not null
);
create table if not exists context_entities (
id text primary key,
entity_type text not null,
name text not null,
payload text not null
);
create table if not exists core_relationships (
id text primary key,
source_id text not null references assets(id) on delete cascade,
target_id text not null,
target_kind text not null,
predicate text not null,
payload text not null
);
create table if not exists asset_versions (
id text primary key,
asset_id text not null references assets(id) on delete cascade,
@@ -306,9 +442,19 @@ class SQLiteAssetRegistryRepository:
payload text not null,
foreign key(actor_id) references actors(id)
);
create table if not exists idempotency_records (
key text primary key,
operation text not null,
request_hash text not null,
status text not null,
payload text not null
);
create index if not exists idx_assets_lifecycle on assets(lifecycle);
create index if not exists idx_representations_asset on representations(asset_id);
create index if not exists idx_metadata_asset on metadata_records(asset_id);
create index if not exists idx_entities_type on context_entities(entity_type);
create index if not exists idx_relationships_source on core_relationships(source_id);
create index if not exists idx_relationships_target on core_relationships(target_id);
create index if not exists idx_versions_asset on asset_versions(asset_id);
create index if not exists idx_audit_target on audit_events(target);
create index if not exists idx_audit_correlation on audit_events(correlation_id);