nl2data-semantic-catalog-postgres 0.1.0__py3-none-any.whl

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 (34) hide show
  1. nl2data_semantic_catalog_postgres/__init__.py +18 -0
  2. nl2data_semantic_catalog_postgres/client.py +139 -0
  3. nl2data_semantic_catalog_postgres/config.py +136 -0
  4. nl2data_semantic_catalog_postgres/envelope.py +327 -0
  5. nl2data_semantic_catalog_postgres/errors.py +188 -0
  6. nl2data_semantic_catalog_postgres/fake_postgres/__init__.py +53 -0
  7. nl2data_semantic_catalog_postgres/fake_postgres/driver.py +209 -0
  8. nl2data_semantic_catalog_postgres/fake_postgres/handlers_audit.py +243 -0
  9. nl2data_semantic_catalog_postgres/fake_postgres/handlers_drafts.py +84 -0
  10. nl2data_semantic_catalog_postgres/fake_postgres/handlers_maintenance.py +263 -0
  11. nl2data_semantic_catalog_postgres/fake_postgres/handlers_publications.py +249 -0
  12. nl2data_semantic_catalog_postgres/fake_postgres/handlers_schema.py +23 -0
  13. nl2data_semantic_catalog_postgres/fake_postgres/handlers_snapshots.py +197 -0
  14. nl2data_semantic_catalog_postgres/fake_postgres/handlers_versions.py +330 -0
  15. nl2data_semantic_catalog_postgres/fake_postgres/keys.py +99 -0
  16. nl2data_semantic_catalog_postgres/fake_postgres/pool.py +152 -0
  17. nl2data_semantic_catalog_postgres/fake_postgres/registry.py +144 -0
  18. nl2data_semantic_catalog_postgres/maintenance.py +235 -0
  19. nl2data_semantic_catalog_postgres/py.typed +0 -0
  20. nl2data_semantic_catalog_postgres/repositories/__init__.py +26 -0
  21. nl2data_semantic_catalog_postgres/repositories/activation.py +849 -0
  22. nl2data_semantic_catalog_postgres/repositories/audit_evidence.py +221 -0
  23. nl2data_semantic_catalog_postgres/repositories/drafts.py +156 -0
  24. nl2data_semantic_catalog_postgres/repositories/evidence.py +415 -0
  25. nl2data_semantic_catalog_postgres/repositories/publications.py +486 -0
  26. nl2data_semantic_catalog_postgres/repositories/snapshots.py +372 -0
  27. nl2data_semantic_catalog_postgres/schema.py +337 -0
  28. nl2data_semantic_catalog_postgres/sql.py +520 -0
  29. nl2data_semantic_catalog_postgres/store.py +716 -0
  30. nl2data_semantic_catalog_postgres/unit_of_work.py +589 -0
  31. nl2data_semantic_catalog_postgres-0.1.0.dist-info/METADATA +76 -0
  32. nl2data_semantic_catalog_postgres-0.1.0.dist-info/RECORD +34 -0
  33. nl2data_semantic_catalog_postgres-0.1.0.dist-info/WHEEL +5 -0
  34. nl2data_semantic_catalog_postgres-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,84 @@
1
+ """Assembly-draft statement handlers for the fake pool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from .driver import _as_dt
8
+ from .keys import _draft_key, _lock_or_fail
9
+
10
+ if TYPE_CHECKING:
11
+ from .driver import _FakeConnection
12
+ from .pool import FakePostgresPool
13
+
14
+
15
+ def _h_insert_assembly_draft(
16
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
17
+ ) -> tuple[list[dict[str, Any]], int]:
18
+ namespace, draft_id = params[0], params[1]
19
+ key = _draft_key(namespace, draft_id)
20
+ _lock_or_fail(pool, conn, key, timeout)
21
+ if (namespace, draft_id) in pool.assembly_drafts:
22
+ return ([], 0)
23
+ conn._touch(key)
24
+ pool.assembly_drafts[(namespace, draft_id)] = {
25
+ "scope_namespace": namespace,
26
+ "draft_id": draft_id,
27
+ "bundle_id": params[2],
28
+ "source_id": params[3],
29
+ "draft_revision": params[4],
30
+ "state": params[5],
31
+ "schema_version": params[6],
32
+ "envelope": params[7],
33
+ "updated_at": _as_dt(params[8]),
34
+ }
35
+ return ([], 1)
36
+
37
+
38
+ def _h_read_assembly_draft(
39
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
40
+ ) -> tuple[list[dict[str, Any]], int]:
41
+ row = pool.assembly_drafts.get((params[0], params[1]))
42
+ if row is None:
43
+ return ([], 0)
44
+ return ([{
45
+ "envelope": row["envelope"],
46
+ "schema_version": row["schema_version"],
47
+ "draft_revision": row["draft_revision"],
48
+ }], 0)
49
+
50
+
51
+ def _h_lock_assembly_draft(
52
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
53
+ ) -> tuple[list[dict[str, Any]], int]:
54
+ row = pool.assembly_drafts.get((params[0], params[1]))
55
+ if row is None:
56
+ return ([], 0)
57
+ _lock_or_fail(pool, conn, _draft_key(params[0], params[1]), timeout)
58
+ return ([{
59
+ "envelope": row["envelope"],
60
+ "schema_version": row["schema_version"],
61
+ "draft_revision": row["draft_revision"],
62
+ }], 0)
63
+
64
+
65
+ def _h_replace_assembly_draft(
66
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
67
+ ) -> tuple[list[dict[str, Any]], int]:
68
+ namespace, draft_id, expected_revision = params[7], params[8], params[9]
69
+ key = _draft_key(namespace, draft_id)
70
+ _lock_or_fail(pool, conn, key, timeout)
71
+ row = pool.assembly_drafts.get((namespace, draft_id))
72
+ if row is None or row["draft_revision"] != expected_revision:
73
+ return ([], 0)
74
+ conn._touch(key)
75
+ row.update(
76
+ bundle_id=params[0],
77
+ source_id=params[1],
78
+ draft_revision=params[2],
79
+ state=params[3],
80
+ schema_version=params[4],
81
+ envelope=params[5],
82
+ updated_at=_as_dt(params[6]),
83
+ )
84
+ return ([], 1)
@@ -0,0 +1,263 @@
1
+ """Lifecycle-event and retention-cleanup statement handlers.
2
+
3
+ The cleanup handlers mirror the real SQL's protection semantics: active
4
+ pointer targets, history entries, and required dependencies are never
5
+ removed, and removal happens in bounded ordered batches.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from .driver import _as_dt
14
+ from .keys import (
15
+ _audit_entry_key,
16
+ _event_key,
17
+ _lock_or_fail,
18
+ _publication_key,
19
+ _snap_key,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from .driver import _FakeConnection
24
+ from .pool import FakePostgresPool
25
+
26
+
27
+ def _h_insert_event(
28
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
29
+ ) -> tuple[list[dict[str, Any]], int]:
30
+ namespace, event_id = params[0], params[1]
31
+ key = _event_key(namespace, event_id)
32
+ # Lock the key slot before checking existence so concurrent inserts of
33
+ # the same event serialize like the real unique index (second one no-ops).
34
+ _lock_or_fail(pool, conn, key, timeout)
35
+ if (namespace, event_id) in pool.events:
36
+ return ([], 0)
37
+ conn._touch(key)
38
+ pool.events[(namespace, event_id)] = {
39
+ "scope_namespace": namespace,
40
+ "event_id": event_id,
41
+ "kind": params[2],
42
+ "member_id": params[3],
43
+ "schema_version": params[4],
44
+ "payload": params[5],
45
+ "occurred_at": _as_dt(params[6]),
46
+ }
47
+ return ([], 1)
48
+
49
+
50
+ def _active_bundle_fingerprints(
51
+ pool: FakePostgresPool, active: set[tuple[str, str, str]]
52
+ ) -> set[str]:
53
+ """Catalog/snapshot fingerprints referenced by active bundle envelopes.
54
+
55
+ Mirrors the SQL ``referenced_catalog_fingerprints`` CTE: descriptor
56
+ catalog fingerprints, source catalog fingerprints, and compatibility
57
+ fingerprints of publications currently selected by a bundle pointer.
58
+ """
59
+ referenced: set[str] = set()
60
+ for row in pool.publications.values():
61
+ if (
62
+ row["scope_namespace"], row["bundle_id"], row["model_version"]
63
+ ) not in active:
64
+ continue
65
+ try:
66
+ envelope = json.loads(row["envelope"])
67
+ except ValueError as error:
68
+ raise ValueError(
69
+ "invalid envelope json in bundle_publications"
70
+ ) from error
71
+ if envelope.get("kind") != "bundle":
72
+ continue
73
+ payload = envelope.get("payload") or {}
74
+ descriptor = payload.get("descriptor") or {}
75
+ fingerprint = descriptor.get("catalog_fingerprint")
76
+ if fingerprint:
77
+ referenced.add(fingerprint)
78
+ for source in payload.get("sources") or []:
79
+ fingerprint = source.get("catalog_fingerprint")
80
+ if fingerprint:
81
+ referenced.add(fingerprint)
82
+ compatibility = payload.get("compatibility") or {}
83
+ referenced.update(
84
+ compatibility.get("compatible_catalog_fingerprints") or []
85
+ )
86
+ return referenced
87
+
88
+
89
+ def _snapshot_required_by(row: dict[str, Any], referenced: set[str]) -> bool:
90
+ """True when an active bundle references this snapshot row.
91
+
92
+ Bundles reference a snapshot either by the snapshot's own fingerprint
93
+ (``descriptor.catalog_fingerprint``) or by the source catalog
94
+ fingerprint the snapshot documents (``payload.source.catalog_fingerprint``).
95
+ A row without a source catalog fingerprint is conservatively retained,
96
+ mirroring the SQL ``NOT IN`` semantics that never match NULL.
97
+ """
98
+ try:
99
+ envelope = json.loads(row["envelope"])
100
+ except ValueError as error:
101
+ raise ValueError(
102
+ "invalid envelope json in metadata_snapshots"
103
+ ) from error
104
+ source = (envelope.get("payload") or {}).get("source") or {}
105
+ fingerprint = source.get("catalog_fingerprint")
106
+ return fingerprint is None or fingerprint in referenced
107
+
108
+
109
+ def _h_delete_expired_snapshots(
110
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
111
+ ) -> tuple[list[dict[str, Any]], int]:
112
+ current = _as_dt(params[0])
113
+ limit = int(params[1])
114
+ pointed = {
115
+ row["snapshot_fingerprint"] for row in pool.snapshot_pointers.values()
116
+ }
117
+ active = {
118
+ (row["scope_namespace"], row["bundle_id"], row["model_version"])
119
+ for row in pool.bundle_pointers.values()
120
+ }
121
+ referenced = _active_bundle_fingerprints(pool, active)
122
+ candidates = [
123
+ ((row["retained_until"], fingerprint), (namespace, fingerprint))
124
+ for (namespace, fingerprint), row in pool.snapshots.items()
125
+ if row["retained_until"] is not None
126
+ and row["retained_until"] < current
127
+ and fingerprint not in pointed
128
+ and fingerprint not in referenced
129
+ and not _snapshot_required_by(row, referenced)
130
+ ]
131
+ removed = 0
132
+ for _, key in sorted(candidates)[:limit]:
133
+ _lock_or_fail(pool, conn, _snap_key(*key), timeout)
134
+ row = pool.snapshots.get(key)
135
+ if row is None:
136
+ continue
137
+ conn._touch(_snap_key(*key))
138
+ del pool.snapshots[key]
139
+ removed += 1
140
+ return ([], removed)
141
+
142
+
143
+ def _h_delete_expired_publications(
144
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
145
+ ) -> tuple[list[dict[str, Any]], int]:
146
+ current = _as_dt(params[0])
147
+ limit = int(params[1])
148
+ protected = {
149
+ (row["scope_namespace"], row["bundle_id"], row["model_version"])
150
+ for row in pool.bundle_pointers.values()
151
+ } | {
152
+ (row["scope_namespace"], row["bundle_id"], row["model_version"])
153
+ for history in pool.bundle_history.values()
154
+ for row in history.values()
155
+ }
156
+ for pub_row in pool.publications.values():
157
+ try:
158
+ envelope = json.loads(pub_row["envelope"])
159
+ except ValueError as error:
160
+ raise ValueError(
161
+ "invalid envelope json in bundle_publications"
162
+ ) from error
163
+ if envelope.get("kind") != "bundle":
164
+ continue
165
+ for dependency in (envelope.get("payload") or {}).get(
166
+ "dependencies"
167
+ ) or []:
168
+ protected.add(
169
+ (
170
+ pub_row["scope_namespace"],
171
+ dependency.get("bundle_id"),
172
+ dependency.get("version"),
173
+ )
174
+ )
175
+ candidates = [
176
+ (
177
+ (row["published_at"], bundle_id, version),
178
+ (namespace, bundle_id, version),
179
+ )
180
+ for (namespace, bundle_id, version), row in pool.publications.items()
181
+ if row["published_at"] < current
182
+ and (namespace, bundle_id, version) not in protected
183
+ ]
184
+ removed = 0
185
+ for _, key in sorted(candidates)[:limit]:
186
+ _lock_or_fail(pool, conn, _publication_key(*key), timeout)
187
+ row = pool.publications.get(key)
188
+ if row is None:
189
+ continue
190
+ conn._touch(_publication_key(*key))
191
+ del pool.publications[key]
192
+ removed += 1
193
+ return ([], removed)
194
+
195
+
196
+ def _h_delete_expired_events(
197
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
198
+ ) -> tuple[list[dict[str, Any]], int]:
199
+ cutoff = _as_dt(params[0])
200
+ limit = int(params[1])
201
+ candidates = [
202
+ ((row["occurred_at"], event_id), (namespace, event_id))
203
+ for (namespace, event_id), row in pool.events.items()
204
+ if row["occurred_at"] < cutoff
205
+ ]
206
+ removed = 0
207
+ for _, key in sorted(candidates)[:limit]:
208
+ _lock_or_fail(pool, conn, _event_key(*key), timeout)
209
+ row = pool.events.get(key)
210
+ if row is None:
211
+ continue
212
+ conn._touch(_event_key(*key))
213
+ del pool.events[key]
214
+ removed += 1
215
+ return ([], removed)
216
+
217
+
218
+ def _non_retired_audit_fingerprints(pool: FakePostgresPool) -> set[tuple[str, str]]:
219
+ """(namespace, fingerprint) pairs of non-retired published versions."""
220
+ return {
221
+ (row["scope_namespace"], row["bundle_fingerprint"])
222
+ for row in pool.published_versions.values()
223
+ if row["lifecycle_state"] != "retired"
224
+ }
225
+
226
+
227
+ def _predecessors_of_protected(
228
+ pool: FakePostgresPool, protected: set[tuple[str, str]]
229
+ ) -> set[str]:
230
+ """Event ids referenced as predecessors by protected audit entries."""
231
+ ids: set[str] = set()
232
+ for (namespace, _event_id), row in pool.audit_entries.items():
233
+ if (namespace, row["bundle_fingerprint"]) not in protected:
234
+ continue
235
+ envelope = json.loads(row["envelope"])
236
+ ids.update((envelope.get("payload") or {}).get("predecessor_event_ids") or [])
237
+ return ids
238
+
239
+
240
+ def _h_delete_expired_audit_entries(
241
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
242
+ ) -> tuple[list[dict[str, Any]], int]:
243
+ cutoff = _as_dt(params[0])
244
+ limit = int(params[1])
245
+ protected = _non_retired_audit_fingerprints(pool)
246
+ protected_ids = _predecessors_of_protected(pool, protected)
247
+ candidates = [
248
+ ((row["occurred_at"], event_id), (namespace, event_id))
249
+ for (namespace, event_id), row in pool.audit_entries.items()
250
+ if row["occurred_at"] < cutoff
251
+ and (namespace, row["bundle_fingerprint"]) not in protected
252
+ and event_id not in protected_ids
253
+ ]
254
+ removed = 0
255
+ for _, key in sorted(candidates)[:limit]:
256
+ _lock_or_fail(pool, conn, _audit_entry_key(*key), timeout)
257
+ row = pool.audit_entries.get(key)
258
+ if row is None:
259
+ continue
260
+ conn._touch(_audit_entry_key(*key))
261
+ del pool.audit_entries[key]
262
+ removed += 1
263
+ return ([], removed)
@@ -0,0 +1,249 @@
1
+ """Publication, accepted-manifest, evidence, and audit statement handlers.
2
+
3
+ Writers lock the key slot before checking existence so concurrent writers
4
+ serialize like the real unique indexes (the second one no-ops or raises
5
+ ``UniqueViolation`` exactly where the real constraint would).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from .driver import UniqueViolation, _as_dt
13
+ from .keys import (
14
+ _audit_key,
15
+ _lock_or_fail,
16
+ _manifest_key,
17
+ _publication_key,
18
+ _verification_evidence_key,
19
+ )
20
+
21
+ if TYPE_CHECKING:
22
+ from .driver import _FakeConnection
23
+ from .pool import FakePostgresPool
24
+
25
+
26
+ def _h_lock_publication_series(
27
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
28
+ ) -> tuple[list[dict[str, Any]], int]:
29
+ _lock_or_fail(pool, conn, ("publication_series", params[0], params[1]), timeout)
30
+ return ([{"pg_advisory_xact_lock": None}], 0)
31
+
32
+
33
+ def _h_insert_publication(
34
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
35
+ ) -> tuple[list[dict[str, Any]], int]:
36
+ namespace, bundle_id, version = params[0], params[1], params[2]
37
+ key = _publication_key(namespace, bundle_id, version)
38
+ # Lock the key slot before checking existence so concurrent publishes of
39
+ # the same version serialize like the real unique index (second one no-ops).
40
+ _lock_or_fail(pool, conn, key, timeout)
41
+ if (namespace, bundle_id, version) in pool.publications:
42
+ return ([], 0)
43
+ conn._touch(key)
44
+ pool.publications[(namespace, bundle_id, version)] = {
45
+ "scope_namespace": namespace,
46
+ "bundle_id": bundle_id,
47
+ "model_version": version,
48
+ "bundle_fingerprint": params[3],
49
+ "schema_version": params[4],
50
+ "envelope": params[5],
51
+ "published_at": _as_dt(params[6]),
52
+ }
53
+ return ([], 1)
54
+
55
+
56
+ def _h_read_publication(
57
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
58
+ ) -> tuple[list[dict[str, Any]], int]:
59
+ row = pool.publications.get((params[0], params[1], params[2]))
60
+ if row is None:
61
+ return ([], 0)
62
+ return (
63
+ [
64
+ {
65
+ "envelope": row["envelope"],
66
+ "schema_version": row["schema_version"],
67
+ "published_at": row["published_at"],
68
+ }
69
+ ],
70
+ 0,
71
+ )
72
+
73
+
74
+ def _h_read_publication_fingerprint(
75
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
76
+ ) -> tuple[list[dict[str, Any]], int]:
77
+ row = pool.publications.get((params[0], params[1], params[2]))
78
+ if row is None:
79
+ return ([], 0)
80
+ return ([{"bundle_fingerprint": row["bundle_fingerprint"]}], 0)
81
+
82
+
83
+ def _h_read_publication_by_fingerprint(
84
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
85
+ ) -> tuple[list[dict[str, Any]], int]:
86
+ namespace, bundle_id, fingerprint = params
87
+ for (scope, candidate_id, _version), row in pool.publications.items():
88
+ if (
89
+ scope == namespace
90
+ and candidate_id == bundle_id
91
+ and row["bundle_fingerprint"] == fingerprint
92
+ ):
93
+ return ([{
94
+ "envelope": row["envelope"],
95
+ "schema_version": row["schema_version"],
96
+ "published_at": row["published_at"],
97
+ "model_version": row["model_version"],
98
+ }], 0)
99
+ return ([], 0)
100
+
101
+
102
+ def _h_list_publications(
103
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
104
+ ) -> tuple[list[dict[str, Any]], int]:
105
+ namespace, bundle_id = params[0], params[1]
106
+ items = [
107
+ (
108
+ row["published_at"],
109
+ row["model_version"],
110
+ row["schema_version"],
111
+ row["envelope"],
112
+ )
113
+ for (ns, bid, _version), row in pool.publications.items()
114
+ if ns == namespace and bid == bundle_id
115
+ ]
116
+ items.sort() # ORDER BY published_at, model_version
117
+ rows = [
118
+ {
119
+ "envelope": envelope,
120
+ "model_version": model_version,
121
+ "schema_version": schema_version,
122
+ }
123
+ for _published_at, model_version, schema_version, envelope in items
124
+ ]
125
+ return (rows, len(rows))
126
+
127
+
128
+ def _h_insert_accepted_manifest(
129
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
130
+ ) -> tuple[list[dict[str, Any]], int]:
131
+ key = _manifest_key(params[0], params[1], params[2])
132
+ _lock_or_fail(pool, conn, key, timeout)
133
+ if key[1:] in pool.accepted_manifests:
134
+ raise UniqueViolation("accepted manifest already exists")
135
+ conn._touch(key)
136
+ pool.accepted_manifests[key[1:]] = {
137
+ "scope_namespace": params[0],
138
+ "bundle_id": params[1],
139
+ "bundle_fingerprint": params[2],
140
+ "schema_version": params[3],
141
+ "envelope": params[4],
142
+ "created_at": _as_dt(params[5]),
143
+ }
144
+ return ([], 1)
145
+
146
+
147
+ def _h_read_accepted_manifest(
148
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
149
+ ) -> tuple[list[dict[str, Any]], int]:
150
+ row = pool.accepted_manifests.get(params)
151
+ if row is None:
152
+ return ([], 0)
153
+ return ([{"envelope": row["envelope"], "schema_version": row["schema_version"]}], 0)
154
+
155
+
156
+ def _h_insert_verification_evidence(
157
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
158
+ ) -> tuple[list[dict[str, Any]], int]:
159
+ key = _verification_evidence_key(params[0], params[1], params[2])
160
+ _lock_or_fail(pool, conn, key, timeout)
161
+ if key[1:] in pool.verification_evidence:
162
+ raise UniqueViolation("verification evidence already exists")
163
+ if any(
164
+ row["scope_namespace"] == params[0]
165
+ and row["evidence_fingerprint"] == params[3]
166
+ for row in pool.verification_evidence.values()
167
+ ):
168
+ raise UniqueViolation("verification evidence fingerprint already exists")
169
+ conn._touch(key)
170
+ pool.verification_evidence[key[1:]] = {
171
+ "scope_namespace": params[0],
172
+ "bundle_id": params[1],
173
+ "bundle_fingerprint": params[2],
174
+ "evidence_fingerprint": params[3],
175
+ "schema_version": params[4],
176
+ "envelope": params[5],
177
+ "created_at": _as_dt(params[6]),
178
+ }
179
+ return ([], 1)
180
+
181
+
182
+ def _h_read_verification_evidence(
183
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
184
+ ) -> tuple[list[dict[str, Any]], int]:
185
+ row = pool.verification_evidence.get(params)
186
+ if row is None:
187
+ return ([], 0)
188
+ return (
189
+ [
190
+ {
191
+ "evidence_fingerprint": row["evidence_fingerprint"],
192
+ "envelope": row["envelope"],
193
+ "schema_version": row["schema_version"],
194
+ }
195
+ ],
196
+ 0,
197
+ )
198
+
199
+
200
+ def _h_insert_publish_audit(
201
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
202
+ ) -> tuple[list[dict[str, Any]], int]:
203
+ key = _audit_key(params[0], params[1], params[2])
204
+ _lock_or_fail(pool, conn, key, timeout)
205
+ if key[1:] in pool.publish_audits:
206
+ raise UniqueViolation("publish audit already exists")
207
+ if params[4] is not None and any(
208
+ row["scope_namespace"] == params[0]
209
+ and row["idempotency_key"] == params[4]
210
+ for row in pool.publish_audits.values()
211
+ ):
212
+ raise UniqueViolation("idempotency key already exists")
213
+ conn._touch(key)
214
+ pool.publish_audits[key[1:]] = {
215
+ "scope_namespace": params[0],
216
+ "bundle_id": params[1],
217
+ "bundle_fingerprint": params[2],
218
+ "audit_id": params[3],
219
+ "idempotency_key": params[4],
220
+ "schema_version": params[5],
221
+ "envelope": params[6],
222
+ "created_at": _as_dt(params[7]),
223
+ }
224
+ return ([], 1)
225
+
226
+
227
+ def _h_read_publish_audit(
228
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
229
+ ) -> tuple[list[dict[str, Any]], int]:
230
+ row = pool.publish_audits.get(params)
231
+ if row is None:
232
+ return ([], 0)
233
+ return ([{"envelope": row["envelope"], "schema_version": row["schema_version"]}], 0)
234
+
235
+
236
+ def _h_read_publish_by_idempotency_key(
237
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
238
+ ) -> tuple[list[dict[str, Any]], int]:
239
+ namespace, idempotency_key = params
240
+ for row in pool.publish_audits.values():
241
+ if (
242
+ row["scope_namespace"] == namespace
243
+ and row["idempotency_key"] == idempotency_key
244
+ ):
245
+ return ([{
246
+ "bundle_id": row["bundle_id"],
247
+ "bundle_fingerprint": row["bundle_fingerprint"],
248
+ }], 0)
249
+ return ([], 0)
@@ -0,0 +1,23 @@
1
+ """Schema-version statement handlers for the fake pool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from .driver import _FakeConnection
9
+ from .pool import FakePostgresPool
10
+
11
+
12
+ def _h_read_schema_version(
13
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
14
+ ) -> tuple[list[dict[str, Any]], int]:
15
+ value = pool.schema_metadata.get("schema_version")
16
+ return ([{"value": value}] if value is not None else [], 0)
17
+
18
+
19
+ def _h_write_schema_version(
20
+ pool: FakePostgresPool, conn: _FakeConnection, params: tuple[Any, ...], timeout: float
21
+ ) -> tuple[list[dict[str, Any]], int]:
22
+ pool.schema_metadata["schema_version"] = str(params[0])
23
+ return ([], 1)