aevum-store-postgres 0.2.0__tar.gz → 0.4.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.
Files changed (17) hide show
  1. aevum_store_postgres-0.4.0/.gitignore +49 -0
  2. {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.4.0}/PKG-INFO +1 -1
  3. {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.4.0}/pyproject.toml +1 -1
  4. {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.4.0}/src/aevum/store/postgres/__init__.py +1 -1
  5. {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.4.0}/src/aevum/store/postgres/ledger.py +113 -20
  6. {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.4.0}/tests/test_ledger.py +79 -0
  7. aevum_store_postgres-0.2.0/.gitignore +0 -31
  8. {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.4.0}/README.md +0 -0
  9. {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.4.0}/src/aevum/store/postgres/consent.py +0 -0
  10. {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.4.0}/src/aevum/store/postgres/migrate.py +0 -0
  11. {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.4.0}/src/aevum/store/postgres/py.typed +0 -0
  12. {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.4.0}/src/aevum/store/postgres/schema.py +0 -0
  13. {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.4.0}/src/aevum/store/postgres/store.py +0 -0
  14. {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.4.0}/tests/conftest.py +0 -0
  15. {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.4.0}/tests/test_pg_consent.py +0 -0
  16. {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.4.0}/tests/test_pg_migrate.py +0 -0
  17. {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.4.0}/tests/test_pg_store.py +0 -0
@@ -0,0 +1,49 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .venv/
7
+ *.egg-info/
8
+
9
+ # Build
10
+ dist/
11
+ build/
12
+ site/
13
+
14
+ # Tools
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ .pytest_cache/
18
+ .hypothesis/
19
+ .cache/
20
+
21
+ # IDE
22
+ .vscode/
23
+ .idea/
24
+ *.swp
25
+ *.swo
26
+
27
+ # OS
28
+ .DS_Store
29
+ Thumbs.db
30
+
31
+ # Verify scripts (run locally, never commit)
32
+ verify_*.py
33
+ scripts/verify_*.py
34
+
35
+ # Aevum development — never commit (Phase 0+)
36
+ aevum_principles.key
37
+ signed_principles_draft.yaml
38
+ tools/sign_principles.py
39
+
40
+ # Private keys — never commit
41
+ *.key
42
+ *.pem
43
+
44
+ # OpenSSF Scorecard output (Phase 0+)
45
+ results.sarif
46
+ verify_phase3.py
47
+ verify_phase7.py
48
+ verify_phase8.py
49
+ verify_phase*.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aevum-store-postgres
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Aevum — PostgreSQL GraphStore + ConsentLedger backend (team deployments).
5
5
  Project-URL: Homepage, https://aevum.build
6
6
  Project-URL: Repository, https://github.com/aevum-labs/aevum
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aevum-store-postgres"
3
- version = "0.2.0"
3
+ version = "0.4.0"
4
4
  description = "Aevum — PostgreSQL GraphStore + ConsentLedger backend (team deployments)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -25,6 +25,6 @@ from aevum.store.postgres.consent import PostgresConsentLedger
25
25
  from aevum.store.postgres.ledger import PostgresLedger
26
26
  from aevum.store.postgres.store import PostgresStore
27
27
 
28
- __version__ = "0.1.0"
28
+ __version__ = "0.4.0"
29
29
 
30
30
  __all__ = ["PostgresStore", "PostgresConsentLedger", "PostgresLedger"]
@@ -11,6 +11,7 @@ The database table is INSERT-only -- no UPDATE or DELETE ever issued.
11
11
  from __future__ import annotations
12
12
 
13
13
  import json
14
+ import logging
14
15
  import threading
15
16
  from typing import Any
16
17
 
@@ -118,6 +119,59 @@ class PostgresLedger:
118
119
  self._conn = conn
119
120
  self._sigchain = sigchain
120
121
  self._lock = lock or threading.Lock()
122
+ self._resume_chain_from_db()
123
+
124
+ def _resume_chain_from_db(self) -> None:
125
+ """
126
+ Seed the in-memory sigchain state from the last persisted event.
127
+
128
+ Without this, every Engine restart begins a new chain at
129
+ sequence=1 / prior_hash=GENESIS_HASH, silently forking the chain.
130
+ After this fix, the sigchain continues from where the last process left off.
131
+
132
+ Idempotent — safe to call on a fresh database (no-op if table empty).
133
+ """
134
+ from psycopg.rows import dict_row
135
+
136
+ log = logging.getLogger(__name__)
137
+ try:
138
+ with self._lock, self._conn.cursor(row_factory=dict_row) as cur:
139
+ cur.execute(
140
+ """
141
+ SELECT sequence, event_id, audit_id,
142
+ event_type, actor, system_time,
143
+ episode_id, causation_id, correlation_id,
144
+ prior_hash, payload_hash, signature,
145
+ signer_key_id, schema_version,
146
+ valid_from, valid_to,
147
+ trace_id, span_id, payload
148
+ FROM aevum_ledger
149
+ ORDER BY sequence DESC
150
+ LIMIT 1
151
+ """
152
+ )
153
+ row = cur.fetchone()
154
+ except Exception as exc:
155
+ log.debug(
156
+ "aevum-store-postgres: could not resume chain from DB (%s) "
157
+ "— starting from genesis. Normal on a fresh database.",
158
+ exc,
159
+ )
160
+ return
161
+
162
+ if row is None:
163
+ return # Fresh database — genesis is correct
164
+
165
+ last_event = _row_to_event(row)
166
+ continuation_hash = AuditEvent.hash_event_for_chain(last_event)
167
+ self._sigchain.restore((last_event.sequence, continuation_hash))
168
+
169
+ log.debug(
170
+ "aevum-store-postgres: resumed chain from DB "
171
+ "— sequence=%d prior_hash=%s…",
172
+ last_event.sequence,
173
+ continuation_hash[:12],
174
+ )
121
175
 
122
176
  def append(
123
177
  self,
@@ -130,6 +184,10 @@ class PostgresLedger:
130
184
  correlation_id: str | None = None,
131
185
  ) -> AuditEvent:
132
186
  with self._lock:
187
+ # Save sigchain state before advancing it.
188
+ # If the INSERT fails, restore prevents the chain from
189
+ # chaining from a ghost event that was never persisted.
190
+ checkpoint = self._sigchain.checkpoint()
133
191
  event = self._sigchain.new_event(
134
192
  event_type=event_type,
135
193
  payload=payload,
@@ -139,29 +197,44 @@ class PostgresLedger:
139
197
  correlation_id=correlation_id,
140
198
  )
141
199
  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
200
+ try:
201
+ with self._conn.cursor() as cur:
202
+ cur.execute(
203
+ """
204
+ INSERT INTO aevum_ledger (
205
+ event_id, audit_id, event_type, actor, system_time,
206
+ episode_id, causation_id, correlation_id,
207
+ prior_hash, payload_hash, signature, signer_key_id,
208
+ schema_version, valid_from, valid_to,
209
+ trace_id, span_id, payload
210
+ ) VALUES (
211
+ %(event_id)s, %(audit_id)s, %(event_type)s, %(actor)s,
212
+ %(system_time)s, %(episode_id)s, %(causation_id)s,
213
+ %(correlation_id)s, %(prior_hash)s, %(payload_hash)s,
214
+ %(signature)s, %(signer_key_id)s, %(schema_version)s,
215
+ %(valid_from)s, %(valid_to)s, %(trace_id)s,
216
+ %(span_id)s, %(payload)s::jsonb
217
+ )
218
+ """,
219
+ row,
158
220
  )
159
- """,
160
- row,
161
- )
162
- self._conn.commit()
221
+ self._conn.commit()
222
+ except Exception:
223
+ self._sigchain.restore(checkpoint)
224
+ raise
163
225
  return event
164
226
 
227
+ def last_audit_id(self) -> str | None:
228
+ """Return the audit_id of the most recently appended event, or None."""
229
+ from psycopg.rows import dict_row
230
+
231
+ with self._lock, self._conn.cursor(row_factory=dict_row) as cur:
232
+ cur.execute(
233
+ "SELECT audit_id FROM aevum_ledger ORDER BY sequence DESC LIMIT 1"
234
+ )
235
+ row = cur.fetchone()
236
+ return row["audit_id"] if row else None
237
+
165
238
  def get(self, audit_id: str) -> AuditEvent:
166
239
  from psycopg.rows import dict_row
167
240
  with self._lock, self._conn.cursor(row_factory=dict_row) as cur:
@@ -187,6 +260,26 @@ class PostgresLedger:
187
260
  result = cur.fetchone()
188
261
  return result[0] if result else 0
189
262
 
263
+ def max_sequence_for_subjects(self, subject_ids: list[str]) -> int:
264
+ """
265
+ Return the highest sequence number among all ingest.accepted events
266
+ whose payload subject_id is in subject_ids. Returns 0 if none found.
267
+ """
268
+ if not subject_ids:
269
+ return 0
270
+ with self._lock, self._conn.cursor() as cur:
271
+ cur.execute(
272
+ """
273
+ SELECT COALESCE(MAX(sequence), 0)
274
+ FROM aevum_ledger
275
+ WHERE event_type = 'ingest.accepted'
276
+ AND payload->>'subject_id' = ANY(%s)
277
+ """,
278
+ (subject_ids,),
279
+ )
280
+ result = cur.fetchone()
281
+ return int(result[0]) if result else 0
282
+
190
283
  def __delitem__(self, key: object) -> None:
191
284
  raise BarrierViolationError(
192
285
  "Attempted to delete a ledger entry -- Barrier 4 (Audit Immutability) violated."
@@ -56,6 +56,18 @@ class FakeCursor:
56
56
  matches = [r for r in self._conn._rows if r.get("audit_id") == audit_id]
57
57
  self._last_result = [list(r.values()) for r in matches]
58
58
  self.description = [[k] for k in (matches[0].keys() if matches else [])]
59
+ elif "order by sequence desc" in sql_lower and "limit 1" in sql_lower:
60
+ if self._conn._rows:
61
+ last_row = max(self._conn._rows, key=lambda r: r.get("sequence", 0))
62
+ if "select audit_id" in sql_lower:
63
+ self._last_result = [[last_row.get("audit_id", "")]]
64
+ self.description = [["audit_id"]]
65
+ else:
66
+ self._last_result = [list(last_row.values())]
67
+ self.description = [[k] for k in last_row]
68
+ else:
69
+ self._last_result = []
70
+ self.description = []
59
71
  elif "order by sequence" in sql_lower:
60
72
  sorted_rows = sorted(self._conn._rows, key=lambda r: r.get("sequence", 0))
61
73
  self._last_result = [list(r.values()) for r in sorted_rows]
@@ -120,6 +132,73 @@ class TestPostgresLedger:
120
132
  assert "payload" in row
121
133
 
122
134
 
135
+ def test_rollback_on_write_failure() -> None:
136
+ """Failed INSERT must not corrupt in-memory sigchain state."""
137
+ from aevum.core.audit.sigchain import GENESIS_HASH
138
+
139
+ class ExplodingCursor:
140
+ description: list = []
141
+ def __enter__(self): return self
142
+ def __exit__(self, *a): pass
143
+ def execute(self, sql: str, params=None) -> None:
144
+ if "INSERT" in sql.upper():
145
+ raise RuntimeError("simulated disk full")
146
+ def fetchone(self): return None
147
+ def fetchall(self): return []
148
+
149
+ class ExplodingConn:
150
+ _rows: list = []
151
+ _sequence: int = 0
152
+ def cursor(self, row_factory=None): return ExplodingCursor()
153
+ def commit(self): pass
154
+
155
+ sc = Sigchain()
156
+ ledger = PostgresLedger(ExplodingConn(), sc)
157
+
158
+ with pytest.raises(RuntimeError):
159
+ ledger.append(event_type="will.fail", payload={}, actor="a")
160
+
161
+ assert sc._sequence == 0, f"rollback failed: _sequence={sc._sequence}"
162
+ assert sc._prior_hash == GENESIS_HASH, "rollback failed: prior_hash changed"
163
+
164
+
165
+ def test_chain_continuity_across_restart() -> None:
166
+ """New PostgresLedger on existing data must continue the chain."""
167
+ conn = FakeConn()
168
+ sc1 = Sigchain()
169
+ ledger1 = PostgresLedger(conn, sc1)
170
+ for i in range(3):
171
+ ledger1.append(event_type=f"s1.{i}", payload={"i": i}, actor="a")
172
+ assert sc1._sequence == 3
173
+
174
+ # Simulate restart: new sigchain + new ledger on same conn
175
+ sc2 = Sigchain()
176
+ assert sc2._sequence == 0 # starts fresh
177
+
178
+ ledger2 = PostgresLedger(conn, sc2) # _resume_chain_from_db fires
179
+
180
+ assert sc2._sequence == 3, (
181
+ f"Expected sequence=3 after resume, got {sc2._sequence}. "
182
+ "Chain fork: each restart should CONTINUE, not start over."
183
+ )
184
+
185
+ e = ledger2.append(event_type="s2.first", payload={}, actor="b")
186
+ assert e.sequence == 4, f"Expected sequence=4, got {e.sequence}"
187
+
188
+
189
+ def test_last_audit_id_empty() -> None:
190
+ conn = FakeConn()
191
+ ledger = PostgresLedger(conn, Sigchain())
192
+ assert ledger.last_audit_id() is None
193
+
194
+
195
+ def test_last_audit_id_after_append() -> None:
196
+ conn = FakeConn()
197
+ ledger = PostgresLedger(conn, Sigchain())
198
+ e = ledger.append(event_type="test", payload={}, actor="a")
199
+ assert ledger.last_audit_id() == e.audit_id()
200
+
201
+
123
202
  # Integration tests -- require a real Postgres database
124
203
  _POSTGRES_DSN = os.environ.get("AEVUM_TEST_POSTGRES_DSN")
125
204
 
@@ -1,31 +0,0 @@
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