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,879 @@
|
|
|
1
|
+
"""Settings sections owned by the store, registered with `lorebase_platform`.
|
|
2
|
+
|
|
3
|
+
- `database` (design 16): connection string, pool and timeouts, optional read replicas. The
|
|
4
|
+
environment alias `LOREBASE_DB_DSN` sets `database.dsn`, `LOREBASE_DB_READ_DSN` sets
|
|
5
|
+
`database.read_dsn` and `LOREBASE_DB_MAINTENANCE_DSN` sets `database.maintenance_dsn`; all
|
|
6
|
+
are handled as secrets.
|
|
7
|
+
- `store`: physical layout and vector storage (read by the initial migration), bulk writes,
|
|
8
|
+
tree node caching, commits, materialization, per-Attribute indexes, reads, neighborhood
|
|
9
|
+
traversal and the profilers of schema inference (read at runtime).
|
|
10
|
+
- `schema`: static validation of schemas, shared by Keeper and the store so that every path
|
|
11
|
+
that validates a schema (schema change sets, the commit pipeline, merges, schema inference)
|
|
12
|
+
applies the same rules: whether CEL expressions are accepted and their budget.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, Literal
|
|
18
|
+
|
|
19
|
+
from pydantic import Field, SecretStr, field_validator, model_validator
|
|
20
|
+
|
|
21
|
+
from lorebase_core.expr import ExprLimits
|
|
22
|
+
from lorebase_core.infer.suggestions import SuggestionOptions
|
|
23
|
+
from lorebase_core.validate import SchemaRules
|
|
24
|
+
from lorebase_platform.secrets import SecretValue
|
|
25
|
+
from lorebase_platform.settings import SectionModel, Settings, register_section
|
|
26
|
+
|
|
27
|
+
DATABASE_SECTION = "database"
|
|
28
|
+
STORE_SECTION = "store"
|
|
29
|
+
SCHEMA_SECTION = "schema"
|
|
30
|
+
ENV_ALIASES = {
|
|
31
|
+
"LOREBASE_DB_DSN": "dsn",
|
|
32
|
+
"LOREBASE_DB_READ_DSN": "read_dsn",
|
|
33
|
+
"LOREBASE_DB_MAINTENANCE_DSN": "maintenance_dsn",
|
|
34
|
+
}
|
|
35
|
+
DEFAULT_DSN = "postgresql+asyncpg://lorebase@localhost:5432/lorebase"
|
|
36
|
+
|
|
37
|
+
PGVECTOR_HNSW_MAX_DIMS = 2000
|
|
38
|
+
"""Largest `vector` dimension pgvector can index with HNSW."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DatabaseSettings(SectionModel):
|
|
42
|
+
"""Connection and pool of the Postgres database."""
|
|
43
|
+
|
|
44
|
+
dsn: SecretValue = Field(
|
|
45
|
+
default=SecretStr(DEFAULT_DSN),
|
|
46
|
+
description="SQLAlchemy URL; postgresql:// and postgres:// are switched to the asyncpg "
|
|
47
|
+
"driver. Literal or secret reference, e.g. secret://env/LOREBASE_DB_DSN (alias "
|
|
48
|
+
"LOREBASE_DB_DSN). Without a password in the URL, asyncpg reads PGPASSWORD or ~/.pgpass.",
|
|
49
|
+
)
|
|
50
|
+
pool_size: int = Field(default=10, ge=1, description="Connections kept open per process.")
|
|
51
|
+
max_overflow: int = Field(
|
|
52
|
+
default=10, ge=0, description="Extra connections opened under load above pool_size."
|
|
53
|
+
)
|
|
54
|
+
pool_timeout_s: float = Field(
|
|
55
|
+
default=30, gt=0, description="Seconds to wait for a free pooled connection."
|
|
56
|
+
)
|
|
57
|
+
pool_recycle_s: int = Field(
|
|
58
|
+
default=1800, ge=-1, description="Connections older than this are replaced; -1 never."
|
|
59
|
+
)
|
|
60
|
+
statement_timeout_ms: int = Field(
|
|
61
|
+
default=30000, ge=0, description="Postgres statement_timeout per connection; 0 disables."
|
|
62
|
+
)
|
|
63
|
+
migration_statement_timeout_ms: int = Field(
|
|
64
|
+
default=0,
|
|
65
|
+
ge=0,
|
|
66
|
+
description="statement_timeout while migrations run; 0 disables (index builds are long).",
|
|
67
|
+
)
|
|
68
|
+
application_name: str = Field(
|
|
69
|
+
default="lorebase", min_length=1, description="Reported in pg_stat_activity."
|
|
70
|
+
)
|
|
71
|
+
maintenance_dsn: SecretValue | None = Field(
|
|
72
|
+
default=None,
|
|
73
|
+
description="Optional URL of a role that owns mat_records (or may create indexes on "
|
|
74
|
+
"it), used only for maintenance DDL: the job worker builds and drops the per-Attribute "
|
|
75
|
+
"indexes with it. Literal or secret reference (alias LOREBASE_DB_MAINTENANCE_DSN). "
|
|
76
|
+
"Unset: only a commit role that owns mat_records manages the indexes.",
|
|
77
|
+
)
|
|
78
|
+
application_role: str | None = Field(
|
|
79
|
+
default=None,
|
|
80
|
+
min_length=1,
|
|
81
|
+
description="Database role the services connect as (subject to row-level security). "
|
|
82
|
+
"Read by migrations only: revisions 0005 and 0008 grant it EXECUTE on the security "
|
|
83
|
+
"definer read functions, which are revoked from PUBLIC, and 0008 refuses to run while "
|
|
84
|
+
"it may create objects in the schema of the tables or of pg_trgm; 0015 revokes UPDATE, "
|
|
85
|
+
"DELETE and TRUNCATE on audit_log from it. Empty grants nothing (the owner role can "
|
|
86
|
+
"still call them).",
|
|
87
|
+
)
|
|
88
|
+
read_dsn: tuple[SecretValue, ...] = Field(
|
|
89
|
+
default=(),
|
|
90
|
+
description="Read replicas (design 15.3): one URL, a comma-separated string or a list "
|
|
91
|
+
"of URLs, each a literal or secret reference. Reads are spread round-robin; empty "
|
|
92
|
+
"keeps every read on the primary. Alias LOREBASE_DB_READ_DSN. Each replica gets its "
|
|
93
|
+
"own pool sized like the primary's.",
|
|
94
|
+
)
|
|
95
|
+
read_max_lag_ms: int = Field(
|
|
96
|
+
default=5000,
|
|
97
|
+
ge=0,
|
|
98
|
+
description="A replica whose replay lag exceeds this serves no reads; 0 disables the "
|
|
99
|
+
"lag check (the head check of a pinned read still applies).",
|
|
100
|
+
)
|
|
101
|
+
read_fallback_to_primary: bool = Field(
|
|
102
|
+
default=True,
|
|
103
|
+
description="Reads that no replica can serve (stale, lagging, unreachable) go to the "
|
|
104
|
+
"primary; false answers 503 instead, protecting the primary from read load.",
|
|
105
|
+
)
|
|
106
|
+
read_probe_timeout_ms: int = Field(
|
|
107
|
+
default=2000,
|
|
108
|
+
ge=1,
|
|
109
|
+
description="Time budget to reach a replica and run its freshness probe before the "
|
|
110
|
+
"next replica or the fallback is tried.",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
@field_validator("read_dsn", mode="before")
|
|
114
|
+
@classmethod
|
|
115
|
+
def _split_read_dsn(cls, value: Any) -> Any:
|
|
116
|
+
if value is None:
|
|
117
|
+
return ()
|
|
118
|
+
if isinstance(value, str):
|
|
119
|
+
return tuple(part.strip() for part in value.split(",") if part.strip())
|
|
120
|
+
return value
|
|
121
|
+
|
|
122
|
+
prepared_statement_cache_size: int = Field(
|
|
123
|
+
default=500,
|
|
124
|
+
ge=0,
|
|
125
|
+
description="Prepared statements kept per connection by the asyncpg driver, keyed by "
|
|
126
|
+
"SQL text; 0 disables the cache.",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class LayoutSettings(SectionModel):
|
|
131
|
+
"""Physical layout fixed when the schema is installed (first migration)."""
|
|
132
|
+
|
|
133
|
+
hash_partitions: int = Field(
|
|
134
|
+
default=8,
|
|
135
|
+
ge=1,
|
|
136
|
+
le=1024,
|
|
137
|
+
description="Hash partitions on domain_id for objects, tree_nodes, mat_records and "
|
|
138
|
+
"mat_links. Read only by the initial migration; changing it later has no effect.",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class EmbeddingStoreSettings(SectionModel):
|
|
143
|
+
"""Vector storage. Must agree with `inference.embeddings`."""
|
|
144
|
+
|
|
145
|
+
dims: int = Field(
|
|
146
|
+
default=384,
|
|
147
|
+
ge=1,
|
|
148
|
+
le=PGVECTOR_HNSW_MAX_DIMS,
|
|
149
|
+
description="Dimension of the HNSW index created on `embeddings`; equals "
|
|
150
|
+
"inference.embeddings.dims.",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class WriteSettings(SectionModel):
|
|
155
|
+
"""Bulk writes of objects, tree nodes, changesets and materialized rows."""
|
|
156
|
+
|
|
157
|
+
insert_batch_rows: int = Field(
|
|
158
|
+
default=1000, ge=1, description="Rows per multi-row INSERT statement."
|
|
159
|
+
)
|
|
160
|
+
copy_threshold_rows: int = Field(
|
|
161
|
+
default=5000,
|
|
162
|
+
ge=0,
|
|
163
|
+
description="Batches with at least this many rows go through COPY into a staging table "
|
|
164
|
+
"and one set-based INSERT; 0 disables COPY.",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class NodeSettings(SectionModel):
|
|
169
|
+
"""Tree node reads (design 6.14: nodes are immutable and cached by hash)."""
|
|
170
|
+
|
|
171
|
+
cache_bytes: int = Field(
|
|
172
|
+
default=64 * 1024 * 1024,
|
|
173
|
+
ge=0,
|
|
174
|
+
description="In-process LRU cache of tree node bodies, in bytes; 0 disables it.",
|
|
175
|
+
)
|
|
176
|
+
prefetch_batch: int = Field(
|
|
177
|
+
default=1000, ge=1, description="Node hashes fetched per query when prefetching."
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class CommitSettings(SectionModel):
|
|
182
|
+
"""Commit pipeline (design 6.6)."""
|
|
183
|
+
|
|
184
|
+
lock_timeout_ms: int = Field(
|
|
185
|
+
default=10000,
|
|
186
|
+
ge=0,
|
|
187
|
+
description="Wait for the branch advisory lock before failing; 0 waits without limit.",
|
|
188
|
+
)
|
|
189
|
+
prefetch_batch: int = Field(
|
|
190
|
+
default=1000,
|
|
191
|
+
ge=1,
|
|
192
|
+
description="Keys per query when validation prefetches materialized Records and Links.",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class MaterializeSettings(SectionModel):
|
|
197
|
+
"""Materialized heads (design 6.11, 8.4)."""
|
|
198
|
+
|
|
199
|
+
text_search_config: str = Field(
|
|
200
|
+
default="simple",
|
|
201
|
+
min_length=1,
|
|
202
|
+
description="Postgres text search configuration of the Record search vectors.",
|
|
203
|
+
)
|
|
204
|
+
read_batch_rows: int = Field(
|
|
205
|
+
default=5000,
|
|
206
|
+
ge=1,
|
|
207
|
+
description="Rows or object bodies read per query by rebuild and verification.",
|
|
208
|
+
)
|
|
209
|
+
verify_max_divergences: int = Field(
|
|
210
|
+
default=1000, ge=1, description="Divergences listed by a verification report."
|
|
211
|
+
)
|
|
212
|
+
derivations_enabled: bool = Field(
|
|
213
|
+
default=True,
|
|
214
|
+
description="Recompute derived Attributes (expression and aggregate Derivations) and "
|
|
215
|
+
"the Links of materialized path Derivations in the materialized head on every commit, "
|
|
216
|
+
"merge and rebuild.",
|
|
217
|
+
)
|
|
218
|
+
fast_forward_history_max_revisions: int = Field(
|
|
219
|
+
default=1000,
|
|
220
|
+
ge=0,
|
|
221
|
+
description="Revisions of a fast-forwarded range written to the target branch's "
|
|
222
|
+
"history one by one, as a rebuild would; a longer range, or one whose start is not on "
|
|
223
|
+
"the source head's first-parent chain, is recorded as one change at the source head. "
|
|
224
|
+
"0 always records one change.",
|
|
225
|
+
)
|
|
226
|
+
derivation_max_incremental_records: int = Field(
|
|
227
|
+
default=10000,
|
|
228
|
+
ge=1,
|
|
229
|
+
description="Records one Derivation recomputes incrementally in one commit (Records "
|
|
230
|
+
"written plus Records whose Links or linked Records changed); above this the "
|
|
231
|
+
"Derivation is recomputed for every Record of its Kind, in batches of read_batch_rows.",
|
|
232
|
+
)
|
|
233
|
+
branch_mode: Literal["copy", "overlay"] = Field(
|
|
234
|
+
default="copy",
|
|
235
|
+
description="How a new branch gets its materialized head when the request names no "
|
|
236
|
+
"mode: copy (full rows, AC-16) or overlay (only the rows it changes, reading its base "
|
|
237
|
+
"as of the fork point; store schema notes).",
|
|
238
|
+
)
|
|
239
|
+
overlay_branch_prefixes: tuple[str, ...] = Field(
|
|
240
|
+
default=("agent/", "extract/", "source/"),
|
|
241
|
+
description="Branches whose name starts with one of these prefixes default to "
|
|
242
|
+
"overlay mode whatever branch_mode says.",
|
|
243
|
+
)
|
|
244
|
+
overlay_max_rows: int = Field(
|
|
245
|
+
default=50000,
|
|
246
|
+
ge=1,
|
|
247
|
+
description="Rows (Records, Links and tombstones) an overlay may hold; a commit that "
|
|
248
|
+
"leaves it with more promotes it to a full copy.",
|
|
249
|
+
)
|
|
250
|
+
overlay_max_base_lag_revisions: int = Field(
|
|
251
|
+
default=1000,
|
|
252
|
+
ge=1,
|
|
253
|
+
description="Generations the base branch may move past an overlay's fork point; "
|
|
254
|
+
"beyond, the overlay is promoted to a full copy (by its next commit, or by a "
|
|
255
|
+
"store.materialize job the base commit queues).",
|
|
256
|
+
)
|
|
257
|
+
overlay_max_per_branch: int = Field(
|
|
258
|
+
default=32,
|
|
259
|
+
ge=0,
|
|
260
|
+
description="Open overlays on one base branch (each base commit copies the rows it "
|
|
261
|
+
"changes into each of them); further branches are created as copies.",
|
|
262
|
+
)
|
|
263
|
+
async_copy_min_rows: int = Field(
|
|
264
|
+
default=10000,
|
|
265
|
+
ge=0,
|
|
266
|
+
description="A copy branch whose source holds more rows (Records and Links) is "
|
|
267
|
+
"created as an overlay at once and turned into a full copy by a store.materialize "
|
|
268
|
+
"job, in batches; smaller copies are made in the creating transaction.",
|
|
269
|
+
)
|
|
270
|
+
async_delete_min_rows: int = Field(
|
|
271
|
+
default=10000,
|
|
272
|
+
ge=0,
|
|
273
|
+
description="Deleting a branch that holds more rows (Records and Links) hides it at "
|
|
274
|
+
"once and leaves its rows to a store.branch_delete job, in batches; smaller branches "
|
|
275
|
+
"are deleted in the deleting transaction.",
|
|
276
|
+
)
|
|
277
|
+
job_batch_rows: int = Field(
|
|
278
|
+
default=5000,
|
|
279
|
+
ge=1,
|
|
280
|
+
description="Most rows a store.materialize or store.branch_delete job copies or "
|
|
281
|
+
"deletes per transaction; each batch must finish within "
|
|
282
|
+
"database.statement_timeout_ms. Batches are sized between job_batch_min_rows and this "
|
|
283
|
+
"by the time they hold the branch lock.",
|
|
284
|
+
)
|
|
285
|
+
job_batch_min_rows: int = Field(
|
|
286
|
+
default=100,
|
|
287
|
+
ge=1,
|
|
288
|
+
description="Fewest rows a branch job batch copies or deletes (job_batch_rows when "
|
|
289
|
+
"that is smaller); batches start at this size.",
|
|
290
|
+
)
|
|
291
|
+
job_batch_target_ms: int = Field(
|
|
292
|
+
default=200,
|
|
293
|
+
ge=1,
|
|
294
|
+
description="Time a branch job batch aims to hold the branch lock while no commit "
|
|
295
|
+
"waits for it; the next batch grows or shrinks toward it.",
|
|
296
|
+
)
|
|
297
|
+
job_batch_contended_ms: int = Field(
|
|
298
|
+
default=50,
|
|
299
|
+
ge=1,
|
|
300
|
+
description="Time a branch job batch aims to hold the branch lock after a commit "
|
|
301
|
+
"waited for it (the batch waited for the lock, or found a commit waiting): a commit "
|
|
302
|
+
"on a branch being copied waits about this long at most.",
|
|
303
|
+
)
|
|
304
|
+
copy_max_failures: int = Field(
|
|
305
|
+
default=3,
|
|
306
|
+
ge=1,
|
|
307
|
+
description="Failed store.materialize jobs (each after store.jobs.max_attempts) a "
|
|
308
|
+
"background copy of an overlay retries with backoff; then the overlay is detached "
|
|
309
|
+
"from its base and rebuilt from its tree instead.",
|
|
310
|
+
)
|
|
311
|
+
copy_retry_max_seconds: float = Field(
|
|
312
|
+
default=3600,
|
|
313
|
+
ge=0,
|
|
314
|
+
description="Longest delay before a failed background copy is retried; the delay "
|
|
315
|
+
"doubles from job_retry_seconds with each failure.",
|
|
316
|
+
)
|
|
317
|
+
job_concurrency: int = Field(
|
|
318
|
+
default=2,
|
|
319
|
+
ge=1,
|
|
320
|
+
description="store.materialize and store.branch_delete jobs running at once across "
|
|
321
|
+
"all workers; a job that finds every slot taken runs again after job_retry_seconds.",
|
|
322
|
+
)
|
|
323
|
+
job_retry_seconds: float = Field(
|
|
324
|
+
default=10,
|
|
325
|
+
ge=0,
|
|
326
|
+
description="Delay before a branch job that found no free slot, a busy branch or a "
|
|
327
|
+
"deletion waiting for its overlays runs again.",
|
|
328
|
+
)
|
|
329
|
+
refresh_branches_on_tag: bool = Field(
|
|
330
|
+
default=True,
|
|
331
|
+
description="When a commit or merge on a Domain's default branch adds a sensitivity "
|
|
332
|
+
"tag, queue a store.sensitivity_refresh job for every other branch whose titles or "
|
|
333
|
+
"search vectors the tag makes stale (overlays through the job of their base), and for "
|
|
334
|
+
"copies of such a branch when they are made: it rewrites, in batches of "
|
|
335
|
+
"job_batch_rows, the titles and search vectors the branch materialized before the tag "
|
|
336
|
+
"(verification reports them as sensitivity_stale). "
|
|
337
|
+
"The branch's schema is left as it is: POST .../branches/{b}/rebase-tags copies the "
|
|
338
|
+
"tags into it. Off: stale rows stay until rewritten that way, by a rebuild or by the "
|
|
339
|
+
"next schema change of their Kind on the branch.",
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class GcSettings(SectionModel):
|
|
344
|
+
"""Garbage collection of unreachable Revisions, tree nodes and objects (design 6.13)."""
|
|
345
|
+
|
|
346
|
+
grace_seconds: int = Field(
|
|
347
|
+
default=3600,
|
|
348
|
+
ge=0,
|
|
349
|
+
description="Revisions younger than this are kept with everything they reach, even when "
|
|
350
|
+
"no ref reaches them. Protects commits that land while a collection runs.",
|
|
351
|
+
)
|
|
352
|
+
mark_batch: int = Field(
|
|
353
|
+
default=1000, ge=1, description="Tree node hashes read per query while marking."
|
|
354
|
+
)
|
|
355
|
+
sweep_batch_rows: int = Field(
|
|
356
|
+
default=5000, ge=1, description="Rows deleted per transaction while sweeping."
|
|
357
|
+
)
|
|
358
|
+
guard_branch_locks: bool = Field(
|
|
359
|
+
default=True,
|
|
360
|
+
description="Take every branch lock of the Domain (without waiting) around each sweep "
|
|
361
|
+
"batch, so no commit that reuses a stored object is in flight while it is deleted. "
|
|
362
|
+
"Off: only the grace period protects commits.",
|
|
363
|
+
)
|
|
364
|
+
max_guarded_branches: int = Field(
|
|
365
|
+
default=500,
|
|
366
|
+
ge=1,
|
|
367
|
+
description="Largest number of refs whose locks one sweep batch takes; a Domain with "
|
|
368
|
+
"more refs needs a larger value (mind max_locks_per_transaction) or guard_branch_locks "
|
|
369
|
+
"off.",
|
|
370
|
+
)
|
|
371
|
+
guard_wait_seconds: float = Field(
|
|
372
|
+
default=60,
|
|
373
|
+
gt=0,
|
|
374
|
+
description="How long a sweep batch waits for commits holding a branch lock before the "
|
|
375
|
+
"collection stops; progress already made is kept.",
|
|
376
|
+
)
|
|
377
|
+
guard_retry_ms: int = Field(
|
|
378
|
+
default=200, ge=1, description="Pause between attempts to take the branch locks."
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class JobSettings(SectionModel):
|
|
383
|
+
"""Job queue workers (design 7.4): claiming, retries and stale locks."""
|
|
384
|
+
|
|
385
|
+
max_attempts: int = Field(
|
|
386
|
+
default=3, ge=1, description="Runs of a job before it is marked failed."
|
|
387
|
+
)
|
|
388
|
+
retry_delay_seconds: float = Field(
|
|
389
|
+
default=60, ge=0, description="Delay before a failed job is run again."
|
|
390
|
+
)
|
|
391
|
+
stale_after_seconds: float = Field(
|
|
392
|
+
default=90,
|
|
393
|
+
gt=0,
|
|
394
|
+
description="A running job whose last heartbeat is older than this is taken for lost "
|
|
395
|
+
"(its worker is gone) and reclaimed: queued again, or failed once it used max_attempts. "
|
|
396
|
+
"Keep it at least twice scribe.worker.heartbeat_interval_s (default three times).",
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class OutboxSettings(SectionModel):
|
|
401
|
+
"""Retention of outbox events (design 10.5), shared by every consumer of the outbox."""
|
|
402
|
+
|
|
403
|
+
retention_s: float = Field(
|
|
404
|
+
default=7 * 24 * 3600.0,
|
|
405
|
+
gt=0,
|
|
406
|
+
description="Events every consumer has handed off are deleted once older than this. "
|
|
407
|
+
"The change feed cannot resume from deleted events.",
|
|
408
|
+
)
|
|
409
|
+
max_age_s: float = Field(
|
|
410
|
+
default=30 * 24 * 3600.0,
|
|
411
|
+
gt=0,
|
|
412
|
+
description="Events older than this are deleted even when a consumer has not handed "
|
|
413
|
+
"them off yet (a consumer that stopped running); counted in lorebase.outbox.pruned "
|
|
414
|
+
"as expired.",
|
|
415
|
+
)
|
|
416
|
+
prune_batch: int = Field(
|
|
417
|
+
default=10000,
|
|
418
|
+
ge=1,
|
|
419
|
+
description="Events deleted per reason by one batch (its own short transaction). A "
|
|
420
|
+
"pruning pass, run by each webhook delivery pass, repeats batches until nothing is "
|
|
421
|
+
"left to prune or outbox.prune_budget_s is spent.",
|
|
422
|
+
)
|
|
423
|
+
prune_budget_s: float = Field(
|
|
424
|
+
default=5.0,
|
|
425
|
+
gt=0,
|
|
426
|
+
description="Time one pruning pass may spend deleting batches of outbox.prune_batch "
|
|
427
|
+
"events; no new batch starts once it is spent (a batch always runs), and the rest "
|
|
428
|
+
"waits for the next pass. Events still prunable are reported by "
|
|
429
|
+
"lorebase.outbox.prune.backlog.",
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
@model_validator(mode="after")
|
|
433
|
+
def _ordered(self) -> OutboxSettings:
|
|
434
|
+
if self.max_age_s < self.retention_s:
|
|
435
|
+
raise ValueError("outbox.max_age_s cannot be shorter than outbox.retention_s")
|
|
436
|
+
return self
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class BundleSettings(SectionModel):
|
|
440
|
+
"""Domain export and import bundles (see docs/operations.md)."""
|
|
441
|
+
|
|
442
|
+
batch_rows: int = Field(
|
|
443
|
+
default=5000, ge=1, description="Rows read, written or verified per batch."
|
|
444
|
+
)
|
|
445
|
+
compression: Literal["gzip", "none"] = Field(
|
|
446
|
+
default="gzip", description="Compression of the bundle's .jsonl files."
|
|
447
|
+
)
|
|
448
|
+
compression_level: int = Field(
|
|
449
|
+
default=6, ge=1, le=9, description="gzip level (1 fastest, 9 smallest)."
|
|
450
|
+
)
|
|
451
|
+
verify_after_import: bool = Field(
|
|
452
|
+
default=True,
|
|
453
|
+
description="Verify every rebuilt branch against its tree after an import.",
|
|
454
|
+
)
|
|
455
|
+
max_member_bytes: int = Field(
|
|
456
|
+
default=17_179_869_184,
|
|
457
|
+
ge=1,
|
|
458
|
+
description="Largest file of a bundle being imported, counted as read (after gzip).",
|
|
459
|
+
)
|
|
460
|
+
max_total_bytes: int = Field(
|
|
461
|
+
default=68_719_476_736,
|
|
462
|
+
ge=1,
|
|
463
|
+
description="Largest sum of the file sizes of a bundle being imported.",
|
|
464
|
+
)
|
|
465
|
+
max_entries: int = Field(
|
|
466
|
+
default=100_000,
|
|
467
|
+
ge=1,
|
|
468
|
+
description="Most files and directories a bundle being imported may hold.",
|
|
469
|
+
)
|
|
470
|
+
max_line_bytes: int = Field(
|
|
471
|
+
default=67_108_864,
|
|
472
|
+
ge=1,
|
|
473
|
+
description="Longest .jsonl record, and largest .json file, read into memory on import.",
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class CsvProfilingSettings(SectionModel):
|
|
478
|
+
"""Reading CSV and TSV files for schema inference."""
|
|
479
|
+
|
|
480
|
+
sample_rows: int = Field(
|
|
481
|
+
default=10000, ge=1, description="Rows kept per file; every row is still counted."
|
|
482
|
+
)
|
|
483
|
+
sampling: Literal["head", "reservoir"] = Field(
|
|
484
|
+
default="reservoir",
|
|
485
|
+
description="head keeps the first rows; reservoir keeps a uniform, seeded sample.",
|
|
486
|
+
)
|
|
487
|
+
seed: int = Field(default=0, description="Seed of reservoir sampling (same file, same rows).")
|
|
488
|
+
encodings: tuple[str, ...] = Field(
|
|
489
|
+
default=("utf-8-sig", "cp1252"),
|
|
490
|
+
min_length=1,
|
|
491
|
+
description="Encodings tried in order; the first that decodes the file wins.",
|
|
492
|
+
)
|
|
493
|
+
sniff_bytes: int = Field(
|
|
494
|
+
default=65536, ge=1024, description="Leading bytes used to detect the delimiter."
|
|
495
|
+
)
|
|
496
|
+
delimiters: str = Field(
|
|
497
|
+
default=",;\t|", min_length=1, description="Delimiters the dialect sniffer may pick."
|
|
498
|
+
)
|
|
499
|
+
max_columns: int = Field(default=500, ge=1, description="Wider files are refused.")
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class RecordProfilingSettings(SectionModel):
|
|
503
|
+
"""Profiling rows read as records (JSON, JSON Lines, Parquet and API Sources)."""
|
|
504
|
+
|
|
505
|
+
sample_rows: int = Field(
|
|
506
|
+
default=10000, ge=1, description="Rows kept per table; every row is still counted."
|
|
507
|
+
)
|
|
508
|
+
sampling: Literal["head", "reservoir"] = Field(
|
|
509
|
+
default="reservoir",
|
|
510
|
+
description="head keeps the first rows; reservoir keeps a uniform, seeded sample.",
|
|
511
|
+
)
|
|
512
|
+
seed: int = Field(default=0, description="Seed of reservoir sampling (same rows, same sample).")
|
|
513
|
+
max_columns: int = Field(
|
|
514
|
+
default=500, ge=1, description="Tables with more distinct columns are refused."
|
|
515
|
+
)
|
|
516
|
+
batch_rows: int = Field(
|
|
517
|
+
default=10000, ge=1, description="Rows decoded per batch of an uploaded Parquet file."
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class PostgresProfilingSettings(SectionModel):
|
|
522
|
+
"""Reading Postgres tables for schema inference: catalog reflection plus sampled SELECTs."""
|
|
523
|
+
|
|
524
|
+
sample_rows: int = Field(default=10000, ge=1, description="Rows read per table (LIMIT).")
|
|
525
|
+
max_tables: int = Field(default=200, ge=1, description="Tables profiled per request.")
|
|
526
|
+
statement_timeout_ms: int = Field(
|
|
527
|
+
default=15000, ge=1, description="statement_timeout of each sampling query."
|
|
528
|
+
)
|
|
529
|
+
exact_row_count: bool = Field(
|
|
530
|
+
default=False,
|
|
531
|
+
description="count(*) each table; false uses the planner estimate (pg_class.reltuples).",
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class SqlProfilingSettings(SectionModel):
|
|
536
|
+
"""Reading MySQL, MariaDB and SQL Server tables for schema inference: catalog reflection
|
|
537
|
+
plus a bounded SELECT per table, profiled as records. The drivers are optional extras
|
|
538
|
+
(`lorebase-keeper[mysql]`, `lorebase-keeper[mssql]`)."""
|
|
539
|
+
|
|
540
|
+
sample_rows: int = Field(default=10000, ge=1, description="Rows read per table (LIMIT/TOP).")
|
|
541
|
+
max_tables: int = Field(default=50, ge=1, description="Tables profiled per request.")
|
|
542
|
+
total_timeout_s: float = Field(
|
|
543
|
+
default=120.0,
|
|
544
|
+
gt=0,
|
|
545
|
+
description="Longest time a request may spend reading tables, checked before each "
|
|
546
|
+
"table; the request is refused when it is exceeded.",
|
|
547
|
+
)
|
|
548
|
+
max_value_chars: int = Field(
|
|
549
|
+
default=1000,
|
|
550
|
+
ge=1,
|
|
551
|
+
description="Longest text kept per value: longer text and unbounded text columns are "
|
|
552
|
+
"cut when read; binary columns without a declared length up to this size, and columns "
|
|
553
|
+
"of unknown type, are not read.",
|
|
554
|
+
)
|
|
555
|
+
statement_timeout_ms: int = Field(
|
|
556
|
+
default=15000,
|
|
557
|
+
ge=1,
|
|
558
|
+
description="Longest execution of each query: max_execution_time (MySQL), "
|
|
559
|
+
"max_statement_time (MariaDB, whole seconds), the driver timeout and LOCK_TIMEOUT "
|
|
560
|
+
"(SQL Server).",
|
|
561
|
+
)
|
|
562
|
+
connect_timeout_s: float = Field(default=10.0, gt=0, description="Connection timeout.")
|
|
563
|
+
count_rows: bool = Field(
|
|
564
|
+
default=True,
|
|
565
|
+
description="count(*) each table so that inference knows the sample is partial; false "
|
|
566
|
+
"takes the sampled rows as the whole table.",
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class ProfilingSettings(SectionModel):
|
|
571
|
+
"""Profilers that feed schema inference (design 9.1)."""
|
|
572
|
+
|
|
573
|
+
csv: CsvProfilingSettings = Field(default_factory=CsvProfilingSettings)
|
|
574
|
+
records: RecordProfilingSettings = Field(default_factory=RecordProfilingSettings)
|
|
575
|
+
postgres: PostgresProfilingSettings = Field(default_factory=PostgresProfilingSettings)
|
|
576
|
+
sql: SqlProfilingSettings = Field(default_factory=SqlProfilingSettings)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
class IndexSettings(SectionModel):
|
|
580
|
+
"""Per-Attribute partial indexes on `mat_records` (`lorebase_store.indexes`)."""
|
|
581
|
+
|
|
582
|
+
enabled: bool = Field(
|
|
583
|
+
default=True,
|
|
584
|
+
description="Create and drop per-Attribute indexes when schemas change. A role that "
|
|
585
|
+
"owns mat_records does so in the commit; other roles queue the changes for the job "
|
|
586
|
+
"worker when database.maintenance_dsn is set, and skip them with a warning otherwise. "
|
|
587
|
+
"False leaves index management to an administrator.",
|
|
588
|
+
)
|
|
589
|
+
inline_max_rows: int = Field(
|
|
590
|
+
default=50000,
|
|
591
|
+
ge=0,
|
|
592
|
+
description="Kinds with at most this many Records on the branch get their indexes "
|
|
593
|
+
"inside the commit transaction; larger ones are queued as jobs and built concurrently.",
|
|
594
|
+
)
|
|
595
|
+
job_batch: int = Field(
|
|
596
|
+
default=10, ge=1, description="Queued index jobs claimed per run of the job runner."
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
class DefinerCeilingSettings(SectionModel):
|
|
601
|
+
"""Largest count arguments the security definer read functions accept.
|
|
602
|
+
|
|
603
|
+
Read when revision 0008 runs and written into the functions, which refuse NULL and values
|
|
604
|
+
outside 1..ceiling; changing them later has no effect. Each is at least the matching cap of
|
|
605
|
+
`store.reads` or `store.traverse` (checked when the settings load).
|
|
606
|
+
"""
|
|
607
|
+
|
|
608
|
+
limit: int = Field(
|
|
609
|
+
default=10000,
|
|
610
|
+
ge=1,
|
|
611
|
+
description="Rows returned by lb_search, lb_links_in and lb_query_by_attribute; at "
|
|
612
|
+
"least reads.max_page_rows and reads.max_search_rows.",
|
|
613
|
+
)
|
|
614
|
+
candidates: int = Field(
|
|
615
|
+
default=100000,
|
|
616
|
+
ge=1,
|
|
617
|
+
description="Candidates ranked by lb_search; at least reads.search_candidates.",
|
|
618
|
+
)
|
|
619
|
+
depth: int = Field(
|
|
620
|
+
default=10, ge=1, description="Hops of lb_neighborhood; at least traverse.max_depth."
|
|
621
|
+
)
|
|
622
|
+
fanout: int = Field(
|
|
623
|
+
default=10000,
|
|
624
|
+
ge=1,
|
|
625
|
+
description="Links per Record and hop of lb_neighborhood; at least traverse.max_fanout.",
|
|
626
|
+
)
|
|
627
|
+
max_steps: int = Field(
|
|
628
|
+
default=1000000,
|
|
629
|
+
ge=1,
|
|
630
|
+
description="Steps (Links walked plus seeds) of lb_neighborhood; at least "
|
|
631
|
+
"traverse.max_links plus traverse.max_seeds.",
|
|
632
|
+
)
|
|
633
|
+
max_nodes: int = Field(
|
|
634
|
+
default=100000,
|
|
635
|
+
ge=1,
|
|
636
|
+
description="Records returned by lb_neighborhood; at least traverse.max_nodes.",
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
class ReadSettings(SectionModel):
|
|
641
|
+
"""Read primitives on materialized heads (`lorebase_store.reads`)."""
|
|
642
|
+
|
|
643
|
+
schema_cache_entries: int = Field(
|
|
644
|
+
default=256,
|
|
645
|
+
ge=0,
|
|
646
|
+
description="Compiled schemas cached per process, keyed by Domain and head Revision "
|
|
647
|
+
"id; 0 disables the cache.",
|
|
648
|
+
)
|
|
649
|
+
max_page_rows: int = Field(
|
|
650
|
+
default=500, ge=1, description="Largest page of Records, Links or query results."
|
|
651
|
+
)
|
|
652
|
+
max_search_rows: int = Field(
|
|
653
|
+
default=100, ge=1, description="Largest number of full-text search results."
|
|
654
|
+
)
|
|
655
|
+
search_candidates: int = Field(
|
|
656
|
+
default=1000,
|
|
657
|
+
ge=1,
|
|
658
|
+
description="Matching Records ranked per search; ranking reads each candidate's search "
|
|
659
|
+
"vector, so this bounds the cost of very common terms.",
|
|
660
|
+
)
|
|
661
|
+
max_hydrate_keys: int = Field(
|
|
662
|
+
default=1000, ge=1, description="Records fetched by one batched hydration."
|
|
663
|
+
)
|
|
664
|
+
use_definer_functions: bool = Field(
|
|
665
|
+
default=True,
|
|
666
|
+
description="Run search, incoming Links, neighborhoods and attribute queries through "
|
|
667
|
+
"the security definer functions of revision 0005, whose index plans survive row-level "
|
|
668
|
+
"security; false runs the same statements as plain SQL.",
|
|
669
|
+
)
|
|
670
|
+
definer_ceilings: DefinerCeilingSettings = Field(default_factory=DefinerCeilingSettings)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
class TraverseSettings(SectionModel):
|
|
674
|
+
"""Neighborhood traversal over `mat_links` (`lorebase_store.traverse`) and the fan-out of
|
|
675
|
+
path Derivations (`lorebase_core.model.paths`)."""
|
|
676
|
+
|
|
677
|
+
max_depth: int = Field(default=3, ge=1, le=10, description="Deepest neighborhood, in hops.")
|
|
678
|
+
max_seeds: int = Field(default=50, ge=1, description="Seed Records of one neighborhood.")
|
|
679
|
+
max_fanout: int = Field(
|
|
680
|
+
default=50, ge=1, description="Links followed from one Record in one hop."
|
|
681
|
+
)
|
|
682
|
+
max_links: int = Field(
|
|
683
|
+
default=2000,
|
|
684
|
+
ge=1,
|
|
685
|
+
description="Links walked by one neighborhood; the walk stops, breadth first, there.",
|
|
686
|
+
)
|
|
687
|
+
max_nodes: int = Field(
|
|
688
|
+
default=200, ge=1, description="Records returned by one neighborhood, nearest first."
|
|
689
|
+
)
|
|
690
|
+
path_fanout: int = Field(
|
|
691
|
+
default=100,
|
|
692
|
+
ge=1,
|
|
693
|
+
description="Links a path Derivation follows from one intermediate Record (smallest "
|
|
694
|
+
"far keys first), at read time and when materialized; a new value rewrites the "
|
|
695
|
+
"materialized paths and the derived values reading a path at the next commit of a "
|
|
696
|
+
"branch, or in the background when a job worker starts. Every process of a "
|
|
697
|
+
"deployment (Keeper, Scribe, job workers) must use the same value.",
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
StorageProfile = Literal["compact", "balanced", "fast"]
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
class StorageSettings(SectionModel):
|
|
705
|
+
"""Storage profile: what the database keeps to trade memory and disk for speed
|
|
706
|
+
(`lorebase_store.storage`, docs/performance.md#storage-profiles).
|
|
707
|
+
|
|
708
|
+
`profile` picks every choice; each other key, when set, overrides the profile's. A change
|
|
709
|
+
takes effect when `python -m lorebase_store.storage apply` runs (batched, resumable,
|
|
710
|
+
reads keep working); until then the database keeps what it has.
|
|
711
|
+
"""
|
|
712
|
+
|
|
713
|
+
profile: StorageProfile = Field(
|
|
714
|
+
default="balanced",
|
|
715
|
+
description="compact: smallest footprint, every feature works, some reads slower; "
|
|
716
|
+
"balanced: the default; fast: more indexes and memory for the lowest latency.",
|
|
717
|
+
)
|
|
718
|
+
lineage_maps: Literal["shared", "inline"] | None = Field(
|
|
719
|
+
default=None,
|
|
720
|
+
description="shared: each distinct lineage map is stored once per Domain and rows "
|
|
721
|
+
"name it (smaller Record and Link rows); inline: every row holds its maps. Default: "
|
|
722
|
+
"shared, inline with fast.",
|
|
723
|
+
)
|
|
724
|
+
values_index: bool | None = Field(
|
|
725
|
+
default=None,
|
|
726
|
+
description="GIN index on Record values, which serves equality on Attributes without "
|
|
727
|
+
"their own index (queries). Default: off with compact.",
|
|
728
|
+
)
|
|
729
|
+
attribute_indexes: Literal["all", "constraints"] | None = Field(
|
|
730
|
+
default=None,
|
|
731
|
+
description="Per-Attribute indexes built: all (indexed, unique and foreign key "
|
|
732
|
+
"Attributes) or constraints (unique and foreign key only; filters on other Attributes "
|
|
733
|
+
"scan the Kind). Default: constraints with compact.",
|
|
734
|
+
)
|
|
735
|
+
lexicon_index: bool | None = Field(
|
|
736
|
+
default=None,
|
|
737
|
+
description="Trigram index of the word lexicon used by typo-tolerant search; without "
|
|
738
|
+
"it corrections compare every word of the searched Kinds. Default: off with compact.",
|
|
739
|
+
)
|
|
740
|
+
embedding_precision: Literal["half", "full"] | None = Field(
|
|
741
|
+
default=None,
|
|
742
|
+
description="Precision of the HNSW index of embeddings: half (halfvec, about 40% "
|
|
743
|
+
"smaller, a few points of recall lost) or full. Stored vectors keep full precision "
|
|
744
|
+
"either way. Default: half with compact.",
|
|
745
|
+
)
|
|
746
|
+
compression: Literal["lz4", "pglz"] | None = Field(
|
|
747
|
+
default=None,
|
|
748
|
+
description="TOAST compression of large values (rows above about 2 kB). Default: pglz "
|
|
749
|
+
"with compact (smaller), lz4 otherwise (faster).",
|
|
750
|
+
)
|
|
751
|
+
records_fillfactor: int | None = Field(
|
|
752
|
+
default=None,
|
|
753
|
+
ge=50,
|
|
754
|
+
le=100,
|
|
755
|
+
description="Fill factor of the Record table: room left in each page for updates. "
|
|
756
|
+
"Default: 90 with fast, 100 otherwise.",
|
|
757
|
+
)
|
|
758
|
+
prewarm: bool | None = Field(
|
|
759
|
+
default=None,
|
|
760
|
+
description="`storage apply` ends by loading the hot relations into shared buffers "
|
|
761
|
+
"with pg_prewarm (as `storage prewarm` does on request); after a restart, autoprewarm "
|
|
762
|
+
"of the server reloads them. Default: on with fast.",
|
|
763
|
+
)
|
|
764
|
+
bm25_index: bool | None = Field(
|
|
765
|
+
default=None,
|
|
766
|
+
description="BM25 index of lexical search on mat_records (ParadeDB pg_search, an "
|
|
767
|
+
"AGPL-3.0 extension of an optional image; docs/search.md#bm25-ranking). Needs a server "
|
|
768
|
+
"that preloads pg_search; elsewhere `storage apply` skips it. Keeper ranks with it "
|
|
769
|
+
"under search.backend: bm25. Default: off in every profile.",
|
|
770
|
+
)
|
|
771
|
+
prewarm_fraction: float = Field(
|
|
772
|
+
default=0.8,
|
|
773
|
+
gt=0,
|
|
774
|
+
le=1,
|
|
775
|
+
description="Share of shared_buffers the prewarm may fill.",
|
|
776
|
+
)
|
|
777
|
+
batch_rows: int = Field(
|
|
778
|
+
default=2000,
|
|
779
|
+
ge=1,
|
|
780
|
+
description="Rows rewritten per transaction by `storage apply` (lineage maps, compaction).",
|
|
781
|
+
)
|
|
782
|
+
lock_timeout_ms: int = Field(
|
|
783
|
+
default=2000,
|
|
784
|
+
ge=0,
|
|
785
|
+
description="Wait for a table lock taken by `storage apply` (index drops, trigger "
|
|
786
|
+
"switches) before retrying later; reads queue behind a waiting lock, so keep it short.",
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
class StoreSettings(SectionModel):
|
|
791
|
+
"""Schema shape, bulk writes, node cache, commits, materialization, indexes, reads and
|
|
792
|
+
profiling."""
|
|
793
|
+
|
|
794
|
+
layout: LayoutSettings = Field(default_factory=LayoutSettings)
|
|
795
|
+
storage: StorageSettings = Field(default_factory=StorageSettings)
|
|
796
|
+
embeddings: EmbeddingStoreSettings = Field(default_factory=EmbeddingStoreSettings)
|
|
797
|
+
writes: WriteSettings = Field(default_factory=WriteSettings)
|
|
798
|
+
nodes: NodeSettings = Field(default_factory=NodeSettings)
|
|
799
|
+
commit: CommitSettings = Field(default_factory=CommitSettings)
|
|
800
|
+
materialize: MaterializeSettings = Field(default_factory=MaterializeSettings)
|
|
801
|
+
gc: GcSettings = Field(default_factory=GcSettings)
|
|
802
|
+
jobs: JobSettings = Field(default_factory=JobSettings)
|
|
803
|
+
outbox: OutboxSettings = Field(default_factory=OutboxSettings)
|
|
804
|
+
bundle: BundleSettings = Field(default_factory=BundleSettings)
|
|
805
|
+
profiling: ProfilingSettings = Field(default_factory=ProfilingSettings)
|
|
806
|
+
indexes: IndexSettings = Field(default_factory=IndexSettings)
|
|
807
|
+
reads: ReadSettings = Field(default_factory=ReadSettings)
|
|
808
|
+
traverse: TraverseSettings = Field(default_factory=TraverseSettings)
|
|
809
|
+
|
|
810
|
+
@model_validator(mode="after")
|
|
811
|
+
def _caps_within_definer_ceilings(self) -> StoreSettings:
|
|
812
|
+
"""The read primitives never pass the definer functions a count above its ceiling."""
|
|
813
|
+
if not self.reads.use_definer_functions:
|
|
814
|
+
return self
|
|
815
|
+
ceilings, reads, walk = self.reads.definer_ceilings, self.reads, self.traverse
|
|
816
|
+
for name, cap, ceiling in (
|
|
817
|
+
("reads.max_page_rows", reads.max_page_rows, ceilings.limit),
|
|
818
|
+
("reads.max_search_rows", reads.max_search_rows, ceilings.limit),
|
|
819
|
+
("reads.search_candidates", reads.search_candidates, ceilings.candidates),
|
|
820
|
+
("traverse.max_depth", walk.max_depth, ceilings.depth),
|
|
821
|
+
("traverse.max_fanout", walk.max_fanout, ceilings.fanout),
|
|
822
|
+
("traverse.max_links + max_seeds", walk.max_links + walk.max_seeds, ceilings.max_steps),
|
|
823
|
+
("traverse.max_nodes", walk.max_nodes, ceilings.max_nodes),
|
|
824
|
+
):
|
|
825
|
+
if cap > ceiling:
|
|
826
|
+
raise ValueError(
|
|
827
|
+
f"{name} ({cap}) exceeds its ceiling in reads.definer_ceilings ({ceiling})"
|
|
828
|
+
)
|
|
829
|
+
return self
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
class SchemaSettings(SectionModel):
|
|
833
|
+
"""Static validation of schemas (design 5.7, 5.8, AC-06), shared by Keeper and the store."""
|
|
834
|
+
|
|
835
|
+
expressions_enabled: bool = Field(
|
|
836
|
+
default=True,
|
|
837
|
+
description="Accept Operations and Derivations written in CEL; every expression is "
|
|
838
|
+
"parsed and type checked against the schema. False refuses them "
|
|
839
|
+
"(schema.expressions_disabled) wherever a schema is validated.",
|
|
840
|
+
)
|
|
841
|
+
expression_limits: ExprLimits = Field(
|
|
842
|
+
default_factory=ExprLimits,
|
|
843
|
+
description="Budget of one schema expression: compiled at validation, evaluated by "
|
|
844
|
+
"the materializer for Derivations (source length, nesting depth, evaluation steps "
|
|
845
|
+
"and time, sizes of built strings and lists, Record lookups, decimal precision).",
|
|
846
|
+
)
|
|
847
|
+
suggestions: SuggestionOptions = Field(
|
|
848
|
+
default_factory=SuggestionOptions,
|
|
849
|
+
description="Schema suggestions aggregated from document extraction: support needed "
|
|
850
|
+
"to list one, how many are listed, examples and samples kept, name matching against "
|
|
851
|
+
"the schema (docs/extraction.md#schema-suggestions).",
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
def rules(self) -> SchemaRules:
|
|
855
|
+
"""Options of `validate_schema` for these settings."""
|
|
856
|
+
return SchemaRules(
|
|
857
|
+
allow_expressions=self.expressions_enabled,
|
|
858
|
+
expression_limits=self.expression_limits,
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
register_section(DATABASE_SECTION, DatabaseSettings, env_aliases=ENV_ALIASES)
|
|
863
|
+
register_section(STORE_SECTION, StoreSettings)
|
|
864
|
+
register_section(SCHEMA_SECTION, SchemaSettings)
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def database_settings(settings: Settings) -> DatabaseSettings:
|
|
868
|
+
"""The validated `database` section of loaded settings."""
|
|
869
|
+
return settings.section(DATABASE_SECTION, DatabaseSettings)
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def store_settings(settings: Settings) -> StoreSettings:
|
|
873
|
+
"""The validated `store` section of loaded settings."""
|
|
874
|
+
return settings.section(STORE_SECTION, StoreSettings)
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def schema_settings(settings: Settings) -> SchemaSettings:
|
|
878
|
+
"""The validated `schema` section of loaded settings."""
|
|
879
|
+
return settings.section(SCHEMA_SECTION, SchemaSettings)
|