aevum-store-postgres 0.2.0__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.
@@ -0,0 +1,31 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .venv/
7
+ *.egg-info/
8
+
9
+ # Build
10
+ dist/
11
+ build/
12
+
13
+ # Tools
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ .pytest_cache/
17
+ .hypothesis/
18
+
19
+ # IDE
20
+ .vscode/
21
+ .idea/
22
+ *.swp
23
+ *.swo
24
+
25
+ # OS
26
+ .DS_Store
27
+ Thumbs.db
28
+
29
+ # Verify scripts (run locally, never commit)
30
+ verify_phase*.py
31
+ scripts/verify_phase*.py
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: aevum-store-postgres
3
+ Version: 0.2.0
4
+ Summary: Aevum — PostgreSQL GraphStore + ConsentLedger backend (team deployments).
5
+ Project-URL: Homepage, https://aevum.build
6
+ Project-URL: Repository, https://github.com/aevum-labs/aevum
7
+ License: Apache-2.0
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: aevum-core
17
+ Requires-Dist: psycopg[binary]<4.0,>=3.1
18
+ Description-Content-Type: text/markdown
19
+
20
+ # aevum-store-postgres
21
+
22
+ PostgreSQL-backed graph store for Aevum. Suitable for team deployments with shared state, concurrent writers, and durable persistence.
23
+
24
+ ```bash
25
+ pip install aevum-store-postgres
26
+ ```
27
+
28
+ ```python
29
+ import psycopg
30
+ from aevum.core import Engine
31
+ from aevum.store.postgres import PostgresStore
32
+ from aevum.store.postgres.store import initialize_schema
33
+
34
+ conn = psycopg.connect("postgresql://user:pass@localhost/aevum")
35
+ initialize_schema(conn)
36
+ engine = Engine(graph_store=PostgresStore(conn))
37
+ ```
38
+
39
+ For single-node deployments without PostgreSQL, use `aevum-store-oxigraph` instead.
40
+ See the [main repository README](https://github.com/aevum-labs/aevum) for backend selection guidance.
@@ -0,0 +1,21 @@
1
+ # aevum-store-postgres
2
+
3
+ PostgreSQL-backed graph store for Aevum. Suitable for team deployments with shared state, concurrent writers, and durable persistence.
4
+
5
+ ```bash
6
+ pip install aevum-store-postgres
7
+ ```
8
+
9
+ ```python
10
+ import psycopg
11
+ from aevum.core import Engine
12
+ from aevum.store.postgres import PostgresStore
13
+ from aevum.store.postgres.store import initialize_schema
14
+
15
+ conn = psycopg.connect("postgresql://user:pass@localhost/aevum")
16
+ initialize_schema(conn)
17
+ engine = Engine(graph_store=PostgresStore(conn))
18
+ ```
19
+
20
+ For single-node deployments without PostgreSQL, use `aevum-store-oxigraph` instead.
21
+ See the [main repository README](https://github.com/aevum-labs/aevum) for backend selection guidance.
@@ -0,0 +1,60 @@
1
+ [project]
2
+ name = "aevum-store-postgres"
3
+ version = "0.2.0"
4
+ description = "Aevum — PostgreSQL GraphStore + ConsentLedger backend (team deployments)."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "Apache-2.0" }
8
+ classifiers = [
9
+ "Development Status :: 3 - Alpha",
10
+ "Intended Audience :: Developers",
11
+ "License :: OSI Approved :: Apache Software License",
12
+ "Programming Language :: Python :: 3.11",
13
+ "Programming Language :: Python :: 3.12",
14
+ "Programming Language :: Python :: 3.13",
15
+ "Typing :: Typed",
16
+ ]
17
+ dependencies = [
18
+ "aevum-core",
19
+ "psycopg[binary]>=3.1,<4.0",
20
+ ]
21
+
22
+ [project.scripts]
23
+ aevum-store-migrate = "aevum.store.postgres.migrate:main"
24
+
25
+ [project.urls]
26
+ Homepage = "https://aevum.build"
27
+ Repository = "https://github.com/aevum-labs/aevum"
28
+
29
+ [build-system]
30
+ requires = ["hatchling"]
31
+ build-backend = "hatchling.build"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/aevum"]
35
+
36
+ [tool.uv.sources]
37
+ aevum-core = { workspace = true }
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
41
+ asyncio_mode = "auto"
42
+ addopts = "--tb=short"
43
+ pythonpath = ["src", "tests"]
44
+
45
+ [tool.mypy]
46
+ strict = true
47
+ python_version = "3.11"
48
+ mypy_path = "src"
49
+ explicit_package_bases = true
50
+ ignore_missing_imports = true
51
+
52
+ [tool.ruff]
53
+ line-length = 130
54
+
55
+ [tool.ruff.lint]
56
+ select = ["E", "F", "UP", "B", "SIM", "I", "ANN"]
57
+ ignore = ["ANN401"]
58
+
59
+ [tool.ruff.lint.per-file-ignores]
60
+ "tests/**" = ["ANN"]
@@ -0,0 +1,30 @@
1
+ """
2
+ aevum.store.postgres -- PostgreSQL backends for graph, consent, and ledger.
3
+
4
+ Usage:
5
+ from aevum.store.postgres import PostgresStore, PostgresConsentLedger, PostgresLedger
6
+ from aevum.store.postgres.ledger import initialize_ledger_schema
7
+ import psycopg
8
+
9
+ conn = psycopg.connect("postgresql://user:pass@host/dbname")
10
+ initialize_schema(conn) # graph + consent DDL
11
+ initialize_ledger_schema(conn) # ledger DDL
12
+
13
+ store = PostgresStore(conn)
14
+ consent = PostgresConsentLedger(conn)
15
+ ledger = PostgresLedger(conn, sigchain)
16
+
17
+ engine = Engine(
18
+ graph_store=store,
19
+ consent_ledger=consent,
20
+ ledger=ledger,
21
+ )
22
+ """
23
+
24
+ from aevum.store.postgres.consent import PostgresConsentLedger
25
+ from aevum.store.postgres.ledger import PostgresLedger
26
+ from aevum.store.postgres.store import PostgresStore
27
+
28
+ __version__ = "0.1.0"
29
+
30
+ __all__ = ["PostgresStore", "PostgresConsentLedger", "PostgresLedger"]
@@ -0,0 +1,110 @@
1
+ """
2
+ PostgresConsentLedger — ConsentLedgerProtocol backed by PostgreSQL.
3
+
4
+ Grants stored as JSONB in aevum_consent_grants.
5
+ OR-Set semantics: revocation wins over concurrent grants.
6
+ Expiration checked in Python (matching InMemoryConsentLedger behaviour).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import contextlib
12
+ import json
13
+ import threading
14
+ from datetime import UTC, datetime
15
+ from typing import Any
16
+
17
+ from aevum.core.consent.models import ConsentGrant
18
+
19
+
20
+ class PostgresConsentLedger:
21
+ """
22
+ ConsentLedgerProtocol implementation backed by PostgreSQL.
23
+
24
+ Args:
25
+ conn: An open psycopg.Connection (autocommit=True recommended).
26
+ lock: Optional shared Lock. Pass the same lock used by PostgresStore
27
+ when both share one connection.
28
+ """
29
+
30
+ def __init__(self, conn: Any, lock: threading.Lock | None = None) -> None:
31
+ self._conn = conn
32
+ self._lock = lock or threading.Lock()
33
+
34
+ # ── ConsentLedgerProtocol ──────────────────────────────────────────────────
35
+
36
+ def add_grant(self, grant: ConsentGrant) -> None:
37
+ """Upsert a consent grant. Replaces any prior record with the same grant_id."""
38
+ sql = """
39
+ INSERT INTO aevum_consent_grants (grant_id, grant_data)
40
+ VALUES (%s, %s::jsonb)
41
+ ON CONFLICT (grant_id) DO UPDATE SET grant_data = EXCLUDED.grant_data
42
+ """
43
+ with self._lock, self._conn.cursor() as cur:
44
+ cur.execute(sql, (grant.grant_id, json.dumps(grant.model_dump())))
45
+
46
+ def revoke_grant(self, grant_id: str) -> None:
47
+ """Mark a grant as revoked (immutable update via JSONB merge)."""
48
+ sql = """
49
+ UPDATE aevum_consent_grants
50
+ SET grant_data = jsonb_set(grant_data, '{revocation_status}', '"revoked"')
51
+ WHERE grant_id = %s
52
+ """
53
+ with self._lock, self._conn.cursor() as cur:
54
+ cur.execute(sql, (grant_id,))
55
+
56
+ def has_consent(
57
+ self,
58
+ *,
59
+ subject_id: str,
60
+ operation: str,
61
+ grantee_id: str,
62
+ purpose: str | None = None,
63
+ ) -> bool:
64
+ """
65
+ Return True if an active, unexpired grant covers this operation.
66
+
67
+ Mirrors InMemoryConsentLedger: loads candidates from DB, checks
68
+ expiration and operation in Python to stay consistent with the spec.
69
+ """
70
+ sql = """
71
+ SELECT grant_data
72
+ FROM aevum_consent_grants
73
+ WHERE grant_data->>'subject_id' = %s
74
+ AND grant_data->>'grantee_id' = %s
75
+ AND grant_data->>'revocation_status' = 'active'
76
+ """
77
+ with self._lock, self._conn.cursor() as cur:
78
+ cur.execute(sql, (subject_id, grantee_id))
79
+ rows = cur.fetchall()
80
+
81
+ now = datetime.now(UTC)
82
+ for row in rows:
83
+ raw = row[0]
84
+ d: dict[str, Any] = json.loads(raw) if isinstance(raw, str) else raw
85
+ if operation not in d.get("operations", []):
86
+ continue
87
+ try:
88
+ expires = datetime.fromisoformat(
89
+ d["expires_at"].replace("Z", "+00:00")
90
+ )
91
+ if now > expires:
92
+ continue
93
+ except (KeyError, ValueError):
94
+ continue
95
+ return True
96
+ return False
97
+
98
+ def all_grants(self) -> list[ConsentGrant]:
99
+ """Return all stored grants (active, revoked, and expired)."""
100
+ sql = "SELECT grant_data FROM aevum_consent_grants"
101
+ with self._lock, self._conn.cursor() as cur:
102
+ cur.execute(sql)
103
+ rows = cur.fetchall()
104
+ grants: list[ConsentGrant] = []
105
+ for row in rows:
106
+ raw = row[0]
107
+ d: dict[str, Any] = json.loads(raw) if isinstance(raw, str) else raw
108
+ with contextlib.suppress(Exception):
109
+ grants.append(ConsentGrant(**d))
110
+ return grants
@@ -0,0 +1,198 @@
1
+ """
2
+ PostgresLedger -- persistent episodic ledger backed by PostgreSQL.
3
+
4
+ AuditEvents are serialized as JSONB. Sigchain verification still works --
5
+ verify_chain() reads all events in sequence order from Postgres.
6
+
7
+ Barrier 4 (Audit Immutability) enforced by __delitem__ and __setitem__.
8
+ The database table is INSERT-only -- no UPDATE or DELETE ever issued.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import threading
15
+ from typing import Any
16
+
17
+ from aevum.core.audit.event import AuditEvent
18
+ from aevum.core.audit.sigchain import Sigchain
19
+ from aevum.core.exceptions import BarrierViolationError, ReplayNotFoundError
20
+
21
+ _DDL_LEDGER = """
22
+ CREATE TABLE IF NOT EXISTS aevum_ledger (
23
+ sequence BIGSERIAL PRIMARY KEY,
24
+ event_id TEXT NOT NULL UNIQUE,
25
+ audit_id TEXT NOT NULL UNIQUE,
26
+ event_type TEXT NOT NULL,
27
+ actor TEXT NOT NULL,
28
+ system_time BIGINT NOT NULL,
29
+ episode_id TEXT,
30
+ causation_id TEXT,
31
+ correlation_id TEXT,
32
+ prior_hash TEXT NOT NULL,
33
+ payload_hash TEXT NOT NULL,
34
+ signature TEXT NOT NULL,
35
+ signer_key_id TEXT NOT NULL,
36
+ schema_version TEXT NOT NULL DEFAULT '1.0',
37
+ valid_from TEXT NOT NULL,
38
+ valid_to TEXT,
39
+ trace_id TEXT,
40
+ span_id TEXT,
41
+ payload JSONB NOT NULL
42
+ );
43
+ CREATE INDEX IF NOT EXISTS idx_aevum_ledger_audit_id ON aevum_ledger (audit_id);
44
+ CREATE INDEX IF NOT EXISTS idx_aevum_ledger_sequence ON aevum_ledger (sequence);
45
+ """
46
+
47
+
48
+ def initialize_ledger_schema(conn: Any) -> None:
49
+ """Create the aevum_ledger table if it does not exist."""
50
+ with conn.cursor() as cur:
51
+ cur.execute(_DDL_LEDGER)
52
+ conn.commit()
53
+
54
+
55
+ def _event_to_row(event: AuditEvent) -> dict[str, Any]:
56
+ return {
57
+ "event_id": event.event_id,
58
+ "audit_id": event.audit_id(),
59
+ "event_type": event.event_type,
60
+ "actor": event.actor,
61
+ "system_time": event.system_time,
62
+ "episode_id": event.episode_id,
63
+ "causation_id": event.causation_id,
64
+ "correlation_id": event.correlation_id,
65
+ "prior_hash": event.prior_hash,
66
+ "payload_hash": event.payload_hash,
67
+ "signature": event.signature,
68
+ "signer_key_id": event.signer_key_id,
69
+ "schema_version": event.schema_version,
70
+ "valid_from": event.valid_from,
71
+ "valid_to": event.valid_to,
72
+ "trace_id": event.trace_id,
73
+ "span_id": event.span_id,
74
+ "payload": json.dumps(event.payload, default=str),
75
+ }
76
+
77
+
78
+ def _row_to_event(row: dict[str, Any]) -> AuditEvent:
79
+ payload = row["payload"]
80
+ if isinstance(payload, str):
81
+ payload = json.loads(payload)
82
+ return AuditEvent(
83
+ event_id=row["event_id"],
84
+ episode_id=row.get("episode_id", ""),
85
+ sequence=row["sequence"],
86
+ event_type=row["event_type"],
87
+ schema_version=row.get("schema_version", "1.0"),
88
+ valid_from=row["valid_from"],
89
+ valid_to=row.get("valid_to"),
90
+ system_time=row["system_time"],
91
+ causation_id=row.get("causation_id"),
92
+ correlation_id=row.get("correlation_id"),
93
+ actor=row["actor"],
94
+ trace_id=row.get("trace_id"),
95
+ span_id=row.get("span_id"),
96
+ payload=payload,
97
+ payload_hash=row["payload_hash"],
98
+ prior_hash=row["prior_hash"],
99
+ signature=row["signature"],
100
+ signer_key_id=row["signer_key_id"],
101
+ )
102
+
103
+
104
+ class PostgresLedger:
105
+ """
106
+ Persistent episodic ledger backed by PostgreSQL.
107
+
108
+ Thread-safe. INSERT-only (Barrier 4 at application layer).
109
+ Pass a shared threading.Lock if sharing a connection with PostgresStore.
110
+ """
111
+
112
+ def __init__(
113
+ self,
114
+ conn: Any,
115
+ sigchain: Sigchain,
116
+ lock: threading.Lock | None = None,
117
+ ) -> None:
118
+ self._conn = conn
119
+ self._sigchain = sigchain
120
+ self._lock = lock or threading.Lock()
121
+
122
+ def append(
123
+ self,
124
+ *,
125
+ event_type: str,
126
+ payload: dict[str, Any],
127
+ actor: str,
128
+ episode_id: str | None = None,
129
+ causation_id: str | None = None,
130
+ correlation_id: str | None = None,
131
+ ) -> AuditEvent:
132
+ with self._lock:
133
+ event = self._sigchain.new_event(
134
+ event_type=event_type,
135
+ payload=payload,
136
+ actor=actor,
137
+ episode_id=episode_id,
138
+ causation_id=causation_id,
139
+ correlation_id=correlation_id,
140
+ )
141
+ row = _event_to_row(event)
142
+ with self._conn.cursor() as cur:
143
+ cur.execute(
144
+ """
145
+ INSERT INTO aevum_ledger (
146
+ event_id, audit_id, event_type, actor, system_time,
147
+ episode_id, causation_id, correlation_id,
148
+ prior_hash, payload_hash, signature, signer_key_id,
149
+ schema_version, valid_from, valid_to,
150
+ trace_id, span_id, payload
151
+ ) VALUES (
152
+ %(event_id)s, %(audit_id)s, %(event_type)s, %(actor)s,
153
+ %(system_time)s, %(episode_id)s, %(causation_id)s,
154
+ %(correlation_id)s, %(prior_hash)s, %(payload_hash)s,
155
+ %(signature)s, %(signer_key_id)s, %(schema_version)s,
156
+ %(valid_from)s, %(valid_to)s, %(trace_id)s,
157
+ %(span_id)s, %(payload)s::jsonb
158
+ )
159
+ """,
160
+ row,
161
+ )
162
+ self._conn.commit()
163
+ return event
164
+
165
+ def get(self, audit_id: str) -> AuditEvent:
166
+ from psycopg.rows import dict_row
167
+ with self._lock, self._conn.cursor(row_factory=dict_row) as cur:
168
+ cur.execute(
169
+ "SELECT * FROM aevum_ledger WHERE audit_id = %s",
170
+ (audit_id,),
171
+ )
172
+ row = cur.fetchone()
173
+ if row is None:
174
+ raise ReplayNotFoundError(f"No ledger entry for {audit_id!r}")
175
+ return _row_to_event(row)
176
+
177
+ def all_events(self) -> list[AuditEvent]:
178
+ from psycopg.rows import dict_row
179
+ with self._lock, self._conn.cursor(row_factory=dict_row) as cur:
180
+ cur.execute("SELECT * FROM aevum_ledger ORDER BY sequence ASC")
181
+ rows = cur.fetchall()
182
+ return [_row_to_event(r) for r in rows]
183
+
184
+ def count(self) -> int:
185
+ with self._lock, self._conn.cursor() as cur:
186
+ cur.execute("SELECT COUNT(*) FROM aevum_ledger")
187
+ result = cur.fetchone()
188
+ return result[0] if result else 0
189
+
190
+ def __delitem__(self, key: object) -> None:
191
+ raise BarrierViolationError(
192
+ "Attempted to delete a ledger entry -- Barrier 4 (Audit Immutability) violated."
193
+ )
194
+
195
+ def __setitem__(self, key: object, value: object) -> None:
196
+ raise BarrierViolationError(
197
+ "Attempted to overwrite a ledger entry -- Barrier 4 (Audit Immutability) violated."
198
+ )
@@ -0,0 +1,134 @@
1
+ """
2
+ migrate_from_oxigraph — export Oxigraph data and import into PostgresStore.
3
+
4
+ Usage:
5
+ from aevum.store.postgres.migrate import migrate_from_oxigraph
6
+ stats = migrate_from_oxigraph(
7
+ source_store=oxigraph_store,
8
+ target_store=postgres_store,
9
+ source_consent=in_memory_ledger, # optional
10
+ target_consent=postgres_consent, # optional
11
+ )
12
+ print(stats) # {"entities": 42, "grants": 5}
13
+
14
+ CLI (after installation):
15
+ aevum-store-migrate --source-dsn "" --target-dsn "postgresql://..."
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import sys
22
+ from typing import TYPE_CHECKING, Any
23
+
24
+ if TYPE_CHECKING:
25
+ from aevum.store.postgres.consent import PostgresConsentLedger
26
+ from aevum.store.postgres.store import PostgresStore
27
+
28
+
29
+ def migrate_from_oxigraph(
30
+ source_store: Any,
31
+ target_store: PostgresStore,
32
+ source_consent: Any | None = None,
33
+ target_consent: PostgresConsentLedger | None = None,
34
+ ) -> dict[str, int]:
35
+ """
36
+ Copy all entities and consent grants from an OxigraphStore to PostgresStore.
37
+
38
+ Args:
39
+ source_store: An OxigraphStore instance (must have sparql_select,
40
+ get_entity, get_entity_classification).
41
+ target_store: The destination PostgresStore.
42
+ source_consent: Optional ConsentLedger to migrate grants from.
43
+ target_consent: Optional PostgresConsentLedger to migrate grants into.
44
+
45
+ Returns:
46
+ dict with counts: {"entities": <n>, "grants": <n>}
47
+ """
48
+ entities_migrated = 0
49
+ grants_migrated = 0
50
+
51
+ # --- Migrate entities ---
52
+ rows = source_store.sparql_select(
53
+ "SELECT DISTINCT ?s WHERE { GRAPH <urn:aevum:knowledge> { ?s ?p ?o } }"
54
+ )
55
+ for row in rows:
56
+ iri: str = row.get("s", "")
57
+ if not iri:
58
+ continue
59
+ # Strip namespace prefix added by OxigraphStore._entity_node
60
+ entity_id = iri[len("urn:aevum:entity:"):] if iri.startswith("urn:aevum:entity:") else iri
61
+
62
+ data = source_store.get_entity(entity_id)
63
+ if data is None:
64
+ # Try with the full IRI (entity_id was stored as IRI)
65
+ data = source_store.get_entity(iri)
66
+ if data is None:
67
+ continue
68
+
69
+ classification = source_store.get_entity_classification(entity_id)
70
+ target_store.store_entity(entity_id, data, classification)
71
+ entities_migrated += 1
72
+
73
+ # --- Migrate consent grants ---
74
+ if source_consent is not None and target_consent is not None:
75
+ for grant in source_consent.all_grants():
76
+ target_consent.add_grant(grant)
77
+ grants_migrated += 1
78
+
79
+ return {"entities": entities_migrated, "grants": grants_migrated}
80
+
81
+
82
+ def main() -> None:
83
+ """CLI entry point for aevum-store-migrate."""
84
+ parser = argparse.ArgumentParser(
85
+ description="Migrate Aevum data from Oxigraph to PostgreSQL"
86
+ )
87
+ parser.add_argument(
88
+ "--source-path",
89
+ default=None,
90
+ help="Path to Oxigraph store directory (omit for in-memory — useful only for testing)",
91
+ )
92
+ parser.add_argument(
93
+ "--target-dsn",
94
+ required=True,
95
+ help="PostgreSQL DSN (e.g. postgresql://user:pass@localhost/aevum)",
96
+ )
97
+ args = parser.parse_args()
98
+
99
+ try:
100
+ import psycopg
101
+ except ImportError:
102
+ print("psycopg is required: pip install psycopg[binary]", file=sys.stderr)
103
+ sys.exit(1)
104
+
105
+ try:
106
+ from aevum.store.oxigraph import OxigraphStore
107
+ except ImportError:
108
+ print("aevum-store-oxigraph is required for migration", file=sys.stderr)
109
+ sys.exit(1)
110
+
111
+ from aevum.store.postgres import PostgresConsentLedger, PostgresStore
112
+ from aevum.store.postgres.schema import initialize_schema
113
+
114
+ source = OxigraphStore(args.source_path)
115
+ conn = psycopg.connect(args.target_dsn, autocommit=True)
116
+ initialize_schema(conn)
117
+
118
+ import threading
119
+ lock = threading.Lock()
120
+ target = PostgresStore(conn, lock)
121
+ target_consent = PostgresConsentLedger(conn, lock)
122
+
123
+ stats = migrate_from_oxigraph(
124
+ source_store=source,
125
+ target_store=target,
126
+ source_consent=None,
127
+ target_consent=target_consent,
128
+ )
129
+ conn.close()
130
+ print(f"Migration complete: {stats['entities']} entities, {stats['grants']} grants")
131
+
132
+
133
+ if __name__ == "__main__":
134
+ main()
@@ -0,0 +1,33 @@
1
+ """
2
+ DDL for the Aevum PostgreSQL schema.
3
+
4
+ Two tables:
5
+ aevum_entities — entity data + classification (GraphStore backend)
6
+ aevum_consent_grants — consent grants as JSONB (ConsentLedger backend)
7
+
8
+ Call initialize_schema(conn) once after connecting.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+ _DDL = """\
16
+ CREATE TABLE IF NOT EXISTS aevum_entities (
17
+ entity_id TEXT PRIMARY KEY,
18
+ data JSONB NOT NULL,
19
+ classification INT NOT NULL DEFAULT 0,
20
+ ingested_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
21
+ );
22
+
23
+ CREATE TABLE IF NOT EXISTS aevum_consent_grants (
24
+ grant_id TEXT PRIMARY KEY,
25
+ grant_data JSONB NOT NULL
26
+ );
27
+ """
28
+
29
+
30
+ def initialize_schema(conn: Any) -> None:
31
+ """Create Aevum tables if they do not already exist."""
32
+ with conn.cursor() as cur:
33
+ cur.execute(_DDL)