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,582 @@
|
|
|
1
|
+
"""Overlay branches (design 6.11, AC-16; store schema notes).
|
|
2
|
+
|
|
3
|
+
An overlay branch stores only the materialized rows it changed and reads the rows of its base
|
|
4
|
+
branch as they were when it was forked:
|
|
5
|
+
|
|
6
|
+
- `refs.base_branch`, `refs.base_revision` and `refs.base_generation` name the base and the
|
|
7
|
+
fork point; they are set exactly when `materialization = 'overlay'`. Both references are
|
|
8
|
+
checked at commit time, like `refs.head` (revision `0004`), so deleting a Domain cascades in
|
|
9
|
+
any order; a base branch cannot be deleted while an overlay depends on it.
|
|
10
|
+
- `mat_records.revision_generation` and `mat_links.revision_generation` hold the generation of
|
|
11
|
+
the Revision that last wrote the row (`0` for rows written before this revision, when no
|
|
12
|
+
overlay existed); `deleted` marks the tombstones of rows an overlay removed.
|
|
13
|
+
- `lb_overlay_rows(domain, branch, table)` renders the resolved rows of an overlay as SQL
|
|
14
|
+
text (`lorebase_store.layout` renders the same text in Python), and the read functions of
|
|
15
|
+
revision `0005` get overlay variants (`lb_search_overlay`, `lb_links_in_overlay`,
|
|
16
|
+
`lb_neighborhood_overlay`, `lb_query_by_attribute_overlay`) that read through it. They
|
|
17
|
+
have the signatures, options and grants of the originals and the argument, scope and
|
|
18
|
+
search_path checks revision `0008` added to them (counts bounded by
|
|
19
|
+
`store.reads.definer_ceilings` as read when this revision runs); on a branch that is not (or
|
|
20
|
+
no longer) an overlay they read its own rows, so a caller that resolved the layout before a
|
|
21
|
+
promotion still gets correct rows. The originals stay unchanged for full branches.
|
|
22
|
+
|
|
23
|
+
Like revision `0008`, the upgrade refuses to run while `database.application_role` may create
|
|
24
|
+
objects in the schema of the tables or of pg_trgm.
|
|
25
|
+
|
|
26
|
+
Revision ID: 0010
|
|
27
|
+
Revises: 0009
|
|
28
|
+
Create Date: 2026-10-21 18:00:00.000000
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import importlib.util
|
|
34
|
+
from collections.abc import Sequence
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from types import ModuleType
|
|
37
|
+
|
|
38
|
+
import sqlalchemy as sa
|
|
39
|
+
from alembic import op
|
|
40
|
+
|
|
41
|
+
revision: str = "0010"
|
|
42
|
+
down_revision: str | Sequence[str] | None = "0009"
|
|
43
|
+
branch_labels: str | Sequence[str] | None = None
|
|
44
|
+
depends_on: str | Sequence[str] | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _revision_module(filename: str) -> ModuleType:
|
|
48
|
+
"""An earlier revision file of this directory, loaded for its SQL templates and checks."""
|
|
49
|
+
path = Path(__file__).with_name(filename)
|
|
50
|
+
spec = importlib.util.spec_from_file_location(f"_lorebase_revision_{path.stem}", path)
|
|
51
|
+
if spec is None or spec.loader is None:
|
|
52
|
+
raise RuntimeError(f"cannot load migration {path}")
|
|
53
|
+
module = importlib.util.module_from_spec(spec)
|
|
54
|
+
spec.loader.exec_module(module)
|
|
55
|
+
return module
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_checks = _revision_module("0008_definer_argument_checks.py")
|
|
59
|
+
_reads = _checks._reads
|
|
60
|
+
|
|
61
|
+
HELPER = "lb_overlay_rows(text, text, text, text)"
|
|
62
|
+
SIGNATURES = (
|
|
63
|
+
"lb_search_overlay(text, text, text[], regconfig, text, integer, integer, real, real)",
|
|
64
|
+
"lb_links_in_overlay(text, text, text, text, integer, text, text, text)",
|
|
65
|
+
"lb_neighborhood_overlay(text, text, jsonb, integer, jsonb, integer, integer, integer)",
|
|
66
|
+
"lb_query_by_attribute_overlay(text, text, text, text, text, text, text, integer, text, text)",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
_RECORD_COLUMNS = (
|
|
70
|
+
"domain_id",
|
|
71
|
+
"branch",
|
|
72
|
+
"kind_id",
|
|
73
|
+
"key",
|
|
74
|
+
"values",
|
|
75
|
+
"derived",
|
|
76
|
+
"title",
|
|
77
|
+
"search",
|
|
78
|
+
"object_hash",
|
|
79
|
+
"lineage",
|
|
80
|
+
"updated_revision",
|
|
81
|
+
"updated_at",
|
|
82
|
+
"revision_generation",
|
|
83
|
+
"deleted",
|
|
84
|
+
)
|
|
85
|
+
_LINK_COLUMNS = (
|
|
86
|
+
"domain_id",
|
|
87
|
+
"branch",
|
|
88
|
+
"relation_id",
|
|
89
|
+
"from_key",
|
|
90
|
+
"to_key",
|
|
91
|
+
"values",
|
|
92
|
+
"object_hash",
|
|
93
|
+
"lineage",
|
|
94
|
+
"revision_generation",
|
|
95
|
+
"deleted",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _array(columns: Sequence[str]) -> str:
|
|
100
|
+
return "ARRAY[" + ", ".join(f"'{c}'" for c in columns) + "]"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
LB_OVERLAY_ROWS = f"""
|
|
104
|
+
CREATE FUNCTION lb_overlay_rows(
|
|
105
|
+
p_domain text, p_branch text, p_table text, p_tail text DEFAULT NULL)
|
|
106
|
+
RETURNS text
|
|
107
|
+
LANGUAGE plpgsql STABLE
|
|
108
|
+
SET search_path = {{search_path}}
|
|
109
|
+
AS $fn$
|
|
110
|
+
DECLARE
|
|
111
|
+
v_columns text[];
|
|
112
|
+
v_key text[];
|
|
113
|
+
v_base text;
|
|
114
|
+
v_generation bigint;
|
|
115
|
+
v_o text := CASE WHEN p_tail IS NULL THEN 'o' ELSE 'm' END;
|
|
116
|
+
v_s text := CASE WHEN p_tail IS NULL THEN 's' ELSE 'm' END;
|
|
117
|
+
v_own text;
|
|
118
|
+
v_from_base text;
|
|
119
|
+
v_shadow text;
|
|
120
|
+
BEGIN
|
|
121
|
+
IF p_table = 'mat_records' THEN
|
|
122
|
+
v_columns := {_array(_RECORD_COLUMNS)};
|
|
123
|
+
v_key := ARRAY['kind_id', 'key'];
|
|
124
|
+
ELSIF p_table = 'mat_links' THEN
|
|
125
|
+
v_columns := {_array(_LINK_COLUMNS)};
|
|
126
|
+
v_key := ARRAY['relation_id', 'from_key', 'to_key'];
|
|
127
|
+
ELSE
|
|
128
|
+
RAISE EXCEPTION 'unknown materialized table %', p_table USING ERRCODE = '22023';
|
|
129
|
+
END IF;
|
|
130
|
+
SELECT r.base_branch, r.base_generation INTO v_base, v_generation
|
|
131
|
+
FROM refs r
|
|
132
|
+
WHERE r.domain_id = p_domain AND r.name = p_branch AND r.materialization = 'overlay';
|
|
133
|
+
IF v_base IS NULL THEN
|
|
134
|
+
RETURN NULL;
|
|
135
|
+
END IF;
|
|
136
|
+
SELECT string_agg(v_o || '.' || quote_ident(c), ', ' ORDER BY n) INTO v_own
|
|
137
|
+
FROM unnest(v_columns) WITH ORDINALITY AS t(c, n);
|
|
138
|
+
SELECT string_agg(
|
|
139
|
+
CASE WHEN c = 'branch' THEN format('CAST(%L AS text)', p_branch)
|
|
140
|
+
ELSE v_s || '.' || quote_ident(c) END, ', ' ORDER BY n)
|
|
141
|
+
INTO v_from_base FROM unnest(v_columns) WITH ORDINALITY AS t(c, n);
|
|
142
|
+
SELECT string_agg(format('x.%1$I = %2$s.%1$I', c, v_s), ' AND ' ORDER BY n)
|
|
143
|
+
INTO v_shadow FROM unnest(v_key) WITH ORDINALITY AS t(c, n);
|
|
144
|
+
-- The same rows as lorebase_store.layout.BranchLayout: overlay rows that are not
|
|
145
|
+
-- tombstones, then base rows written at or before the fork that the overlay does not
|
|
146
|
+
-- shadow, while the ref is still that overlay. With `p_tail` (conditions on alias m,
|
|
147
|
+
-- ORDER BY, LIMIT; built by the read functions only) each half ends with it, as
|
|
148
|
+
-- BranchLayout.records_arms does.
|
|
149
|
+
v_own := format(
|
|
150
|
+
'SELECT %1$s FROM %2$I %5$s WHERE %5$s.domain_id = %3$L AND %5$s.branch = %4$L '
|
|
151
|
+
'AND NOT %5$s.deleted', v_own, p_table, p_domain, p_branch, v_o);
|
|
152
|
+
v_from_base := format(
|
|
153
|
+
'SELECT %1$s FROM %2$I %7$s WHERE %7$s.domain_id = %3$L AND %7$s.branch = %4$L '
|
|
154
|
+
'AND %7$s.revision_generation <= %5$s AND EXISTS (SELECT 1 FROM refs f '
|
|
155
|
+
'WHERE f.domain_id = %3$L AND f.name = %6$L AND f.base_branch = %4$L '
|
|
156
|
+
'AND f.base_generation = %5$s OFFSET 0) AND NOT EXISTS (SELECT 1 FROM %2$I x '
|
|
157
|
+
'WHERE x.domain_id = %3$L AND x.branch = %6$L AND %8$s OFFSET 0)',
|
|
158
|
+
v_from_base, p_table, p_domain, v_base, v_generation, p_branch, v_s, v_shadow);
|
|
159
|
+
IF p_tail IS NULL THEN
|
|
160
|
+
RETURN '(' || v_own || ' UNION ALL ' || v_from_base || ')';
|
|
161
|
+
END IF;
|
|
162
|
+
RETURN '((' || v_own || p_tail || ') UNION ALL (' || v_from_base || p_tail || '))';
|
|
163
|
+
END
|
|
164
|
+
$fn$
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
_SOURCES = """
|
|
168
|
+
v_records text;
|
|
169
|
+
v_links text;
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
_RESOLVE = """
|
|
173
|
+
v_records := coalesce(lb_overlay_rows(p_domain, p_branch, 'mat_records'), 'mat_records');
|
|
174
|
+
v_links := coalesce(lb_overlay_rows(p_domain, p_branch, 'mat_links'), 'mat_links');
|
|
175
|
+
"""
|
|
176
|
+
"""Run after the checks of revision 0008: the branch's rows are resolved only for a Domain in
|
|
177
|
+
the transaction scope, through the checked search_path."""
|
|
178
|
+
|
|
179
|
+
LB_SEARCH_OVERLAY = """
|
|
180
|
+
CREATE FUNCTION lb_search_overlay(
|
|
181
|
+
p_domain text, p_branch text, p_kinds text[], p_config regconfig, p_query text,
|
|
182
|
+
p_candidates integer, p_limit integer,
|
|
183
|
+
p_trigram_threshold real DEFAULT NULL, p_trigram_weight real DEFAULT 0)
|
|
184
|
+
RETURNS TABLE (kind_id text, key text, title text, score real)
|
|
185
|
+
{options}
|
|
186
|
+
SET pg_trgm.similarity_threshold = '0.3'
|
|
187
|
+
AS $fn$
|
|
188
|
+
DECLARE
|
|
189
|
+
v_match text := 'm.search @@ q.ts';
|
|
190
|
+
v_score text := 'ts_rank_cd(c.search, q.ts)';
|
|
191
|
+
v_kinds text := '';
|
|
192
|
+
{sources}
|
|
193
|
+
BEGIN
|
|
194
|
+
IF p_candidates < 1 OR p_limit < 1 THEN
|
|
195
|
+
RAISE EXCEPTION 'candidates and limit must be positive' USING ERRCODE = '22023';
|
|
196
|
+
END IF;
|
|
197
|
+
IF p_kinds IS NOT NULL THEN
|
|
198
|
+
v_kinds := ' AND m.kind_id = ANY ($3)';
|
|
199
|
+
END IF;
|
|
200
|
+
IF p_trigram_threshold IS NOT NULL THEN
|
|
201
|
+
PERFORM set_config('pg_trgm.similarity_threshold', p_trigram_threshold::text, true);
|
|
202
|
+
v_match := '(m.search @@ q.ts OR m.title % $5)';
|
|
203
|
+
v_score := v_score || ' + $9 * similarity(coalesce(c.title, ''''), $5)';
|
|
204
|
+
END IF;
|
|
205
|
+
RETURN QUERY EXECUTE format(
|
|
206
|
+
'SELECT c.kind_id, c.key, c.title, CAST(%s AS real) AS score '
|
|
207
|
+
'FROM (SELECT m.kind_id, m.key, m.title, m.search '
|
|
208
|
+
' FROM %s m, (SELECT websearch_to_tsquery($4, $5) AS ts) q '
|
|
209
|
+
' WHERE m.domain_id = %L AND m.branch = %L%s AND %s LIMIT $6) AS c, '
|
|
210
|
+
' (SELECT websearch_to_tsquery($4, $5) AS ts) q '
|
|
211
|
+
'ORDER BY score DESC, c.kind_id, c.key LIMIT $7',
|
|
212
|
+
v_score, v_records, p_domain, p_branch, v_kinds, v_match)
|
|
213
|
+
USING p_domain, p_branch, p_kinds, p_config, p_query, p_candidates, p_limit,
|
|
214
|
+
p_trigram_threshold, p_trigram_weight;
|
|
215
|
+
END
|
|
216
|
+
$fn$
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
LB_LINKS_IN_OVERLAY = """
|
|
220
|
+
CREATE FUNCTION lb_links_in_overlay(
|
|
221
|
+
p_domain text, p_branch text, p_relation text, p_to_key text, p_limit integer,
|
|
222
|
+
p_after text DEFAULT NULL, p_from_kind text DEFAULT NULL, p_attribute text DEFAULT NULL)
|
|
223
|
+
RETURNS TABLE (relation_id text, from_key text, to_key text, "values" jsonb,
|
|
224
|
+
object_hash bytea)
|
|
225
|
+
{options}
|
|
226
|
+
AS $fn$
|
|
227
|
+
DECLARE
|
|
228
|
+
v_after text := '';
|
|
229
|
+
{sources}
|
|
230
|
+
BEGIN
|
|
231
|
+
IF p_limit < 1 THEN
|
|
232
|
+
RAISE EXCEPTION 'limit must be positive' USING ERRCODE = '22023';
|
|
233
|
+
END IF;
|
|
234
|
+
IF (p_from_kind IS NULL) <> (p_attribute IS NULL) THEN
|
|
235
|
+
RAISE EXCEPTION 'a foreign-key Relation needs both its from Kind and its Attribute'
|
|
236
|
+
USING ERRCODE = '22023';
|
|
237
|
+
END IF;
|
|
238
|
+
IF p_attribute IS NULL THEN
|
|
239
|
+
IF p_after IS NOT NULL THEN
|
|
240
|
+
v_after := ' AND l.from_key > $5';
|
|
241
|
+
END IF;
|
|
242
|
+
RETURN QUERY EXECUTE format(
|
|
243
|
+
'SELECT l.relation_id, l.from_key, l.to_key, l."values", l.object_hash '
|
|
244
|
+
'FROM %s l WHERE l.domain_id = %L AND l.branch = %L '
|
|
245
|
+
'AND l.relation_id = $3 AND l.to_key = $4%s ORDER BY l.from_key LIMIT $6',
|
|
246
|
+
v_links, p_domain, p_branch, v_after)
|
|
247
|
+
USING p_domain, p_branch, p_relation, p_to_key, p_after, p_limit;
|
|
248
|
+
ELSE
|
|
249
|
+
IF p_after IS NOT NULL THEN
|
|
250
|
+
v_after := ' AND m.key > $5';
|
|
251
|
+
END IF;
|
|
252
|
+
RETURN QUERY EXECUTE format(
|
|
253
|
+
'SELECT CAST(%L AS text), m.key, CAST($4 AS text), CAST(''{{}}'' AS jsonb), '
|
|
254
|
+
'CAST(NULL AS bytea) FROM %s m '
|
|
255
|
+
'WHERE m.domain_id = %L AND m.branch = %L AND m.kind_id = %L '
|
|
256
|
+
'AND (m."values" ->> %L) = $4%s ORDER BY m.key LIMIT $6',
|
|
257
|
+
p_relation, v_records, p_domain, p_branch, p_from_kind, p_attribute, v_after)
|
|
258
|
+
USING p_domain, p_branch, p_relation, p_to_key, p_after, p_limit;
|
|
259
|
+
END IF;
|
|
260
|
+
END
|
|
261
|
+
$fn$
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
LB_QUERY_BY_ATTRIBUTE_OVERLAY = """
|
|
265
|
+
CREATE FUNCTION lb_query_by_attribute_overlay(
|
|
266
|
+
p_domain text, p_branch text, p_kind text, p_attribute text, p_type text, p_op text,
|
|
267
|
+
p_value text, p_limit integer, p_after_value text DEFAULT NULL,
|
|
268
|
+
p_after_key text DEFAULT NULL)
|
|
269
|
+
RETURNS TABLE ({record_columns})
|
|
270
|
+
{options}
|
|
271
|
+
AS $fn$
|
|
272
|
+
DECLARE
|
|
273
|
+
v_raw text := format('(m."values" ->> %L)', p_attribute);
|
|
274
|
+
v_expression text;
|
|
275
|
+
v_value text;
|
|
276
|
+
v_op text;
|
|
277
|
+
v_sql text;
|
|
278
|
+
v_tail text;
|
|
279
|
+
v_order text;
|
|
280
|
+
v_rows text;
|
|
281
|
+
BEGIN
|
|
282
|
+
IF p_limit < 1 THEN
|
|
283
|
+
RAISE EXCEPTION 'limit must be positive' USING ERRCODE = '22023';
|
|
284
|
+
END IF;
|
|
285
|
+
v_op := CASE p_op WHEN 'eq' THEN '=' WHEN 'lt' THEN '<' WHEN 'lte' THEN '<='
|
|
286
|
+
WHEN 'gt' THEN '>' WHEN 'gte' THEN '>=' END;
|
|
287
|
+
IF v_op IS NULL THEN
|
|
288
|
+
RAISE EXCEPTION 'unknown comparison %', p_op USING ERRCODE = '22023';
|
|
289
|
+
END IF;
|
|
290
|
+
IF p_type = 'text' THEN
|
|
291
|
+
v_expression := v_raw;
|
|
292
|
+
v_value := 'CAST(%s AS text)';
|
|
293
|
+
ELSIF p_type = 'md5' THEN
|
|
294
|
+
v_expression := 'md5' || v_raw;
|
|
295
|
+
v_value := 'md5(CAST(%s AS text))';
|
|
296
|
+
ELSIF p_type IN ('bigint', 'numeric', 'double precision', 'boolean') THEN
|
|
297
|
+
v_expression := format('(%s::%s)', v_raw, p_type);
|
|
298
|
+
v_value := 'CAST(CAST(%s AS text) AS ' || p_type || ')';
|
|
299
|
+
ELSE
|
|
300
|
+
RAISE EXCEPTION 'unknown index type %', p_type USING ERRCODE = '22023';
|
|
301
|
+
END IF;
|
|
302
|
+
-- Conditions, order and limit, applied inside each half of an overlay's rows (each
|
|
303
|
+
-- half reads at most one page, the base half through the base's partial index).
|
|
304
|
+
v_tail := format(' AND m.kind_id = %L AND %s %s %s', p_kind, v_expression, v_op,
|
|
305
|
+
format(v_value, '$1'));
|
|
306
|
+
IF p_op = 'eq' THEN
|
|
307
|
+
IF p_after_key IS NOT NULL THEN
|
|
308
|
+
v_tail := v_tail || ' AND m.key > $3';
|
|
309
|
+
END IF;
|
|
310
|
+
v_order := 'm.key';
|
|
311
|
+
ELSE
|
|
312
|
+
IF p_after_key IS NOT NULL THEN
|
|
313
|
+
v_tail := v_tail || format(
|
|
314
|
+
' AND (%s, m.key) > (%s, $3)', v_expression, format(v_value, '$2'));
|
|
315
|
+
END IF;
|
|
316
|
+
v_order := v_expression || ', m.key';
|
|
317
|
+
END IF;
|
|
318
|
+
v_tail := v_tail || ' ORDER BY ' || v_order || ' LIMIT $4';
|
|
319
|
+
v_rows := lb_overlay_rows(p_domain, p_branch, 'mat_records', v_tail);
|
|
320
|
+
IF v_rows IS NULL THEN
|
|
321
|
+
v_rows := format('(SELECT * FROM mat_records m WHERE m.domain_id = %L '
|
|
322
|
+
'AND m.branch = %L%s) AS m', p_domain, p_branch, v_tail);
|
|
323
|
+
ELSE
|
|
324
|
+
v_rows := v_rows || ' AS m';
|
|
325
|
+
END IF;
|
|
326
|
+
v_sql := 'SELECT m.kind_id, m.key, m."values", m.title, m.object_hash, m.lineage, '
|
|
327
|
+
'm.updated_revision, m.updated_at FROM ' || v_rows
|
|
328
|
+
|| ' ORDER BY ' || v_order || ' LIMIT $4';
|
|
329
|
+
RETURN QUERY EXECUTE v_sql USING p_value, p_after_value, p_after_key, p_limit;
|
|
330
|
+
END
|
|
331
|
+
$fn$
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
LB_NEIGHBORHOOD_OVERLAY = """
|
|
335
|
+
CREATE FUNCTION lb_neighborhood_overlay(
|
|
336
|
+
p_domain text, p_branch text, p_seeds jsonb, p_depth integer, p_relations jsonb,
|
|
337
|
+
p_fanout integer, p_max_steps integer, p_max_nodes integer)
|
|
338
|
+
RETURNS TABLE (item text, depth integer, {record_columns}, relation_id text, from_key text,
|
|
339
|
+
to_key text, link_values jsonb, link_hash bytea)
|
|
340
|
+
{options}
|
|
341
|
+
AS $fn$
|
|
342
|
+
DECLARE
|
|
343
|
+
{sources}
|
|
344
|
+
v_parts text[] := ARRAY[]::text[];
|
|
345
|
+
v_relation jsonb;
|
|
346
|
+
v_step text;
|
|
347
|
+
v_join text;
|
|
348
|
+
v_nulls text := 'CAST(NULL AS text), CAST(NULL AS text), CAST(NULL AS text), '
|
|
349
|
+
'CAST(NULL AS text), CAST(NULL AS text), CAST(NULL AS jsonb), '
|
|
350
|
+
'CAST(NULL AS bytea)';
|
|
351
|
+
BEGIN
|
|
352
|
+
IF p_depth < 1 OR p_fanout < 1 OR p_max_steps < 1 OR p_max_nodes < 1 THEN
|
|
353
|
+
RAISE EXCEPTION 'depth and caps must be positive' USING ERRCODE = '22023';
|
|
354
|
+
END IF;
|
|
355
|
+
IF jsonb_typeof(p_seeds) <> 'array' OR jsonb_typeof(p_relations) <> 'object' THEN
|
|
356
|
+
RAISE EXCEPTION 'seeds must be an array and relations an object'
|
|
357
|
+
USING ERRCODE = '22023';
|
|
358
|
+
END IF;
|
|
359
|
+
-- Lateral, so that the Relation and the frontier key reach both halves of an overlay's
|
|
360
|
+
-- Links as index conditions (OFFSET 0 keeps it a subquery).
|
|
361
|
+
v_join := format(
|
|
362
|
+
'CROSS JOIN LATERAL (SELECT * FROM %s l WHERE l.domain_id = %L AND l.branch = %L '
|
|
363
|
+
'AND l.relation_id = r.relation_id AND l.@near@ = w.key OFFSET 0) AS l',
|
|
364
|
+
v_links, p_domain, p_branch);
|
|
365
|
+
IF jsonb_array_length(coalesce(p_relations -> 'out', '[]')) > 0 THEN
|
|
366
|
+
v_parts := v_parts || (
|
|
367
|
+
'SELECT r.to_kind, l.to_key, l.relation_id, r.from_kind, l.from_key, r.to_kind, '
|
|
368
|
+
'l.to_key, l."values", l.object_hash FROM out_rels r '
|
|
369
|
+
|| replace(v_join, '@near@', 'from_key')
|
|
370
|
+
|| ' WHERE r.from_kind = w.kind_id LIMIT $3');
|
|
371
|
+
END IF;
|
|
372
|
+
IF jsonb_array_length(coalesce(p_relations -> 'in', '[]')) > 0 THEN
|
|
373
|
+
v_parts := v_parts || (
|
|
374
|
+
'SELECT r.from_kind, l.from_key, l.relation_id, r.from_kind, l.from_key, '
|
|
375
|
+
'r.to_kind, l.to_key, l."values", l.object_hash FROM in_rels r '
|
|
376
|
+
|| replace(v_join, '@near@', 'to_key')
|
|
377
|
+
|| ' WHERE r.to_kind = w.kind_id LIMIT $3');
|
|
378
|
+
END IF;
|
|
379
|
+
IF jsonb_array_length(coalesce(p_relations -> 'fk_out', '[]')) > 0 THEN
|
|
380
|
+
v_parts := v_parts || format(
|
|
381
|
+
'SELECT f.to_kind, m."values" ->> f.attribute, f.relation_id, f.from_kind, '
|
|
382
|
+
'm.key, f.to_kind, m."values" ->> f.attribute, CAST(''{{}}'' AS jsonb), '
|
|
383
|
+
'CAST(NULL AS bytea) FROM fk_rels f JOIN %s m ON m.domain_id = %L '
|
|
384
|
+
'AND m.branch = %L AND m.kind_id = w.kind_id AND m.key = w.key '
|
|
385
|
+
'WHERE f.from_kind = w.kind_id AND m."values" ->> f.attribute IS NOT NULL '
|
|
386
|
+
'LIMIT $3', v_records, p_domain, p_branch);
|
|
387
|
+
END IF;
|
|
388
|
+
FOR v_relation IN
|
|
389
|
+
SELECT e FROM jsonb_array_elements(coalesce(p_relations -> 'fk_in', '[]')) AS e
|
|
390
|
+
LOOP
|
|
391
|
+
IF (v_relation ->> 0) IS NULL OR (v_relation ->> 1) IS NULL
|
|
392
|
+
OR (v_relation ->> 2) IS NULL OR (v_relation ->> 3) IS NULL THEN
|
|
393
|
+
RAISE EXCEPTION 'a foreign-key Relation is [id, from Kind, to Kind, Attribute]'
|
|
394
|
+
USING ERRCODE = '22023';
|
|
395
|
+
END IF;
|
|
396
|
+
v_parts := v_parts || format(
|
|
397
|
+
'SELECT CAST(%1$L AS text), key, CAST(%2$L AS text), CAST(%1$L AS text), key, '
|
|
398
|
+
'CAST(%3$L AS text), w.key, CAST(''{{}}'' AS jsonb), CAST(NULL AS bytea) '
|
|
399
|
+
'FROM %7$s mat_records WHERE w.kind_id = %3$L AND domain_id = %4$L '
|
|
400
|
+
'AND branch = %5$L AND kind_id = %1$L AND ("values" ->> %6$L) = w.key LIMIT $3',
|
|
401
|
+
v_relation ->> 1, v_relation ->> 0, v_relation ->> 2, p_domain, p_branch,
|
|
402
|
+
v_relation ->> 3, v_records);
|
|
403
|
+
END LOOP;
|
|
404
|
+
IF cardinality(v_parts) = 0 THEN
|
|
405
|
+
v_step := 'SELECT w.depth + 1, w.kind_id, w.key, w.kind_id, w.key, ' || v_nulls
|
|
406
|
+
|| ' FROM walk w WHERE false';
|
|
407
|
+
ELSE
|
|
408
|
+
v_step := 'SELECT w.depth + 1, w.kind_id, w.key, n.* FROM walk w '
|
|
409
|
+
'CROSS JOIN LATERAL (SELECT * FROM ('
|
|
410
|
+
|| (SELECT string_agg('(' || p.part || ')', ' UNION ALL ' ORDER BY p.n)
|
|
411
|
+
FROM unnest(v_parts) WITH ORDINALITY AS p(part, n))
|
|
412
|
+
|| ') AS hop LIMIT $3) AS n WHERE w.depth < $2';
|
|
413
|
+
END IF;
|
|
414
|
+
RETURN QUERY EXECUTE format($sql$
|
|
415
|
+
WITH RECURSIVE
|
|
416
|
+
out_rels(relation_id, from_kind, to_kind) AS (
|
|
417
|
+
SELECT e ->> 0, e ->> 1, e ->> 2 FROM jsonb_array_elements($4 -> 'out') AS e),
|
|
418
|
+
in_rels(relation_id, from_kind, to_kind) AS (
|
|
419
|
+
SELECT e ->> 0, e ->> 1, e ->> 2 FROM jsonb_array_elements($4 -> 'in') AS e),
|
|
420
|
+
fk_rels(relation_id, from_kind, to_kind, attribute) AS (
|
|
421
|
+
SELECT e ->> 0, e ->> 1, e ->> 2, e ->> 3
|
|
422
|
+
FROM jsonb_array_elements($4 -> 'fk_out') AS e),
|
|
423
|
+
walk(depth, via_kind, via_key, kind_id, key, relation_id, from_kind, from_key, to_kind,
|
|
424
|
+
to_key, link_values, link_hash) AS (
|
|
425
|
+
SELECT 0, CAST(NULL AS text), CAST(NULL AS text), s ->> 0, s ->> 1, %1$s
|
|
426
|
+
FROM jsonb_array_elements($1) AS s
|
|
427
|
+
UNION ALL
|
|
428
|
+
%2$s
|
|
429
|
+
) CYCLE kind_id, key SET is_cycle USING path,
|
|
430
|
+
walked AS MATERIALIZED (
|
|
431
|
+
SELECT depth, via_kind, via_key, kind_id, key, relation_id, from_kind, from_key,
|
|
432
|
+
to_kind, to_key, link_values, link_hash
|
|
433
|
+
FROM walk LIMIT $5),
|
|
434
|
+
nodes AS MATERIALIZED (
|
|
435
|
+
SELECT kind_id, key, min(depth) AS depth FROM walked GROUP BY kind_id, key
|
|
436
|
+
ORDER BY min(depth), kind_id, key LIMIT $6 + 1),
|
|
437
|
+
links AS (
|
|
438
|
+
SELECT DISTINCT ON (relation_id, from_key, to_key) relation_id, from_key, to_key,
|
|
439
|
+
link_values, link_hash
|
|
440
|
+
FROM walked w
|
|
441
|
+
WHERE relation_id IS NOT NULL
|
|
442
|
+
AND EXISTS (SELECT 1 FROM nodes n WHERE n.kind_id = w.from_kind AND n.key = w.from_key)
|
|
443
|
+
AND EXISTS (SELECT 1 FROM nodes n WHERE n.kind_id = w.to_kind AND n.key = w.to_key))
|
|
444
|
+
SELECT 'record', n.depth, m.kind_id, m.key, m."values", m.title, m.object_hash, m.lineage,
|
|
445
|
+
m.updated_revision, m.updated_at, CAST(NULL AS text), CAST(NULL AS text),
|
|
446
|
+
CAST(NULL AS text), CAST(NULL AS jsonb), CAST(NULL AS bytea)
|
|
447
|
+
FROM nodes n
|
|
448
|
+
CROSS JOIN LATERAL (
|
|
449
|
+
SELECT * FROM %5$s m WHERE m.domain_id = %3$L AND m.branch = %4$L
|
|
450
|
+
AND m.kind_id = n.kind_id AND m.key = n.key LIMIT 1) AS m
|
|
451
|
+
UNION ALL
|
|
452
|
+
SELECT 'link', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
|
|
453
|
+
relation_id, from_key, to_key, link_values, link_hash
|
|
454
|
+
FROM links
|
|
455
|
+
UNION ALL
|
|
456
|
+
SELECT 'steps', CAST((SELECT count(*) FROM walked) AS integer), NULL, NULL, NULL, NULL,
|
|
457
|
+
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL
|
|
458
|
+
UNION ALL
|
|
459
|
+
SELECT 'fanout', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
|
|
460
|
+
NULL, NULL
|
|
461
|
+
WHERE EXISTS (
|
|
462
|
+
SELECT 1 FROM walked WHERE depth > 0 GROUP BY depth, via_kind, via_key
|
|
463
|
+
HAVING count(DISTINCT (relation_id, from_key, to_key)) >= $3)
|
|
464
|
+
$sql$, v_nulls, v_step, p_domain, p_branch, v_records)
|
|
465
|
+
USING p_seeds, p_depth, p_fanout, p_relations, p_max_steps, p_max_nodes;
|
|
466
|
+
END
|
|
467
|
+
$fn$
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
COUNTS = {
|
|
472
|
+
LB_SEARCH_OVERLAY: ("p_candidates", "p_limit"),
|
|
473
|
+
LB_LINKS_IN_OVERLAY: ("p_limit",),
|
|
474
|
+
LB_QUERY_BY_ATTRIBUTE_OVERLAY: ("p_limit",),
|
|
475
|
+
LB_NEIGHBORHOOD_OVERLAY: ("p_depth", "p_fanout", "p_max_steps", "p_max_nodes"),
|
|
476
|
+
}
|
|
477
|
+
"""Each overlay template and its count arguments, as in revision 0008."""
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _columns() -> None:
|
|
481
|
+
op.add_column("refs", sa.Column("base_branch", sa.Text(), nullable=True))
|
|
482
|
+
op.add_column("refs", sa.Column("base_revision", sa.LargeBinary(), nullable=True))
|
|
483
|
+
op.add_column("refs", sa.Column("base_generation", sa.BigInteger(), nullable=True))
|
|
484
|
+
op.create_foreign_key(
|
|
485
|
+
op.f("fk_refs_domain_id_base_branch_refs"),
|
|
486
|
+
"refs",
|
|
487
|
+
"refs",
|
|
488
|
+
["domain_id", "base_branch"],
|
|
489
|
+
["domain_id", "name"],
|
|
490
|
+
deferrable=True,
|
|
491
|
+
initially="DEFERRED",
|
|
492
|
+
)
|
|
493
|
+
op.create_foreign_key(
|
|
494
|
+
op.f("fk_refs_domain_id_base_revision_revisions"),
|
|
495
|
+
"refs",
|
|
496
|
+
"revisions",
|
|
497
|
+
["domain_id", "base_revision"],
|
|
498
|
+
["domain_id", "id"],
|
|
499
|
+
deferrable=True,
|
|
500
|
+
initially="DEFERRED",
|
|
501
|
+
)
|
|
502
|
+
op.create_check_constraint(
|
|
503
|
+
op.f("ck_refs_overlay_base"),
|
|
504
|
+
"refs",
|
|
505
|
+
"(materialization = 'overlay') = (base_branch IS NOT NULL "
|
|
506
|
+
"AND base_revision IS NOT NULL AND base_generation IS NOT NULL)",
|
|
507
|
+
)
|
|
508
|
+
op.create_index(
|
|
509
|
+
"ix_refs_domain_id_base_branch",
|
|
510
|
+
"refs",
|
|
511
|
+
["domain_id", "base_branch"],
|
|
512
|
+
unique=False,
|
|
513
|
+
postgresql_where=sa.text("base_branch IS NOT NULL"),
|
|
514
|
+
)
|
|
515
|
+
for table in ("mat_records", "mat_links"):
|
|
516
|
+
op.add_column(
|
|
517
|
+
table,
|
|
518
|
+
sa.Column(
|
|
519
|
+
"revision_generation", sa.BigInteger(), server_default=sa.text("0"), nullable=False
|
|
520
|
+
),
|
|
521
|
+
)
|
|
522
|
+
op.add_column(
|
|
523
|
+
table,
|
|
524
|
+
sa.Column("deleted", sa.Boolean(), server_default=sa.text("false"), nullable=False),
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def upgrade() -> None:
|
|
529
|
+
role = _reads._application_role()
|
|
530
|
+
if role is not None:
|
|
531
|
+
exists = (
|
|
532
|
+
op.get_bind()
|
|
533
|
+
.execute(
|
|
534
|
+
sa.text("SELECT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :r)"), {"r": role}
|
|
535
|
+
)
|
|
536
|
+
.scalar()
|
|
537
|
+
)
|
|
538
|
+
if not exists:
|
|
539
|
+
raise RuntimeError(
|
|
540
|
+
f"database.application_role names {role!r}, which does not exist; create the "
|
|
541
|
+
"role first or leave the setting empty"
|
|
542
|
+
)
|
|
543
|
+
_checks._refuse_writable_schemas(role)
|
|
544
|
+
_columns()
|
|
545
|
+
search_path = _reads._search_path()
|
|
546
|
+
op.execute(LB_OVERLAY_ROWS.format(search_path=search_path))
|
|
547
|
+
op.execute(f"REVOKE ALL ON FUNCTION {HELPER} FROM PUBLIC")
|
|
548
|
+
options = _reads._OPTIONS.format(search_path=search_path).strip()
|
|
549
|
+
for template, counts in COUNTS.items():
|
|
550
|
+
guards = _checks._guards(search_path, counts)
|
|
551
|
+
if "{sources}" in template:
|
|
552
|
+
guards += _RESOLVE
|
|
553
|
+
statement = _checks._create_or_replace(template, guards)
|
|
554
|
+
op.execute(
|
|
555
|
+
statement.format(
|
|
556
|
+
options=options, record_columns=_reads._RECORD_COLUMNS, sources=_SOURCES.strip()
|
|
557
|
+
)
|
|
558
|
+
)
|
|
559
|
+
for signature in SIGNATURES:
|
|
560
|
+
op.execute(f"REVOKE ALL ON FUNCTION {signature} FROM PUBLIC")
|
|
561
|
+
if role is not None:
|
|
562
|
+
quoted = op.get_bind().execute(sa.text("SELECT quote_ident(:r)"), {"r": role}).scalar()
|
|
563
|
+
for signature in SIGNATURES:
|
|
564
|
+
op.execute(f"GRANT EXECUTE ON FUNCTION {signature} TO {quoted}")
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def downgrade() -> None:
|
|
568
|
+
for signature in reversed(SIGNATURES):
|
|
569
|
+
op.execute(f"DROP FUNCTION IF EXISTS {signature}")
|
|
570
|
+
op.execute(f"DROP FUNCTION IF EXISTS {HELPER}")
|
|
571
|
+
for table in ("mat_links", "mat_records"):
|
|
572
|
+
op.drop_column(table, "deleted")
|
|
573
|
+
op.drop_column(table, "revision_generation")
|
|
574
|
+
op.drop_index("ix_refs_domain_id_base_branch", table_name="refs")
|
|
575
|
+
op.drop_constraint(op.f("ck_refs_overlay_base"), "refs", type_="check")
|
|
576
|
+
op.drop_constraint(
|
|
577
|
+
op.f("fk_refs_domain_id_base_revision_revisions"), "refs", type_="foreignkey"
|
|
578
|
+
)
|
|
579
|
+
op.drop_constraint(op.f("fk_refs_domain_id_base_branch_refs"), "refs", type_="foreignkey")
|
|
580
|
+
op.drop_column("refs", "base_generation")
|
|
581
|
+
op.drop_column("refs", "base_revision")
|
|
582
|
+
op.drop_column("refs", "base_branch")
|