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,436 @@
|
|
|
1
|
+
"""Incremental materialization: apply one Revision's changeset to a branch head (design 6.11).
|
|
2
|
+
|
|
3
|
+
Runs inside the commit transaction (invariant I4). Rows are written in bulk: one statement
|
|
4
|
+
batch per partition and operation, then the derived values the changeset can affect
|
|
5
|
+
(`materialize.derived`; every derived value when the schema changed) and the Links of
|
|
6
|
+
materialized path Derivations (`materialize.paths`; every one when the schema or
|
|
7
|
+
`store.traverse.path_fanout` changed), then one set-based
|
|
8
|
+
search vector update per Kind.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections import defaultdict
|
|
14
|
+
from collections.abc import Mapping, Sequence
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
20
|
+
|
|
21
|
+
from lorebase_core.expr import ExprLimits
|
|
22
|
+
from lorebase_core.model import Schema
|
|
23
|
+
from lorebase_core.revision import ChangesetEntry
|
|
24
|
+
from lorebase_store.materialize.derived import AppliedChanges, DerivedValues
|
|
25
|
+
from lorebase_store.materialize.overlay import check_overlay, promote
|
|
26
|
+
from lorebase_store.materialize.paths import PathLinks
|
|
27
|
+
from lorebase_store.materialize.rows import (
|
|
28
|
+
PartitionType,
|
|
29
|
+
next_lineage,
|
|
30
|
+
next_lineage_revisions,
|
|
31
|
+
parse_partition,
|
|
32
|
+
record_title,
|
|
33
|
+
search_weights,
|
|
34
|
+
split_link_key,
|
|
35
|
+
)
|
|
36
|
+
from lorebase_store.materialize.sensitivity import branch_tagged_schema
|
|
37
|
+
from lorebase_store.repositories.materializations import BranchMaterializationRepository
|
|
38
|
+
from lorebase_store.repositories.materialized import (
|
|
39
|
+
HistoryRow,
|
|
40
|
+
LinkRow,
|
|
41
|
+
MaterializedRepository,
|
|
42
|
+
RecordRow,
|
|
43
|
+
SchemaRow,
|
|
44
|
+
)
|
|
45
|
+
from lorebase_store.repositories.revisions import RevisionRepository
|
|
46
|
+
from lorebase_store.runtime import StoreRuntime
|
|
47
|
+
from lorebase_store.settings import StoreSettings
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class MaterializedChange:
|
|
52
|
+
"""A changeset entry with the new object body (`None` when the element is removed)."""
|
|
53
|
+
|
|
54
|
+
entry: ChangesetEntry
|
|
55
|
+
body: Mapping[str, Any] | None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def apply_changeset(
|
|
59
|
+
session: AsyncSession,
|
|
60
|
+
domain_id: str,
|
|
61
|
+
branch: str,
|
|
62
|
+
*,
|
|
63
|
+
revision_id: bytes,
|
|
64
|
+
at: datetime,
|
|
65
|
+
changes: Sequence[MaterializedChange],
|
|
66
|
+
schema: Schema,
|
|
67
|
+
schema_hash: bytes | None,
|
|
68
|
+
refresh_kinds: Sequence[str] = (),
|
|
69
|
+
settings: StoreSettings,
|
|
70
|
+
expression_limits: ExprLimits | None = None,
|
|
71
|
+
runtime: StoreRuntime | None = None,
|
|
72
|
+
lineage_source: str | None = None,
|
|
73
|
+
fast_forward_from: bytes | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Apply a Revision's changeset to the materialized head of `branch`.
|
|
76
|
+
|
|
77
|
+
`lineage_source` names the branch a fast-forward moves `branch` to: the branch's
|
|
78
|
+
first-parent chain becomes the source's, so a written Record that the source holds with
|
|
79
|
+
the same object takes the source row's lineage, lineage revisions and update Revision (a
|
|
80
|
+
Link, its lineage), as
|
|
81
|
+
a rebuild folding that chain would (one primary-key read per batch of changed keys; a
|
|
82
|
+
source row without lineage revisions is folded from the changeset instead).
|
|
83
|
+
`fast_forward_from` is the branch's head before that fast-forward: when it lies on the
|
|
84
|
+
first-parent chain of `revision_id` within `materialize.fast_forward_history_max_revisions`
|
|
85
|
+
Revisions, the branch history gets one row per change of each of them, as a rebuild
|
|
86
|
+
writes it; otherwise one row per change at `revision_id`.
|
|
87
|
+
|
|
88
|
+
`schema` is the schema at the Revision; `schema_hash` is its schema partition hash when
|
|
89
|
+
the schema changed. `refresh_kinds` lists Kinds whose title or search settings changed:
|
|
90
|
+
their title and search vectors are recomputed for every Record. `expression_limits`
|
|
91
|
+
bound the evaluation of Derivations (`schema.expression_limits`).
|
|
92
|
+
|
|
93
|
+
The caller holds the branch lock. When the schema changes, an overlay over a small base
|
|
94
|
+
is promoted to a full copy first; an overlay over a large base, or one whose background
|
|
95
|
+
copy is queued or running, keeps its overlay rows: the change is applied to the rows the
|
|
96
|
+
changeset writes, and the copy job (queued if needed) recomputes the rest after the
|
|
97
|
+
switch (`reconcile`, `materialize.background`). An overlay over its limits is promoted
|
|
98
|
+
afterwards (`materialize.overlay`); overlays based on the branch get the previous state
|
|
99
|
+
of the rows it changes. A `building` branch raises `BranchMaterializingError`.
|
|
100
|
+
"""
|
|
101
|
+
runtime = runtime or StoreRuntime.from_settings(settings)
|
|
102
|
+
# Titles and search vectors use the default branch's tags too (`materialize.sensitivity`).
|
|
103
|
+
tagged = await branch_tagged_schema(session, domain_id, branch, schema, runtime=runtime)
|
|
104
|
+
generation = (await RevisionRepository(session, domain_id).generations([revision_id]))[
|
|
105
|
+
revision_id
|
|
106
|
+
]
|
|
107
|
+
repository = MaterializedRepository(
|
|
108
|
+
session,
|
|
109
|
+
domain_id,
|
|
110
|
+
branch,
|
|
111
|
+
writes=settings.writes,
|
|
112
|
+
generation=generation,
|
|
113
|
+
lock_timeout_ms=settings.commit.lock_timeout_ms,
|
|
114
|
+
)
|
|
115
|
+
layout = await repository.layout()
|
|
116
|
+
if layout.building:
|
|
117
|
+
# Imported here: the commit package imports the materializer.
|
|
118
|
+
from lorebase_store.commit.errors import BranchMaterializingError
|
|
119
|
+
|
|
120
|
+
raise BranchMaterializingError(
|
|
121
|
+
f"branch {branch!r} is being built from its tree in the background; retry when "
|
|
122
|
+
"its materialization is done"
|
|
123
|
+
)
|
|
124
|
+
deferred = False
|
|
125
|
+
if schema_hash is not None and layout.overlay:
|
|
126
|
+
deferred = await defer_schema_change(
|
|
127
|
+
session, domain_id, branch, refresh_kinds=refresh_kinds, at=at, runtime=runtime
|
|
128
|
+
)
|
|
129
|
+
if not deferred:
|
|
130
|
+
await promote(session, domain_id, branch, runtime=runtime, locked=True)
|
|
131
|
+
repository.forget_layout()
|
|
132
|
+
fast_forwarded = (
|
|
133
|
+
None
|
|
134
|
+
if fast_forward_from is None
|
|
135
|
+
else await repository.first_parent_range(
|
|
136
|
+
revision_id,
|
|
137
|
+
fast_forward_from,
|
|
138
|
+
limit=settings.materialize.fast_forward_history_max_revisions,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
if fast_forwarded is not None:
|
|
142
|
+
await repository.insert_chain_history(fast_forwarded)
|
|
143
|
+
else:
|
|
144
|
+
await repository.insert_history(
|
|
145
|
+
[
|
|
146
|
+
HistoryRow(
|
|
147
|
+
partition=change.entry.partition,
|
|
148
|
+
element_key=change.entry.key,
|
|
149
|
+
revision_id=revision_id,
|
|
150
|
+
at=at,
|
|
151
|
+
old_hash=change.entry.old_hash,
|
|
152
|
+
new_hash=change.entry.new_hash,
|
|
153
|
+
provenance=dict(change.entry.provenance) or None,
|
|
154
|
+
)
|
|
155
|
+
for change in changes
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
if schema_hash is not None:
|
|
159
|
+
await repository.set_schema(SchemaRow(schema_hash, schema.content()))
|
|
160
|
+
by_partition: dict[str, list[MaterializedChange]] = defaultdict(list)
|
|
161
|
+
for change in changes:
|
|
162
|
+
by_partition[change.entry.partition].append(change)
|
|
163
|
+
derived = None
|
|
164
|
+
if settings.materialize.derivations_enabled:
|
|
165
|
+
derived = DerivedValues(
|
|
166
|
+
repository,
|
|
167
|
+
schema,
|
|
168
|
+
limits=expression_limits or ExprLimits(),
|
|
169
|
+
now=at,
|
|
170
|
+
batch=settings.materialize.read_batch_rows,
|
|
171
|
+
max_incremental=settings.materialize.derivation_max_incremental_records,
|
|
172
|
+
fanout=settings.traverse.path_fanout,
|
|
173
|
+
)
|
|
174
|
+
paths = None
|
|
175
|
+
if settings.materialize.derivations_enabled:
|
|
176
|
+
paths = PathLinks(
|
|
177
|
+
repository,
|
|
178
|
+
schema,
|
|
179
|
+
fanout=settings.traverse.path_fanout,
|
|
180
|
+
batch=settings.materialize.read_batch_rows,
|
|
181
|
+
max_incremental=settings.materialize.derivation_max_incremental_records,
|
|
182
|
+
)
|
|
183
|
+
source = (
|
|
184
|
+
None
|
|
185
|
+
if lineage_source is None
|
|
186
|
+
else MaterializedRepository(session, domain_id, lineage_source, writes=settings.writes)
|
|
187
|
+
)
|
|
188
|
+
applied = AppliedChanges()
|
|
189
|
+
keep_previous = (derived is not None and bool(derived.steps)) or bool(paths and paths.paths)
|
|
190
|
+
written_kinds: list[str] = []
|
|
191
|
+
for partition, group in sorted(by_partition.items()):
|
|
192
|
+
parsed = parse_partition(partition)
|
|
193
|
+
if parsed.type is PartitionType.KIND:
|
|
194
|
+
await _apply_records(
|
|
195
|
+
repository,
|
|
196
|
+
parsed.element_id,
|
|
197
|
+
group,
|
|
198
|
+
tagged,
|
|
199
|
+
revision_id,
|
|
200
|
+
at,
|
|
201
|
+
applied,
|
|
202
|
+
keep_previous=keep_previous,
|
|
203
|
+
source=source,
|
|
204
|
+
)
|
|
205
|
+
written_kinds.append(parsed.element_id)
|
|
206
|
+
elif parsed.type is PartitionType.RELATION:
|
|
207
|
+
await _apply_links(repository, parsed.element_id, group, source=source)
|
|
208
|
+
applied.links[parsed.element_id].update(split_link_key(c.entry.key) for c in group)
|
|
209
|
+
# A deferred schema change recomputes only what the changeset touched; the copy job
|
|
210
|
+
# reconciles every other row after its switch.
|
|
211
|
+
whole = schema_hash is not None and not deferred
|
|
212
|
+
changed: dict[str, set[str]] = {}
|
|
213
|
+
# A change of `store.traverse.path_fanout` recomputes whatever follows it, once.
|
|
214
|
+
stale = schema_hash is None and paths is not None and await paths.fanout_stale(derived)
|
|
215
|
+
if derived is not None:
|
|
216
|
+
if whole:
|
|
217
|
+
await derived.prune()
|
|
218
|
+
changed = await derived.recompute_all()
|
|
219
|
+
elif stale and derived.reads_paths:
|
|
220
|
+
changed = await derived.recompute_all()
|
|
221
|
+
elif derived.steps:
|
|
222
|
+
changed = await derived.recompute(applied)
|
|
223
|
+
if paths is not None:
|
|
224
|
+
if whole or stale:
|
|
225
|
+
if whole:
|
|
226
|
+
await paths.prune()
|
|
227
|
+
await paths.recompute_all()
|
|
228
|
+
await paths.record_fanout()
|
|
229
|
+
elif paths.paths:
|
|
230
|
+
await paths.recompute(applied)
|
|
231
|
+
if deferred:
|
|
232
|
+
refresh_kinds = ()
|
|
233
|
+
search_config = settings.materialize.text_search_config
|
|
234
|
+
for kind_id in written_kinds:
|
|
235
|
+
if kind_id not in refresh_kinds:
|
|
236
|
+
await repository.refresh_search(
|
|
237
|
+
kind_id,
|
|
238
|
+
search_weights(tagged.kinds.get(kind_id)),
|
|
239
|
+
config=search_config,
|
|
240
|
+
revision=revision_id,
|
|
241
|
+
keys=sorted(applied.written.get(kind_id, ())),
|
|
242
|
+
)
|
|
243
|
+
for kind_id, keys in sorted(changed.items()):
|
|
244
|
+
if keys and kind_id not in refresh_kinds and derived and derived.searchable(kind_id):
|
|
245
|
+
await repository.refresh_search(
|
|
246
|
+
kind_id,
|
|
247
|
+
search_weights(tagged.kinds.get(kind_id)),
|
|
248
|
+
config=search_config,
|
|
249
|
+
keys=sorted(keys),
|
|
250
|
+
)
|
|
251
|
+
for kind_id in refresh_kinds:
|
|
252
|
+
await refresh_kind(repository, tagged, kind_id, settings=settings)
|
|
253
|
+
await check_overlay(repository, generation=generation, runtime=runtime)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
async def defer_schema_change(
|
|
257
|
+
session: AsyncSession,
|
|
258
|
+
domain_id: str,
|
|
259
|
+
branch: str,
|
|
260
|
+
*,
|
|
261
|
+
refresh_kinds: Sequence[str],
|
|
262
|
+
at: datetime,
|
|
263
|
+
runtime: StoreRuntime,
|
|
264
|
+
) -> bool:
|
|
265
|
+
"""For a schema change on the overlay `branch`: whether its background copy takes it
|
|
266
|
+
over (`reconcile`) instead of a promotion in the writing transaction. It does when a copy
|
|
267
|
+
is queued or running, or when the base holds more than `async_copy_min_rows` rows (the
|
|
268
|
+
copy is then queued here)."""
|
|
269
|
+
# Imported here: the background jobs import the overlay helpers this module uses.
|
|
270
|
+
from lorebase_store.materialize.background import queue_materialization
|
|
271
|
+
from lorebase_store.materialize.overlay import base_is_large
|
|
272
|
+
from lorebase_store.repositories.refs import RefRepository
|
|
273
|
+
|
|
274
|
+
states = BranchMaterializationRepository(session, domain_id)
|
|
275
|
+
if not await states.active(branch):
|
|
276
|
+
ref = await RefRepository(session, domain_id).get(branch)
|
|
277
|
+
if ref is None or ref.base_branch is None:
|
|
278
|
+
return False
|
|
279
|
+
if not await base_is_large(session, domain_id, ref.base_branch, runtime=runtime):
|
|
280
|
+
return False
|
|
281
|
+
await queue_materialization(session, domain_id, ref, at=at)
|
|
282
|
+
await states.add_reconcile(branch, refresh_kinds=refresh_kinds, at=at)
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
async def refresh_kind(
|
|
287
|
+
repository: MaterializedRepository, schema: Schema, kind_id: str, *, settings: StoreSettings
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Recompute title and search vector of every Record of a Kind."""
|
|
290
|
+
kind = schema.kinds.get(kind_id)
|
|
291
|
+
async for rows in repository.stream_records(
|
|
292
|
+
kind_id, batch=settings.materialize.read_batch_rows
|
|
293
|
+
):
|
|
294
|
+
await repository.update_titles(
|
|
295
|
+
kind_id, [(row.key, record_title(kind, row.values)) for row in rows]
|
|
296
|
+
)
|
|
297
|
+
await repository.refresh_search(
|
|
298
|
+
kind_id, search_weights(kind), config=settings.materialize.text_search_config
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
async def _apply_records(
|
|
303
|
+
repository: MaterializedRepository,
|
|
304
|
+
kind_id: str,
|
|
305
|
+
changes: Sequence[MaterializedChange],
|
|
306
|
+
schema: Schema,
|
|
307
|
+
revision_id: bytes,
|
|
308
|
+
at: datetime,
|
|
309
|
+
applied: AppliedChanges,
|
|
310
|
+
*,
|
|
311
|
+
keep_previous: bool,
|
|
312
|
+
source: MaterializedRepository | None = None,
|
|
313
|
+
) -> None:
|
|
314
|
+
removed = [c.entry.key for c in changes if c.body is None]
|
|
315
|
+
written = [c for c in changes if c.body is not None]
|
|
316
|
+
updated = [c.entry.key for c in written if c.entry.old_hash is not None]
|
|
317
|
+
wanted = [*updated, *removed] if keep_previous else updated
|
|
318
|
+
previous = await repository.records(kind_id, wanted) if wanted else {}
|
|
319
|
+
applied.written[kind_id].update(c.entry.key for c in written)
|
|
320
|
+
applied.removed[kind_id].update(removed)
|
|
321
|
+
if keep_previous:
|
|
322
|
+
for key in [*updated, *removed]:
|
|
323
|
+
row = previous.get(key)
|
|
324
|
+
applied.previous[(kind_id, key)] = None if row is None else row.values
|
|
325
|
+
if removed:
|
|
326
|
+
await repository.delete_records(kind_id, removed)
|
|
327
|
+
if not written:
|
|
328
|
+
return
|
|
329
|
+
kind = schema.kinds.get(kind_id)
|
|
330
|
+
sourced = (
|
|
331
|
+
{} if source is None else await source.records(kind_id, [c.entry.key for c in written])
|
|
332
|
+
)
|
|
333
|
+
rows: list[RecordRow] = []
|
|
334
|
+
for change in written:
|
|
335
|
+
assert change.body is not None
|
|
336
|
+
assert change.entry.new_hash is not None
|
|
337
|
+
values: dict[str, Any] = dict(change.body.get("values", {}))
|
|
338
|
+
same = sourced.get(change.entry.key)
|
|
339
|
+
if (
|
|
340
|
+
same is not None
|
|
341
|
+
and same.object_hash == change.entry.new_hash
|
|
342
|
+
and same.lineage_revisions is not None
|
|
343
|
+
):
|
|
344
|
+
rows.append(
|
|
345
|
+
RecordRow(
|
|
346
|
+
kind_id=kind_id,
|
|
347
|
+
key=change.entry.key,
|
|
348
|
+
values=values,
|
|
349
|
+
title=record_title(kind, values),
|
|
350
|
+
object_hash=change.entry.new_hash,
|
|
351
|
+
lineage=dict(same.lineage),
|
|
352
|
+
updated_revision=same.updated_revision,
|
|
353
|
+
updated_at=same.updated_at,
|
|
354
|
+
lineage_revisions=dict(same.lineage_revisions),
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
continue
|
|
358
|
+
before = previous.get(change.entry.key)
|
|
359
|
+
rows.append(
|
|
360
|
+
RecordRow(
|
|
361
|
+
kind_id=kind_id,
|
|
362
|
+
key=change.entry.key,
|
|
363
|
+
values=values,
|
|
364
|
+
title=record_title(kind, values),
|
|
365
|
+
object_hash=change.entry.new_hash,
|
|
366
|
+
lineage=next_lineage(
|
|
367
|
+
None if before is None else before.lineage, change.entry.provenance, values
|
|
368
|
+
),
|
|
369
|
+
updated_revision=revision_id,
|
|
370
|
+
updated_at=at,
|
|
371
|
+
lineage_revisions=next_lineage_revisions(
|
|
372
|
+
None if before is None else before.lineage_revisions,
|
|
373
|
+
change.entry.provenance,
|
|
374
|
+
revision_id,
|
|
375
|
+
values,
|
|
376
|
+
created=change.entry.old_hash is None,
|
|
377
|
+
),
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
await repository.upsert_records(rows)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
async def _apply_links(
|
|
384
|
+
repository: MaterializedRepository,
|
|
385
|
+
relation_id: str,
|
|
386
|
+
changes: Sequence[MaterializedChange],
|
|
387
|
+
*,
|
|
388
|
+
source: MaterializedRepository | None = None,
|
|
389
|
+
) -> None:
|
|
390
|
+
removed = [split_link_key(c.entry.key) for c in changes if c.body is None]
|
|
391
|
+
written = [c for c in changes if c.body is not None]
|
|
392
|
+
if removed:
|
|
393
|
+
await repository.delete_links(relation_id, removed)
|
|
394
|
+
if not written:
|
|
395
|
+
return
|
|
396
|
+
updated = [split_link_key(c.entry.key) for c in written if c.entry.old_hash is not None]
|
|
397
|
+
previous = await repository.links(relation_id, updated) if updated else {}
|
|
398
|
+
sourced = (
|
|
399
|
+
{}
|
|
400
|
+
if source is None
|
|
401
|
+
else await source.links(relation_id, [split_link_key(c.entry.key) for c in written])
|
|
402
|
+
)
|
|
403
|
+
rows: list[LinkRow] = []
|
|
404
|
+
for change in written:
|
|
405
|
+
assert change.body is not None
|
|
406
|
+
assert change.entry.new_hash is not None
|
|
407
|
+
from_key, to_key = split_link_key(change.entry.key)
|
|
408
|
+
values: dict[str, Any] = dict(change.body.get("values", {}))
|
|
409
|
+
before = previous.get((from_key, to_key))
|
|
410
|
+
same = sourced.get((from_key, to_key))
|
|
411
|
+
if same is not None and same.object_hash == change.entry.new_hash:
|
|
412
|
+
# A fast-forward: the source's Lineage, as a rebuild folding its chain gives.
|
|
413
|
+
rows.append(
|
|
414
|
+
LinkRow(
|
|
415
|
+
relation_id=relation_id,
|
|
416
|
+
from_key=from_key,
|
|
417
|
+
to_key=to_key,
|
|
418
|
+
values=values,
|
|
419
|
+
object_hash=change.entry.new_hash,
|
|
420
|
+
lineage=dict(same.lineage),
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
continue
|
|
424
|
+
rows.append(
|
|
425
|
+
LinkRow(
|
|
426
|
+
relation_id=relation_id,
|
|
427
|
+
from_key=from_key,
|
|
428
|
+
to_key=to_key,
|
|
429
|
+
values=values,
|
|
430
|
+
object_hash=change.entry.new_hash,
|
|
431
|
+
lineage=next_lineage(
|
|
432
|
+
None if before is None else before.lineage, change.entry.provenance, values
|
|
433
|
+
),
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
await repository.upsert_links(rows)
|