lorebase-store 0.2.3__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.
- lorebase_store/__init__.py +1 -0
- lorebase_store/_migrations/env.py +106 -0
- lorebase_store/_migrations/script.py.mako +27 -0
- lorebase_store/_migrations/versions/0001_initial_schema.py +1071 -0
- lorebase_store/_migrations/versions/0002_merge_attempt_details.py +74 -0
- lorebase_store/_migrations/versions/0003_webhooks.py +127 -0
- lorebase_store/_migrations/versions/0004_defer_revision_references.py +38 -0
- lorebase_store/_migrations/versions/0005_security_definer_reads.py +396 -0
- lorebase_store/_migrations/versions/0006_api_key_usage.py +48 -0
- lorebase_store/_migrations/versions/0007_document_extraction.py +154 -0
- lorebase_store/_migrations/versions/0008_definer_argument_checks.py +178 -0
- lorebase_store/_migrations/versions/0009_api_key_networks.py +33 -0
- lorebase_store/_migrations/versions/0010_overlay_branches.py +582 -0
- lorebase_store/_migrations/versions/0011_branch_scoped_search_index.py +229 -0
- lorebase_store/_migrations/versions/0012_overlay_neighborhood_lookups.py +185 -0
- lorebase_store/_migrations/versions/0013_web_sessions.py +72 -0
- lorebase_store/_migrations/versions/0014_embedding_jobs.py +70 -0
- lorebase_store/_migrations/versions/0015_append_only_audit_log.py +83 -0
- lorebase_store/_migrations/versions/0016_sealed_webhook_keys.py +55 -0
- lorebase_store/_migrations/versions/0017_background_branch_copies.py +77 -0
- lorebase_store/_migrations/versions/0018_search_lexicon.py +477 -0
- lorebase_store/_migrations/versions/0019_vault_bindings.py +56 -0
- lorebase_store/_migrations/versions/0020_outbox_consumers.py +150 -0
- lorebase_store/_migrations/versions/0021_schema_suggestions.py +114 -0
- lorebase_store/_migrations/versions/0022_read_audit_and_shared_rate_limits.py +133 -0
- lorebase_store/_migrations/versions/0023_agent_trace.py +96 -0
- lorebase_store/_migrations/versions/0024_embedding_template_keys.py +205 -0
- lorebase_store/_migrations/versions/0025_snapshot_refs.py +35 -0
- lorebase_store/_migrations/versions/0026_plain_reverse_link_index.py +112 -0
- lorebase_store/_migrations/versions/0027_vault_rotation_and_tombstones.py +145 -0
- lorebase_store/_migrations/versions/0028_operation_deferred_events.py +71 -0
- lorebase_store/_migrations/versions/0029_agent_trace_sessions.py +108 -0
- lorebase_store/_migrations/versions/0030_path_fanout.py +31 -0
- lorebase_store/_migrations/versions/0031_branch_builds.py +91 -0
- lorebase_store/_migrations/versions/0032_lineage_revisions.py +35 -0
- lorebase_store/_migrations/versions/0033_vault_moved_tokens.py +30 -0
- lorebase_store/_migrations/versions/0034_inference_endpoint_modes.py +73 -0
- lorebase_store/_migrations/versions/0035_search_truncation.py +104 -0
- lorebase_store/_migrations/versions/0036_lineage_maps.py +325 -0
- lorebase_store/_migrations/versions/0037_bm25_search.py +346 -0
- lorebase_store/auth/__init__.py +52 -0
- lorebase_store/auth/api_keys.py +161 -0
- lorebase_store/auth/audit.py +382 -0
- lorebase_store/auth/checkpoints.py +185 -0
- lorebase_store/auth/grants.py +161 -0
- lorebase_store/auth/principals.py +158 -0
- lorebase_store/auth/scope.py +70 -0
- lorebase_store/auth/sessions.py +153 -0
- lorebase_store/auth/unit.py +53 -0
- lorebase_store/auth/usage.py +70 -0
- lorebase_store/blobs.py +73 -0
- lorebase_store/bm25.py +346 -0
- lorebase_store/bulk.py +147 -0
- lorebase_store/bundle.py +1830 -0
- lorebase_store/changefeed.py +261 -0
- lorebase_store/commit/__init__.py +42 -0
- lorebase_store/commit/changes.py +67 -0
- lorebase_store/commit/errors.py +60 -0
- lorebase_store/commit/migrations.py +335 -0
- lorebase_store/commit/pipeline.py +754 -0
- lorebase_store/commit/schema.py +123 -0
- lorebase_store/db.py +58 -0
- lorebase_store/definer.py +50 -0
- lorebase_store/digests.py +99 -0
- lorebase_store/embeddings.py +444 -0
- lorebase_store/endpoint_modes.py +59 -0
- lorebase_store/extraction/__init__.py +61 -0
- lorebase_store/extraction/items.py +187 -0
- lorebase_store/extraction/materialize.py +364 -0
- lorebase_store/extraction/proposal.py +65 -0
- lorebase_store/extraction/review.py +168 -0
- lorebase_store/gc.py +970 -0
- lorebase_store/indexes.py +1033 -0
- lorebase_store/jobs.py +651 -0
- lorebase_store/keyring.py +72 -0
- lorebase_store/layout.py +335 -0
- lorebase_store/materialize/__init__.py +103 -0
- lorebase_store/materialize/apply.py +436 -0
- lorebase_store/materialize/background.py +1184 -0
- lorebase_store/materialize/branch.py +268 -0
- lorebase_store/materialize/build.py +536 -0
- lorebase_store/materialize/derived.py +540 -0
- lorebase_store/materialize/lineage_backfill.py +502 -0
- lorebase_store/materialize/links.py +266 -0
- lorebase_store/materialize/overlay.py +292 -0
- lorebase_store/materialize/paths.py +315 -0
- lorebase_store/materialize/rebuild.py +326 -0
- lorebase_store/materialize/rows.py +153 -0
- lorebase_store/materialize/sensitivity.py +66 -0
- lorebase_store/materialize/snapshot.py +205 -0
- lorebase_store/materialize/stale.py +444 -0
- lorebase_store/materialize/verify.py +321 -0
- lorebase_store/merging/__init__.py +39 -0
- lorebase_store/merging/attempts.py +334 -0
- lorebase_store/merging/lineage.py +123 -0
- lorebase_store/merging/service.py +652 -0
- lorebase_store/migrations.py +133 -0
- lorebase_store/models/__init__.py +136 -0
- lorebase_store/models/base.py +64 -0
- lorebase_store/models/enums.py +132 -0
- lorebase_store/models/governance.py +268 -0
- lorebase_store/models/ingestion.py +288 -0
- lorebase_store/models/materialized.py +277 -0
- lorebase_store/models/proposals.py +195 -0
- lorebase_store/models/queues.py +87 -0
- lorebase_store/models/tracing.py +116 -0
- lorebase_store/models/versioning.py +222 -0
- lorebase_store/models/webhooks.py +96 -0
- lorebase_store/nodestore.py +384 -0
- lorebase_store/objectstore.py +162 -0
- lorebase_store/outbox_events.py +166 -0
- lorebase_store/profiling/__init__.py +58 -0
- lorebase_store/profiling/delimited.py +109 -0
- lorebase_store/profiling/postgres.py +211 -0
- lorebase_store/profiling/record_files.py +127 -0
- lorebase_store/profiling/records.py +92 -0
- lorebase_store/profiling/sql_databases.py +298 -0
- lorebase_store/ratelimit.py +129 -0
- lorebase_store/reads.py +1491 -0
- lorebase_store/replicas.py +434 -0
- lorebase_store/repositories/__init__.py +152 -0
- lorebase_store/repositories/changesets.py +59 -0
- lorebase_store/repositories/deferred_events.py +142 -0
- lorebase_store/repositories/documents.py +305 -0
- lorebase_store/repositories/domains.py +97 -0
- lorebase_store/repositories/events.py +205 -0
- lorebase_store/repositories/index_view.py +489 -0
- lorebase_store/repositories/invocations.py +68 -0
- lorebase_store/repositories/materializations.py +276 -0
- lorebase_store/repositories/materialized.py +1129 -0
- lorebase_store/repositories/merges.py +417 -0
- lorebase_store/repositories/proposal_items.py +199 -0
- lorebase_store/repositories/proposals.py +184 -0
- lorebase_store/repositories/provenance.py +77 -0
- lorebase_store/repositories/refs.py +452 -0
- lorebase_store/repositories/revisions.py +273 -0
- lorebase_store/repositories/scope.py +33 -0
- lorebase_store/repositories/source_runs.py +123 -0
- lorebase_store/repositories/suggestions.py +638 -0
- lorebase_store/runtime.py +87 -0
- lorebase_store/schema_cache.py +50 -0
- lorebase_store/settings.py +879 -0
- lorebase_store/storage.py +1478 -0
- lorebase_store/telemetry.py +142 -0
- lorebase_store/testing.py +108 -0
- lorebase_store/tracing.py +505 -0
- lorebase_store/traverse.py +578 -0
- lorebase_store/vault/__init__.py +50 -0
- lorebase_store/vault/keys.py +216 -0
- lorebase_store/vault/quotes.py +133 -0
- lorebase_store/vault/rotation.py +209 -0
- lorebase_store/vault/settings.py +152 -0
- lorebase_store/vault/store.py +987 -0
- lorebase_store/vault/transfer.py +558 -0
- lorebase_store/webhooks/__init__.py +24 -0
- lorebase_store/webhooks/delivery.py +637 -0
- lorebase_store/webhooks/keys.py +126 -0
- lorebase_store/webhooks/repository.py +142 -0
- lorebase_store/webhooks/settings.py +138 -0
- lorebase_store/webhooks/signing.py +50 -0
- lorebase_store-0.2.3.dist-info/METADATA +13 -0
- lorebase_store-0.2.3.dist-info/RECORD +163 -0
- lorebase_store-0.2.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Operation invocations (design 10.1): one row per execution, idempotent per principal and key."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from sqlalchemy import Table, insert, select
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
11
|
+
|
|
12
|
+
from lorebase_store.models import metadata
|
|
13
|
+
from lorebase_store.models.enums import InvocationStatus
|
|
14
|
+
|
|
15
|
+
INVOCATIONS: Table = metadata.tables["operation_invocations"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class InvocationRow:
|
|
20
|
+
id: str
|
|
21
|
+
operation_id: str
|
|
22
|
+
principal: str
|
|
23
|
+
params: dict[str, Any]
|
|
24
|
+
status: InvocationStatus
|
|
25
|
+
revision_id: bytes | None
|
|
26
|
+
proposal_id: str | None
|
|
27
|
+
idempotency_key: str | None
|
|
28
|
+
created_at: datetime
|
|
29
|
+
duration_ms: int | None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class InvocationRepository:
|
|
33
|
+
"""Invocations of one Domain."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, session: AsyncSession, domain_id: str) -> None:
|
|
36
|
+
self._session = session
|
|
37
|
+
self._domain = domain_id
|
|
38
|
+
|
|
39
|
+
async def insert(self, row: InvocationRow) -> None:
|
|
40
|
+
await self._session.execute(
|
|
41
|
+
insert(INVOCATIONS).values(
|
|
42
|
+
domain_id=self._domain,
|
|
43
|
+
id=row.id,
|
|
44
|
+
operation_id=row.operation_id,
|
|
45
|
+
principal=row.principal,
|
|
46
|
+
params=row.params,
|
|
47
|
+
status=row.status,
|
|
48
|
+
revision_id=row.revision_id,
|
|
49
|
+
proposal_id=row.proposal_id,
|
|
50
|
+
idempotency_key=row.idempotency_key,
|
|
51
|
+
created_at=row.created_at,
|
|
52
|
+
duration_ms=row.duration_ms,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
async def by_key(self, principal: str, idempotency_key: str) -> InvocationRow | None:
|
|
57
|
+
"""The invocation a principal recorded under an idempotency key."""
|
|
58
|
+
result = await self._session.execute(
|
|
59
|
+
select(INVOCATIONS).where(
|
|
60
|
+
INVOCATIONS.c.domain_id == self._domain,
|
|
61
|
+
INVOCATIONS.c.principal == principal,
|
|
62
|
+
INVOCATIONS.c.idempotency_key == idempotency_key,
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
found = result.mappings().one_or_none()
|
|
66
|
+
if found is None:
|
|
67
|
+
return None
|
|
68
|
+
return InvocationRow(**{k: v for k, v in found.items() if k != "domain_id"})
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Progress of background branch copies (`branch_materializations`, revisions 0017, 0031).
|
|
2
|
+
|
|
3
|
+
A row exists for each branch a `store.materialize` job turns into a full copy
|
|
4
|
+
(`lorebase_store.materialize.background`): from the base rows of an overlay (`base`) or from
|
|
5
|
+
the tree of its head (`tree`). It holds the status, the phase and cursor where the next
|
|
6
|
+
batch starts, the rows done out of the rows to read, the failed jobs and the schema changes
|
|
7
|
+
to reconcile.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Collection
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from enum import StrEnum
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from sqlalchemy import Table, func, select, text, update
|
|
19
|
+
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
|
20
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
21
|
+
|
|
22
|
+
from lorebase_store.models import metadata
|
|
23
|
+
from lorebase_store.repositories.refs import advisory_lock_key
|
|
24
|
+
|
|
25
|
+
MATERIALIZATIONS: Table = metadata.tables["branch_materializations"]
|
|
26
|
+
|
|
27
|
+
_FENCE = text("SELECT pg_advisory_xact_lock(:key)")
|
|
28
|
+
_FENCE_SHARED = text("SELECT pg_advisory_xact_lock_shared(:key)")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def copy_fence(
|
|
32
|
+
session: AsyncSession, domain_id: str, branch: str, *, shared: bool = False
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Take the transaction-scoped fence between the batches of a background copy of `branch`
|
|
35
|
+
(`shared`) and whatever removes or replaces its rows outside a commit (promotion, re-fork,
|
|
36
|
+
rebuild, the switch to a full copy; exclusive). A batch inserts with `ON CONFLICT DO
|
|
37
|
+
NOTHING`: one running while such a transaction deletes rows of the branch could insert a
|
|
38
|
+
row again after the delete. Commits do not take it."""
|
|
39
|
+
key = advisory_lock_key(domain_id, f"\x00copy-fence\x00{branch}")
|
|
40
|
+
await session.execute(_FENCE_SHARED if shared else _FENCE, {"key": key})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MaterializationStatus(StrEnum):
|
|
44
|
+
"""Where a background copy stands."""
|
|
45
|
+
|
|
46
|
+
PENDING = "pending"
|
|
47
|
+
"""Queued, or waiting for a retry after a failed attempt."""
|
|
48
|
+
RUNNING = "running"
|
|
49
|
+
DONE = "done"
|
|
50
|
+
FAILED = "failed"
|
|
51
|
+
"""Its job used every attempt; the branch stays a correct overlay."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
ACTIVE = (MaterializationStatus.PENDING, MaterializationStatus.RUNNING)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class MaterializationStrategy(StrEnum):
|
|
58
|
+
"""Where a background copy reads the rows it writes."""
|
|
59
|
+
|
|
60
|
+
BASE = "base"
|
|
61
|
+
"""The base rows an overlay reads, as of its fork point."""
|
|
62
|
+
TREE = "tree"
|
|
63
|
+
"""The tree of the branch head (a `building` branch)."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class MaterializationPhase(StrEnum):
|
|
67
|
+
"""What the next batch does; `switch` makes the branch a full copy.
|
|
68
|
+
|
|
69
|
+
`base`: `records`, `links`, `history`, `switch`. `tree`: `clear` (rows left by an
|
|
70
|
+
earlier state), `tree_history`, `tree_rows`, `derived`, `paths`, `switch`. After the
|
|
71
|
+
switch, a copy that got schema changes reconciles: `prune`, `derived`, `paths`,
|
|
72
|
+
`search`.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
RECORDS = "records"
|
|
76
|
+
LINKS = "links"
|
|
77
|
+
HISTORY = "history"
|
|
78
|
+
CLEAR = "clear"
|
|
79
|
+
TREE_HISTORY = "tree_history"
|
|
80
|
+
TREE_ROWS = "tree_rows"
|
|
81
|
+
SWITCH = "switch"
|
|
82
|
+
PRUNE = "prune"
|
|
83
|
+
DERIVED = "derived"
|
|
84
|
+
PATHS = "paths"
|
|
85
|
+
SEARCH = "search"
|
|
86
|
+
DONE = "done"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True, slots=True)
|
|
90
|
+
class MaterializationState:
|
|
91
|
+
branch: str
|
|
92
|
+
status: MaterializationStatus
|
|
93
|
+
phase: MaterializationPhase
|
|
94
|
+
base_branch: str
|
|
95
|
+
base_generation: int
|
|
96
|
+
cursor: list[Any] | None
|
|
97
|
+
rows_done: int
|
|
98
|
+
rows_total: int | None
|
|
99
|
+
error: str | None
|
|
100
|
+
created_at: datetime
|
|
101
|
+
updated_at: datetime
|
|
102
|
+
finished_at: datetime | None
|
|
103
|
+
strategy: MaterializationStrategy = MaterializationStrategy.BASE
|
|
104
|
+
failures: int = 0
|
|
105
|
+
reconcile: dict[str, Any] | None = None
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def active(self) -> bool:
|
|
109
|
+
return self.status in ACTIVE
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _state(row: Any) -> MaterializationState:
|
|
113
|
+
cursor = row["cursor"]
|
|
114
|
+
return MaterializationState(
|
|
115
|
+
branch=row["branch"],
|
|
116
|
+
status=MaterializationStatus(row["status"]),
|
|
117
|
+
phase=MaterializationPhase(row["phase"]),
|
|
118
|
+
base_branch=row["base_branch"],
|
|
119
|
+
base_generation=int(row["base_generation"]),
|
|
120
|
+
cursor=None if cursor is None else list(cursor["key"]),
|
|
121
|
+
rows_done=int(row["rows_done"]),
|
|
122
|
+
rows_total=None if row["rows_total"] is None else int(row["rows_total"]),
|
|
123
|
+
error=row["error"],
|
|
124
|
+
created_at=row["created_at"],
|
|
125
|
+
updated_at=row["updated_at"],
|
|
126
|
+
finished_at=row["finished_at"],
|
|
127
|
+
strategy=MaterializationStrategy(row["strategy"]),
|
|
128
|
+
failures=int(row["failures"]),
|
|
129
|
+
reconcile=row["reconcile"],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class BranchMaterializationRepository:
|
|
134
|
+
"""Background copy progress of the branches of one Domain."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, session: AsyncSession, domain_id: str) -> None:
|
|
137
|
+
self._session = session
|
|
138
|
+
self._domain = domain_id
|
|
139
|
+
|
|
140
|
+
async def get(self, branch: str, *, for_update: bool = False) -> MaterializationState | None:
|
|
141
|
+
query = select(MATERIALIZATIONS).where(
|
|
142
|
+
MATERIALIZATIONS.c.domain_id == self._domain, MATERIALIZATIONS.c.branch == branch
|
|
143
|
+
)
|
|
144
|
+
if for_update:
|
|
145
|
+
query = query.with_for_update()
|
|
146
|
+
found = (await self._session.execute(query)).mappings().one_or_none()
|
|
147
|
+
return None if found is None else _state(found)
|
|
148
|
+
|
|
149
|
+
async def of(self, branches: Collection[str]) -> dict[str, MaterializationState]:
|
|
150
|
+
"""The rows of `branches` that have one, by branch."""
|
|
151
|
+
if not branches:
|
|
152
|
+
return {}
|
|
153
|
+
result = await self._session.execute(
|
|
154
|
+
select(MATERIALIZATIONS).where(
|
|
155
|
+
MATERIALIZATIONS.c.domain_id == self._domain,
|
|
156
|
+
MATERIALIZATIONS.c.branch.in_(list(branches)),
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
return {row["branch"]: _state(row) for row in result.mappings()}
|
|
160
|
+
|
|
161
|
+
async def active(self, branch: str) -> bool:
|
|
162
|
+
"""Whether a background copy of `branch` is queued or running."""
|
|
163
|
+
found = await self._session.scalar(
|
|
164
|
+
select(func.count())
|
|
165
|
+
.select_from(MATERIALIZATIONS)
|
|
166
|
+
.where(
|
|
167
|
+
MATERIALIZATIONS.c.domain_id == self._domain,
|
|
168
|
+
MATERIALIZATIONS.c.branch == branch,
|
|
169
|
+
MATERIALIZATIONS.c.status.in_([s.value for s in ACTIVE]),
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
return bool(found)
|
|
173
|
+
|
|
174
|
+
async def count_active(self) -> int:
|
|
175
|
+
"""Background copies queued or running in the Domain."""
|
|
176
|
+
found = await self._session.scalar(
|
|
177
|
+
select(func.count())
|
|
178
|
+
.select_from(MATERIALIZATIONS)
|
|
179
|
+
.where(
|
|
180
|
+
MATERIALIZATIONS.c.domain_id == self._domain,
|
|
181
|
+
MATERIALIZATIONS.c.status.in_([s.value for s in ACTIVE]),
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
return int(found or 0)
|
|
185
|
+
|
|
186
|
+
async def start(
|
|
187
|
+
self,
|
|
188
|
+
branch: str,
|
|
189
|
+
*,
|
|
190
|
+
base_branch: str,
|
|
191
|
+
base_generation: int,
|
|
192
|
+
status: MaterializationStatus,
|
|
193
|
+
at: datetime,
|
|
194
|
+
strategy: MaterializationStrategy = MaterializationStrategy.BASE,
|
|
195
|
+
failures: int = 0,
|
|
196
|
+
reconcile: dict[str, Any] | None = None,
|
|
197
|
+
) -> MaterializationState:
|
|
198
|
+
"""Start (or restart from the first row) the copy of `branch`: from its fork point
|
|
199
|
+
(`base`) or from the tree of its head (`tree`)."""
|
|
200
|
+
first = (
|
|
201
|
+
MaterializationPhase.RECORDS
|
|
202
|
+
if strategy is MaterializationStrategy.BASE
|
|
203
|
+
else MaterializationPhase.CLEAR
|
|
204
|
+
)
|
|
205
|
+
values: dict[str, Any] = {
|
|
206
|
+
"status": status.value,
|
|
207
|
+
"phase": first.value,
|
|
208
|
+
"strategy": strategy.value,
|
|
209
|
+
"base_branch": base_branch,
|
|
210
|
+
"base_generation": base_generation,
|
|
211
|
+
"cursor": None,
|
|
212
|
+
"rows_done": 0,
|
|
213
|
+
"rows_total": None,
|
|
214
|
+
"error": None,
|
|
215
|
+
"failures": failures,
|
|
216
|
+
"reconcile": reconcile,
|
|
217
|
+
"updated_at": at,
|
|
218
|
+
"finished_at": None,
|
|
219
|
+
}
|
|
220
|
+
statement = pg_insert(MATERIALIZATIONS).values(
|
|
221
|
+
domain_id=self._domain, branch=branch, created_at=at, **values
|
|
222
|
+
)
|
|
223
|
+
await self._session.execute(
|
|
224
|
+
statement.on_conflict_do_update(
|
|
225
|
+
index_elements=[MATERIALIZATIONS.c.domain_id, MATERIALIZATIONS.c.branch],
|
|
226
|
+
set_=values,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
found = await self.get(branch)
|
|
230
|
+
assert found is not None
|
|
231
|
+
return found
|
|
232
|
+
|
|
233
|
+
async def update(self, branch: str, *, at: datetime, **values: Any) -> None:
|
|
234
|
+
"""Set columns of the row of `branch` (`cursor` as a list of key values or None)."""
|
|
235
|
+
if "status" in values:
|
|
236
|
+
values["status"] = MaterializationStatus(values["status"]).value
|
|
237
|
+
if "phase" in values:
|
|
238
|
+
values["phase"] = MaterializationPhase(values["phase"]).value
|
|
239
|
+
if "strategy" in values:
|
|
240
|
+
values["strategy"] = MaterializationStrategy(values["strategy"]).value
|
|
241
|
+
if "cursor" in values and values["cursor"] is not None:
|
|
242
|
+
values["cursor"] = {"key": list(values["cursor"])}
|
|
243
|
+
await self._session.execute(
|
|
244
|
+
update(MATERIALIZATIONS)
|
|
245
|
+
.where(
|
|
246
|
+
MATERIALIZATIONS.c.domain_id == self._domain,
|
|
247
|
+
MATERIALIZATIONS.c.branch == branch,
|
|
248
|
+
)
|
|
249
|
+
.values(updated_at=at, **values)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
async def finish(self, branch: str, *, at: datetime) -> None:
|
|
253
|
+
"""Mark the copy of `branch` done (also when another path made it a full copy)."""
|
|
254
|
+
await self.update(
|
|
255
|
+
branch,
|
|
256
|
+
at=at,
|
|
257
|
+
status=MaterializationStatus.DONE,
|
|
258
|
+
phase=MaterializationPhase.DONE,
|
|
259
|
+
cursor=None,
|
|
260
|
+
error=None,
|
|
261
|
+
failures=0,
|
|
262
|
+
reconcile=None,
|
|
263
|
+
finished_at=at,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
async def add_reconcile(
|
|
267
|
+
self, branch: str, *, refresh_kinds: Collection[str], at: datetime
|
|
268
|
+
) -> None:
|
|
269
|
+
"""Record a schema change made while the copy of `branch` runs: after its switch, the
|
|
270
|
+
job recomputes what a schema change recomputes on a full branch (`refresh_kinds`:
|
|
271
|
+
Kinds whose titles and search vectors change)."""
|
|
272
|
+
state = await self.get(branch, for_update=True)
|
|
273
|
+
if state is None:
|
|
274
|
+
return
|
|
275
|
+
kinds = set((state.reconcile or {}).get("refresh_kinds", ())) | set(refresh_kinds)
|
|
276
|
+
await self.update(branch, at=at, reconcile={"refresh_kinds": sorted(kinds)})
|