@fayz-ai/db 0.13.0 → 0.14.0
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.
- package/canonical-schema.json +106 -23
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/schema/spine.d.ts +42 -37
- package/dist/schema/spine.d.ts.map +1 -1
- package/migrations/000_baseline.sql +29667 -0
- package/migrations/001_the_tenant_has_apps.sql +314 -0
- package/migrations/002_the_spine_drops_the_vendor_and_the_dead.sql +129 -0
- package/migrations/003_the_role_rpcs_finish_and_the_mirrors_go.sql +135 -0
- package/migrations/004_the_projections_get_a_successor.sql +312 -0
- package/migrations/005_the_archetypes_anchor_to_the_unit.sql +90 -0
- package/migrations/006_the_helpers_read_the_source.sql +153 -0
- package/migrations/007_the_compat_machinery_leaves_with_the_tables.sql +77 -0
- package/migrations/008_the_switchers_see_across_accounts.sql +150 -0
- package/migrations/009_the_compat_aliases_nobody_adopted.sql +57 -0
- package/migrations/010_the_new_tenant_is_born_with_its_trade.sql +201 -0
- package/migrations/011_leadcontrol_joins_the_family.sql +116 -0
- package/migrations/012_chefcontrol_wears_its_own_mark.sql +28 -0
- package/migrations/013_the_workspace_switch_reaches_the_other_half.sql +156 -0
- package/migrations/014_the_chooser_can_read_what_it_offers.sql +91 -0
- package/migrations/015_the_unit_bindings_belong_to_one_workspace.sql +86 -0
- package/migrations/016_the_rule_stops_asking_every_row.sql +543 -0
- package/migrations/017_the_pin_is_checked_against_the_right_workspace.sql +139 -0
- package/migrations/018_the_bootstrap_says_what_you_reach.sql +114 -0
- package/migrations/019_distributing_one_row_stops_costing_the_table.sql +245 -0
- package/migrations/020_the_agenda_asks_where_someone_works.sql +81 -0
- package/migrations/022_the_line_says_which_document_it_belongs_to.sql +109 -0
- package/package.json +1 -1
- package/migrations/0000_legacy_quarantine.sql +0 -86
- package/migrations/0000b_straggler_sweep.sql +0 -37
- package/migrations/000_core_v1_convert.sql +0 -343
- package/migrations/001_core.sql +0 -372
- package/migrations/002_rls_user_tenant_ids.sql +0 -64
- package/migrations/003_invitations.sql +0 -17
- package/migrations/004_archetypes.sql +0 -343
- package/migrations/005_locations_archetype.sql +0 -18
- package/migrations/006_archetype_rls.sql +0 -74
- package/migrations/007_archetype_grants.sql +0 -4
- package/migrations/008_grants.sql +0 -5
- package/migrations/009_anon_hardening.sql +0 -19
- package/migrations/010_migration_ledger.sql +0 -46
- package/migrations/011_anon_write_revoke.sql +0 -12
- package/migrations/012_rename_migration_ledger.sql +0 -61
- package/migrations/013_sequences_rls.sql +0 -22
- package/migrations/014_plan_entitlements.sql +0 -28
- package/migrations/015_agent_guard.sql +0 -108
- package/migrations/016_agent_guard_actor.sql +0 -102
- package/migrations/017_core_addresses.sql +0 -115
- package/migrations/018_global_search.sql +0 -482
- package/migrations/019_tenant_members_person_id.sql +0 -20
- package/migrations/020_fayz_projects.sql +0 -63
- package/migrations/021_tenant_roles.sql +0 -74
- package/migrations/022_invite_acceptance.sql +0 -91
- package/migrations/023_team_visibility.sql +0 -75
- package/migrations/024_analytics_engine.sql +0 -317
- package/migrations/025_created_by.sql +0 -75
- package/migrations/026_audit_trail.sql +0 -73
- package/migrations/027_domain_events.sql +0 -266
- package/migrations/028_tenant_scoped_token.sql +0 -127
- package/migrations/029_connections.sql +0 -186
- package/migrations/030_effect_idempotency.sql +0 -159
- package/migrations/031_sync_run_message.sql +0 -39
- package/migrations/032_connection_secrets.sql +0 -227
- package/migrations/033_sync_schedule.sql +0 -651
- package/migrations/034_custom_fields.sql +0 -55
- package/migrations/035_field_registry.sql +0 -148
- package/migrations/036_analytics_run_batch.sql +0 -84
- package/migrations/037_sync_tick_one_at_a_time.sql +0 -256
- package/migrations/038_onboarding_responses.sql +0 -103
- package/migrations/039_unit_tree.sql +0 -270
- package/migrations/040_resource_grants.sql +0 -474
- package/migrations/041_scoped_columns.sql +0 -192
- package/migrations/042_unit_scope_policies.sql +0 -145
- package/migrations/043_view_invoker.sql +0 -90
- package/migrations/044_unit_member_facts.sql +0 -47
- package/migrations/045_unit_entry.sql +0 -236
- package/migrations/046_membership_visible_to_members.sql +0 -85
- package/migrations/047_tasks.sql +0 -266
- package/migrations/048_every_login_is_a_person.sql +0 -190
- package/migrations/049_bookable_people.sql +0 -126
- package/migrations/050_authz_core.sql +0 -230
- package/migrations/051_authz_helpers.sql +0 -330
- package/migrations/052_authz_fanout.sql +0 -253
- package/migrations/053_authz_catalog_templates.sql +0 -402
- package/migrations/060_tenancy_seam.sql +0 -132
- package/migrations/061_create_tenant.sql +0 -125
- package/migrations/062_invitations.sql +0 -329
- package/migrations/063_legacy_tenancy_retired.sql +0 -568
- package/migrations/064_agent_guard_authz.sql +0 -118
- package/migrations/064b_supersede_unit_scope.sql +0 -205
- package/migrations/065_scaffold_sweep.sql +0 -139
- package/migrations/066_shadow_report.sql +0 -122
- package/migrations/070_permission_catalog.sql +0 -205
- package/migrations/071_v1_permission_map.sql +0 -266
- package/migrations/072_role_rpcs.sql +0 -126
- package/migrations/073_legacy_rbac_retire.sql +0 -471
- package/migrations/075_company_legal_entities.sql +0 -169
- package/migrations/076_tenant_settings.sql +0 -251
- package/migrations/080_people_roles.sql +0 -269
- package/migrations/081_people_identity.sql +0 -271
- package/migrations/082_people_membership_link.sql +0 -142
- package/migrations/083_people_data_migration.sql +0 -75
- package/migrations/084_people_registrations_tokens.sql +0 -282
- package/migrations/085_catalog_products.sql +0 -333
- package/migrations/086_catalog_pricing.sql +0 -194
- package/migrations/087_catalog_packages.sql +0 -134
- package/migrations/090_orders_venda.sql +0 -166
- package/migrations/091_orders_scaffold.sql +0 -92
- package/migrations/095_config_field_rules.sql +0 -261
- package/migrations/096_audit_append_only.sql +0 -256
- package/migrations/097_sequences_authorized.sql +0 -111
- package/migrations/098_holidays.sql +0 -80
- package/migrations/099_access_grants.sql +0 -293
- package/migrations/100_migration_schema.sql +0 -217
- package/migrations/101_migration_allowlist.sql +0 -202
- package/migrations/102_migration_rpcs.sql +0 -933
- package/migrations/103_migration_reconcile_ponr.sql +0 -132
- package/migrations/105_policies_to_authenticated.sql +0 -71
- package/migrations/106_function_search_path.sql +0 -55
- package/migrations/107_core_table_prefix.sql +0 -361
- package/migrations/108_authz_write_guards.sql +0 -149
- package/migrations/109_catalog_migration_promoted_columns.sql +0 -126
- package/migrations/110_people_kind_vocabulary.sql +0 -120
- package/migrations/111_migration_excluded_is_a_decision.sql +0 -259
- package/migrations/111_replacing_a_view_keeps_its_dependents.sql +0 -132
- package/migrations/112_money_precision_standard.sql +0 -132
- package/migrations/113_addresses_units_companies.sql +0 -114
- package/migrations/114_migrations_ledger_immutable.sql +0 -65
- package/migrations/115_registration_honours_field_rules.sql +0 -172
- package/migrations/116_field_rules_check_needs_a_tenant.sql +0 -72
- package/migrations/117_authz_reconciliation_schedule.sql +0 -82
- package/migrations/118_migration_catalog_writers.sql +0 -450
- package/migrations/119_migration_config_writers.sql +0 -193
- package/migrations/120_migration_facet_writer.sql +0 -139
- package/migrations/121_migration_financial_writers.sql +0 -294
- package/migrations/122_migration_inventory_writers.sql +0 -245
- package/migrations/123_migration_order_item_writer.sql +0 -144
- package/migrations/124_migration_ledger_writers.sql +0 -170
- package/migrations/125_migration_authz_people_writers.sql +0 -205
- package/migrations/126_owner_scoping_is_configuration.sql +0 -247
- package/migrations/127_scaffold_audit_hook.sql +0 -98
- package/migrations/128_shadow_tables_enforced.sql +0 -93
- package/migrations/129_order_kind_terminal_states.sql +0 -57
- package/migrations/130_every_login_is_a_person.sql +0 -196
- package/migrations/131_migration_invoice_split.sql +0 -127
- package/migrations/132_audit_prune_scheduled.sql +0 -28
- package/migrations/133_unit_is_a_session_property.sql +0 -155
- package/migrations/134_field_rules_on_read.sql +0 -341
- package/migrations/135_distribution_polarity.sql +0 -158
- package/migrations/136_unit_id_means_locality.sql +0 -358
- package/migrations/137_vendor_named_spine_objects.sql +0 -126
- package/migrations/138_grants_inherit_down_the_tree.sql +0 -178
- package/migrations/139_distributable_is_a_scaffold_option.sql +0 -109
- package/migrations/140_masking_is_a_scaffold_option.sql +0 -85
- package/migrations/141_renamed_objects_keep_a_compat_name.sql +0 -192
- package/migrations/142_a_person_has_addresses.sql +0 -212
- package/migrations/143_a_professional_has_a_rate.sql +0 -75
- package/migrations/144_a_product_can_be_sold_in_sizes.sql +0 -57
- package/migrations/145_the_app_says_which_tenant_it_is_in.sql +0 -123
- package/migrations/146_products_stock_has_one_writer.sql +0 -67
- package/migrations/147_every_archetype_has_a_kind_registry.sql +0 -388
- package/migrations/148_the_legacy_archetype_stops_being_writable.sql +0 -154
- package/migrations/149_the_event_log_gets_a_consumer.sql +0 -331
- package/migrations/150_the_app_is_configured_in_the_database.sql +0 -285
- package/migrations/151_stock_balance_is_an_archetype.sql +0 -144
- package/migrations/152_a_file_is_an_archetype.sql +0 -222
- package/migrations/153_a_plugin_calls_a_service_through_one_door.sql +0 -199
- package/migrations/154_the_plan_is_a_ceiling.sql +0 -258
- package/migrations/155_a_tenant_can_leave.sql +0 -271
- package/migrations/156_the_service_door_has_a_server_side.sql +0 -335
- package/migrations/157_nobody_creates_objects_in_public.sql +0 -74
- package/migrations/158_the_registry_owns_the_status_vocabulary.sql +0 -153
- package/migrations/159_the_plan_catalogue_gets_its_first_rows.sql +0 -132
- package/migrations/160_the_vertical_decides_what_starts_on.sql +0 -246
- package/migrations/161_a_tenant_leaves_with_its_data.sql +0 -363
- package/migrations/162_every_name_has_one_creator_for_real.sql +0 -126
- package/migrations/163_a_species_nobody_declared_goes_to_quarantine.sql +0 -184
- package/migrations/164_the_sequence_table_joins_the_register.sql +0 -26
- package/migrations/165_a_view_is_not_a_way_in.sql +0 -90
- package/migrations/166_the_platform_billing_leftovers_go.sql +0 -72
- package/migrations/167_a_view_gets_back_what_its_table_grants.sql +0 -112
- package/migrations/168_the_grant_census_stops_crying_wolf.sql +0 -33
- package/migrations/169_the_census_asks_postgres_instead_of_guessing.sql +0 -45
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
-- ============================================================================
|
|
2
|
-
-- 103_migration_reconcile_ponr.sql — the reconciliation report (PRD 10 R6)
|
|
3
|
-
-- and the point-of-no-return stamp (ADR 0008).
|
|
4
|
-
--
|
|
5
|
-
-- reconcile(batch): counts per source table, a checksum over the tenant's
|
|
6
|
-
-- ledger rows (stable across a replay), FK sanity (migrated target ids that no
|
|
7
|
-
-- longer exist), financial sums Σ(ledger) vs Σ(destination) per (table, column)
|
|
8
|
-
-- to the cent unless an approved exception covers the key, open quarantines and
|
|
9
|
-
-- pending rows. green ⇔ nothing open, nothing missing, every sum matches (or is
|
|
10
|
-
-- excepted), nothing pending. Stored in migration.reconciliation_reports.
|
|
11
|
-
--
|
|
12
|
-
-- PONR: the first write to a fenced tenant's business data by anyone who is
|
|
13
|
-
-- NOT the migration stamps tenant_fence.first_v2_write_at; from then on the
|
|
14
|
-
-- kill switch refuses. Canonical stamp: AFTER row triggers on public.orders and
|
|
15
|
-
-- public.people (cheap: one GUC read, one PK lookup) — the two tables every
|
|
16
|
-
-- V2 session writes first. migration.mark_v2_write(tenant) is the explicit
|
|
17
|
-
-- form for a service that knows it wrote as V2.
|
|
18
|
-
-- ============================================================================
|
|
19
|
-
|
|
20
|
-
CREATE OR REPLACE FUNCTION migration.reconcile(p_batch uuid) RETURNS jsonb
|
|
21
|
-
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
22
|
-
DECLARE
|
|
23
|
-
b migration.batches%ROWTYPE;
|
|
24
|
-
v_batch_counts jsonb; v_tenant_counts jsonb; v_checksum text;
|
|
25
|
-
v_fk jsonb := '[]'::jsonb; v_fk_missing integer := 0; v_fk_checked integer := 0;
|
|
26
|
-
v_values jsonb := '[]'::jsonb; v_value_mismatch integer := 0;
|
|
27
|
-
v_quar jsonb; v_quar_open integer; v_exceptions jsonb; v_pending integer;
|
|
28
|
-
v_green boolean; v_report jsonb; v_id uuid;
|
|
29
|
-
r record; v_missing integer; v_sample jsonb; v_dest numeric; v_led numeric; v_exc boolean; v_match boolean;
|
|
30
|
-
BEGIN
|
|
31
|
-
SELECT * INTO b FROM migration.batches WHERE id = p_batch;
|
|
32
|
-
IF NOT FOUND THEN RAISE EXCEPTION 'reconcile: unknown batch %', p_batch USING ERRCODE = 'P0002'; END IF;
|
|
33
|
-
|
|
34
|
-
SELECT coalesce(jsonb_object_agg(source_table, c), '{}'::jsonb) INTO v_batch_counts
|
|
35
|
-
FROM (SELECT source_table, jsonb_object_agg(status, n) AS c
|
|
36
|
-
FROM (SELECT source_table, status, count(*) AS n FROM migration.ledger WHERE batch_id = p_batch GROUP BY 1, 2) s
|
|
37
|
-
GROUP BY source_table) t;
|
|
38
|
-
SELECT coalesce(jsonb_object_agg(source_table, c), '{}'::jsonb) INTO v_tenant_counts
|
|
39
|
-
FROM (SELECT source_table, jsonb_object_agg(status, n) AS c
|
|
40
|
-
FROM (SELECT source_table, status, count(*) AS n FROM migration.ledger
|
|
41
|
-
WHERE tenant_id = b.tenant_id AND source_project = b.source_project GROUP BY 1, 2) s
|
|
42
|
-
GROUP BY source_table) t;
|
|
43
|
-
SELECT md5(coalesce(string_agg(
|
|
44
|
-
concat_ws('|', source_table, source_numeric_id, source_uuid, target_table, target_id, status, checksum, cursor), E'\n'
|
|
45
|
-
ORDER BY source_table, source_numeric_id, source_uuid, target_table), ''))
|
|
46
|
-
INTO v_checksum
|
|
47
|
-
FROM migration.ledger WHERE tenant_id = b.tenant_id AND source_project = b.source_project;
|
|
48
|
-
|
|
49
|
-
FOR r IN SELECT target_table, count(*) AS n FROM migration.ledger
|
|
50
|
-
WHERE tenant_id = b.tenant_id AND source_project = b.source_project AND status = 'migrated'
|
|
51
|
-
AND target_table IS NOT NULL AND target_id IS NOT NULL
|
|
52
|
-
GROUP BY 1 ORDER BY 1 LOOP
|
|
53
|
-
v_fk_checked := v_fk_checked + r.n;
|
|
54
|
-
IF to_regclass(r.target_table) IS NULL THEN
|
|
55
|
-
v_fk := v_fk || jsonb_build_object('target_table', r.target_table, 'checked', r.n, 'missing', r.n, 'note', 'destination table absent');
|
|
56
|
-
v_fk_missing := v_fk_missing + r.n;
|
|
57
|
-
CONTINUE;
|
|
58
|
-
END IF;
|
|
59
|
-
EXECUTE format('SELECT count(*) FROM (SELECT DISTINCT l.target_id FROM migration.ledger l WHERE l.tenant_id = $1 AND l.source_project = $2 AND l.status = ''migrated'' AND l.target_table = $3) l LEFT JOIN %s d ON d.id = l.target_id WHERE d.id IS NULL', r.target_table)
|
|
60
|
-
INTO v_missing USING b.tenant_id, b.source_project, r.target_table;
|
|
61
|
-
EXECUTE format('SELECT coalesce(jsonb_agg(x), ''[]''::jsonb) FROM (SELECT l.target_id AS x FROM (SELECT DISTINCT l.target_id FROM migration.ledger l WHERE l.tenant_id = $1 AND l.source_project = $2 AND l.status = ''migrated'' AND l.target_table = $3) l LEFT JOIN %s d ON d.id = l.target_id WHERE d.id IS NULL LIMIT 20) s', r.target_table)
|
|
62
|
-
INTO v_sample USING b.tenant_id, b.source_project, r.target_table;
|
|
63
|
-
v_fk := v_fk || jsonb_build_object('target_table', r.target_table, 'checked', r.n, 'missing', v_missing, 'sample', v_sample);
|
|
64
|
-
v_fk_missing := v_fk_missing + v_missing;
|
|
65
|
-
END LOOP;
|
|
66
|
-
|
|
67
|
-
FOR r IN SELECT l.target_table, k AS col FROM migration.ledger l, LATERAL jsonb_object_keys(l.financial) k
|
|
68
|
-
WHERE l.tenant_id = b.tenant_id AND l.source_project = b.source_project AND l.status = 'migrated' AND l.financial IS NOT NULL
|
|
69
|
-
GROUP BY 1, 2 ORDER BY 1, 2 LOOP
|
|
70
|
-
SELECT coalesce(sum((financial ->> r.col)::numeric), 0) INTO v_led FROM migration.ledger
|
|
71
|
-
WHERE tenant_id = b.tenant_id AND source_project = b.source_project AND status = 'migrated' AND target_table = r.target_table AND financial ? r.col;
|
|
72
|
-
IF to_regclass(r.target_table) IS NULL OR NOT migration._has_column(r.target_table, r.col) THEN
|
|
73
|
-
v_dest := NULL;
|
|
74
|
-
ELSE
|
|
75
|
-
EXECUTE format('SELECT coalesce(sum(d.%I), 0) FROM %s d WHERE d.id IN (SELECT DISTINCT l.target_id FROM migration.ledger l WHERE l.tenant_id = $1 AND l.source_project = $2 AND l.status = ''migrated'' AND l.target_table = $3 AND l.financial ? $4)', r.col, r.target_table)
|
|
76
|
-
INTO v_dest USING b.tenant_id, b.source_project, r.target_table, r.col;
|
|
77
|
-
END IF;
|
|
78
|
-
v_exc := EXISTS (SELECT 1 FROM migration.exceptions e WHERE e.tenant_id = b.tenant_id AND e.key = r.target_table || '.' || r.col AND (e.batch_id IS NULL OR e.batch_id = p_batch));
|
|
79
|
-
v_match := v_dest IS NOT NULL AND round(v_led, 2) = round(v_dest, 2);
|
|
80
|
-
IF NOT v_match AND NOT v_exc THEN v_value_mismatch := v_value_mismatch + 1; END IF;
|
|
81
|
-
v_values := v_values || jsonb_build_object('target_table', r.target_table, 'column', r.col, 'ledger_sum', round(v_led, 2),
|
|
82
|
-
'destination_sum', round(v_dest, 2), 'match', v_match, 'exception', v_exc);
|
|
83
|
-
END LOOP;
|
|
84
|
-
|
|
85
|
-
SELECT coalesce(sum(n), 0)::integer, coalesce(jsonb_object_agg(kind, n), '{}'::jsonb) INTO v_quar_open, v_quar
|
|
86
|
-
FROM (SELECT q.kind, count(*) AS n FROM migration.quarantine q JOIN migration.ledger l ON l.id = q.ledger_id
|
|
87
|
-
WHERE l.tenant_id = b.tenant_id AND l.source_project = b.source_project AND q.resolved_at IS NULL GROUP BY q.kind) s;
|
|
88
|
-
SELECT count(*)::integer INTO v_pending FROM migration.ledger
|
|
89
|
-
WHERE tenant_id = b.tenant_id AND source_project = b.source_project AND status = 'pending';
|
|
90
|
-
SELECT coalesce(jsonb_agg(jsonb_build_object('id', e.id, 'source_table', e.source_table, 'key', e.key, 'reason', e.reason, 'approved_by', e.approved_by, 'approved_at', e.approved_at) ORDER BY e.id), '[]'::jsonb)
|
|
91
|
-
INTO v_exceptions FROM migration.exceptions e WHERE e.tenant_id = b.tenant_id AND (e.batch_id IS NULL OR e.batch_id = p_batch);
|
|
92
|
-
|
|
93
|
-
v_green := v_quar_open = 0 AND v_fk_missing = 0 AND v_value_mismatch = 0 AND v_pending = 0;
|
|
94
|
-
v_report := jsonb_build_object(
|
|
95
|
-
'batch', jsonb_build_object('id', b.id, 'status', b.status, 'source_project', b.source_project, 'source_tenant_id', b.source_tenant_id,
|
|
96
|
-
'cursor_from', b.cursor_from, 'cursor_to', b.cursor_to, 'outcomes', b.counts, 'rows', v_batch_counts),
|
|
97
|
-
'tenant', jsonb_build_object('tenant_id', b.tenant_id, 'source_project', b.source_project, 'counts', v_tenant_counts, 'checksum', v_checksum,
|
|
98
|
-
'fk', jsonb_build_object('checked', v_fk_checked, 'missing', v_fk_missing, 'by_table', v_fk),
|
|
99
|
-
'values', v_values, 'quarantine', jsonb_build_object('open', v_quar_open, 'by_kind', v_quar),
|
|
100
|
-
'pending', v_pending, 'exceptions', v_exceptions),
|
|
101
|
-
'green', v_green, 'generated_at', now());
|
|
102
|
-
INSERT INTO migration.reconciliation_reports (tenant_id, batch_id, green, report) VALUES (b.tenant_id, p_batch, v_green, v_report) RETURNING id INTO v_id;
|
|
103
|
-
RETURN v_report || jsonb_build_object('report_id', v_id);
|
|
104
|
-
END $$;
|
|
105
|
-
REVOKE ALL ON FUNCTION migration.reconcile(uuid) FROM public, anon, authenticated;
|
|
106
|
-
GRANT EXECUTE ON FUNCTION migration.reconcile(uuid) TO service_role;
|
|
107
|
-
|
|
108
|
-
-- ─────────────────────────────── PONR stamp ────────────────────────────
|
|
109
|
-
CREATE OR REPLACE FUNCTION migration.trg_ponr_stamp() RETURNS trigger
|
|
110
|
-
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
111
|
-
DECLARE v_tenant uuid;
|
|
112
|
-
BEGIN
|
|
113
|
-
IF coalesce(current_setting('migration.writer', true), '') = 'on' THEN RETURN NULL; END IF;
|
|
114
|
-
v_tenant := CASE WHEN TG_OP = 'DELETE' THEN OLD.tenant_id ELSE NEW.tenant_id END;
|
|
115
|
-
IF v_tenant IS NOT NULL THEN
|
|
116
|
-
UPDATE migration.tenant_fence SET first_v2_write_at = now()
|
|
117
|
-
WHERE tenant_id = v_tenant AND first_v2_write_at IS NULL AND state IN ('fenced', 'v2_writer');
|
|
118
|
-
END IF;
|
|
119
|
-
RETURN NULL;
|
|
120
|
-
END $$;
|
|
121
|
-
REVOKE ALL ON FUNCTION migration.trg_ponr_stamp() FROM public, anon, authenticated;
|
|
122
|
-
|
|
123
|
-
DO $$
|
|
124
|
-
DECLARE t text;
|
|
125
|
-
BEGIN
|
|
126
|
-
FOREACH t IN ARRAY ARRAY['orders', 'people'] LOOP
|
|
127
|
-
IF to_regclass('public.' || t) IS NOT NULL THEN
|
|
128
|
-
EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', t || '_migration_ponr', t);
|
|
129
|
-
EXECUTE format('CREATE TRIGGER %I AFTER INSERT OR UPDATE OR DELETE ON public.%I FOR EACH ROW EXECUTE FUNCTION migration.trg_ponr_stamp()', t || '_migration_ponr', t);
|
|
130
|
-
END IF;
|
|
131
|
-
END LOOP;
|
|
132
|
-
END $$;
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
-- ============================================================================
|
|
2
|
-
-- 105_policies_to_authenticated.sql — no policy without `TO authenticated` (#147).
|
|
3
|
-
--
|
|
4
|
-
-- The 001-era policies were written FOR PUBLIC (no role clause): audit_logs,
|
|
5
|
-
-- invitations, invoices, notifications, profiles, subscriptions, tenants,
|
|
6
|
-
-- tenant_members/locations (compat views since #134), tenant_role*. A policy
|
|
7
|
-
-- applying to PUBLIC is evaluated for anon too, and every new table born with
|
|
8
|
-
-- a PUBLIC policy widens the anonymous surface by accident. The rule of the
|
|
9
|
-
-- authz spec (§7, non-negotiable) is `to authenticated` on every policy; the
|
|
10
|
-
-- verification suite (900_verification/001_posture) grades it, so this file
|
|
11
|
-
-- re-creates each remaining PUBLIC policy with the same predicate, scoped to
|
|
12
|
-
-- authenticated. Dynamic and idempotent: a policy that is already role-scoped
|
|
13
|
-
-- is untouched, and re-running finds nothing to do.
|
|
14
|
-
--
|
|
15
|
-
-- Anon never loses anything it should keep: the core chain has no anonymous
|
|
16
|
-
-- read surface on these tables (the storefront/booking anon paths live in the
|
|
17
|
-
-- shop/agenda plugins and are named `_anon_*` policies with an explicit role).
|
|
18
|
-
-- ============================================================================
|
|
19
|
-
DO $$
|
|
20
|
-
DECLARE r record; v_cmd text;
|
|
21
|
-
BEGIN
|
|
22
|
-
FOR r IN
|
|
23
|
-
SELECT schemaname, tablename, policyname, permissive, cmd, qual, with_check
|
|
24
|
-
FROM pg_policies
|
|
25
|
-
WHERE roles = '{public}'
|
|
26
|
-
AND (schemaname = 'app'
|
|
27
|
-
-- the generic isolation policies 002 stamps on every public table with a
|
|
28
|
-
-- tenant_id (including plugin tables it discovers on a re-run): same
|
|
29
|
-
-- predicate, so scoping them to authenticated changes nothing for a
|
|
30
|
-
-- signed-in user and closes the anonymous evaluation
|
|
31
|
-
OR policyname LIKE 'tenant\_isolation\_%'
|
|
32
|
-
-- the spine's own 001-era tables only: a plugin (shop, agenda…) that
|
|
33
|
-
-- deliberately opened a PUBLIC read policy for its anonymous surface is
|
|
34
|
-
-- its own decision, and this file must converge the same way whether it
|
|
35
|
-
-- runs before or after that plugin's migrations
|
|
36
|
-
OR (schemaname = 'public' AND tablename IN (
|
|
37
|
-
'tenants', 'profiles', 'tenant_members', 'plans', 'permissions', 'role_permissions',
|
|
38
|
-
'tenant_role_overrides', 'tenant_roles', 'invitations', 'payment_events', 'locations',
|
|
39
|
-
'location_members', 'audit_logs', 'subscriptions', 'invoices', 'notifications',
|
|
40
|
-
'fayz_projects', 'addresses')))
|
|
41
|
-
LOOP
|
|
42
|
-
v_cmd := format('CREATE POLICY %I ON %I.%I AS %s FOR %s TO authenticated',
|
|
43
|
-
r.policyname, r.schemaname, r.tablename,
|
|
44
|
-
CASE WHEN r.permissive = 'PERMISSIVE' THEN 'PERMISSIVE' ELSE 'RESTRICTIVE' END,
|
|
45
|
-
r.cmd);
|
|
46
|
-
IF r.qual IS NOT NULL THEN v_cmd := v_cmd || format(' USING (%s)', r.qual); END IF;
|
|
47
|
-
IF r.with_check IS NOT NULL THEN v_cmd := v_cmd || format(' WITH CHECK (%s)', r.with_check); END IF;
|
|
48
|
-
EXECUTE format('DROP POLICY IF EXISTS %I ON %I.%I', r.policyname, r.schemaname, r.tablename);
|
|
49
|
-
EXECUTE v_cmd;
|
|
50
|
-
END LOOP;
|
|
51
|
-
END $$;
|
|
52
|
-
|
|
53
|
-
-- ── anon executes nothing in schema app ─────────────────────────────────
|
|
54
|
-
-- Functions in `app` are born with the built-in default (EXECUTE to PUBLIC);
|
|
55
|
-
-- 050's per-schema default-privilege REVOKE cannot subtract it, so every helper
|
|
56
|
-
-- has to revoke explicitly. One slipped (a hashing helper). Sweep the schema so
|
|
57
|
-
-- the rule holds regardless of who forgets: nothing callable in `app` for anon
|
|
58
|
-
-- or PUBLIC; the intended callers (authenticated/service_role) keep what they
|
|
59
|
-
-- were granted explicitly. Trigger functions cannot be called directly anyway.
|
|
60
|
-
DO $$
|
|
61
|
-
DECLARE r record;
|
|
62
|
-
BEGIN
|
|
63
|
-
FOR r IN
|
|
64
|
-
SELECT p.oid::regprocedure AS sig
|
|
65
|
-
FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
66
|
-
WHERE n.nspname = 'app'
|
|
67
|
-
AND has_function_privilege('anon', p.oid, 'EXECUTE')
|
|
68
|
-
LOOP
|
|
69
|
-
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM public, anon', r.sig);
|
|
70
|
-
END LOOP;
|
|
71
|
-
END $$;
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
-- ============================================================================
|
|
2
|
-
-- 106_function_search_path.sql — every function in public/app/migration gets a
|
|
3
|
-
-- fixed search_path.
|
|
4
|
-
--
|
|
5
|
-
-- Found by applying the canonical chain to a real Supabase project (the
|
|
6
|
-
-- rehearsal, 2026-08-19): the platform's own linter flags 25 functions whose
|
|
7
|
-
-- search_path is resolved from the caller's session. For a SECURITY DEFINER
|
|
8
|
-
-- function that is a privilege-escalation vector — a caller who can create an
|
|
9
|
-
-- object in a schema earlier on the path decides what an unqualified name in
|
|
10
|
-
-- the body means. `public.handle_new_user` (spine 001, runs on every signup)
|
|
11
|
-
-- was the one definer in that state; the rest are trigger and helper functions
|
|
12
|
-
-- where it is a lint rather than a hole, but the rule is cheap to hold.
|
|
13
|
-
--
|
|
14
|
-
-- Done as ALTER, not CREATE OR REPLACE: the bodies are not touched, so this
|
|
15
|
-
-- cannot change behaviour beyond making name resolution deterministic. The
|
|
16
|
-
-- path pinned per schema is the one those bodies already resolve through
|
|
17
|
-
-- (their own schema first, then public and extensions), so unqualified
|
|
18
|
-
-- references keep meaning what they mean today. Functions that already declare
|
|
19
|
-
-- a search_path — every RPC this phase adds declares `SET search_path = ''` —
|
|
20
|
-
-- are left alone.
|
|
21
|
-
--
|
|
22
|
-
-- Idempotent: re-running finds nothing without a search_path and does nothing.
|
|
23
|
-
-- ============================================================================
|
|
24
|
-
DO $$
|
|
25
|
-
DECLARE
|
|
26
|
-
r record;
|
|
27
|
-
v_path text;
|
|
28
|
-
BEGIN
|
|
29
|
-
FOR r IN
|
|
30
|
-
SELECT p.oid, n.nspname, p.oid::regprocedure AS sig, p.prosecdef
|
|
31
|
-
FROM pg_proc p
|
|
32
|
-
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
33
|
-
WHERE n.nspname IN ('public', 'app', 'migration')
|
|
34
|
-
AND p.prokind = 'f'
|
|
35
|
-
AND NOT EXISTS (
|
|
36
|
-
SELECT 1 FROM unnest(coalesce(p.proconfig, '{}')) c WHERE c LIKE 'search_path=%')
|
|
37
|
-
-- NOT OURS TO PIN. An extension installed into public brings its own
|
|
38
|
-
-- functions with it — school has btree_gist, so gbt_date_penalty and
|
|
39
|
-
-- friends sit in public owned by a superuser, and the sweep stops on
|
|
40
|
-
-- "must be owner of function". Even with the rights it would be the wrong
|
|
41
|
-
-- move: those bodies belong to the extension, its next upgrade rewrites
|
|
42
|
-
-- them, and the lint they trip is the extension author's to answer.
|
|
43
|
-
AND NOT EXISTS (
|
|
44
|
-
SELECT 1 FROM pg_depend d
|
|
45
|
-
WHERE d.objid = p.oid AND d.classid = 'pg_proc'::regclass AND d.deptype = 'e')
|
|
46
|
-
-- and never a function this role cannot alter, whatever brought it here.
|
|
47
|
-
AND pg_catalog.pg_has_role(p.proowner, 'USAGE')
|
|
48
|
-
LOOP
|
|
49
|
-
v_path := CASE r.nspname
|
|
50
|
-
WHEN 'public' THEN 'public, extensions, pg_temp'
|
|
51
|
-
ELSE format('%I, public, extensions, pg_temp', r.nspname)
|
|
52
|
-
END;
|
|
53
|
-
EXECUTE format('ALTER FUNCTION %s SET search_path = %s', r.sig, v_path);
|
|
54
|
-
END LOOP;
|
|
55
|
-
END $$;
|
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
-- ============================================================================
|
|
2
|
-
-- 107_core_table_prefix.sql — eight core tables drop the `plg_` prefix.
|
|
3
|
-
--
|
|
4
|
-
-- `plg_` means "owned by plugin <name>": docs/plugins/PLUGIN-CONVENTION.md §3
|
|
5
|
-
-- reads every table a plugin owns as `plg_<plugin>_*`, and
|
|
6
|
-
-- scripts/emit-plg-census.mjs is the ledger of that rule. Eight tables of the
|
|
7
|
-
-- CORE SPINE wear the prefix with no plugin segment at all, so the name claims
|
|
8
|
-
-- an owner that does not exist and the census has to special-case them:
|
|
9
|
-
--
|
|
10
|
-
-- plg_analytics_read_models → analytics_read_models (024)
|
|
11
|
-
-- plg_connections → connections (029)
|
|
12
|
-
-- plg_connection_secrets → connection_secrets (032)
|
|
13
|
-
-- plg_connector_schedules → connector_schedules (033)
|
|
14
|
-
-- plg_entity_fields → entity_fields (035)
|
|
15
|
-
-- plg_event_log → event_log (027)
|
|
16
|
-
-- plg_sync_runs → sync_runs (029)
|
|
17
|
-
-- plg_sync_schedule → sync_schedule (033)
|
|
18
|
-
--
|
|
19
|
-
-- WHY THIS FILE AND NOT AN EDIT OF 024/027/029/031/032/033/035. Those files are
|
|
20
|
-
-- applied in production pools; an applied migration is never edited (the ledger
|
|
21
|
-
-- turns an edit into a hard stop). The rename is therefore additive: the frozen
|
|
22
|
-
-- files keep creating the old names on a pool that has never seen them, and
|
|
23
|
-
-- this file carries them over.
|
|
24
|
-
--
|
|
25
|
-
-- WHY NO COMPAT VIEWS UNDER THE OLD NAMES. The tenancy retirement
|
|
26
|
-
-- (063_legacy_tenancy_retired.sql) proved that shape impossible here: on the
|
|
27
|
-
-- bench's force-replay pass the frozen files re-run and execute
|
|
28
|
-
-- `ALTER TABLE public.plg_x ENABLE ROW LEVEL SECURITY`, `ADD COLUMN IF NOT
|
|
29
|
-
-- EXISTS`, `CREATE POLICY … ON public.plg_x` — every one of which errors with
|
|
30
|
-
-- "this operation is not supported for views". 053 answered that with
|
|
31
|
-
-- projection TABLES because the legacy names had to keep taking writes. Here
|
|
32
|
-
-- they must not: the whole point is that the old name stops existing.
|
|
33
|
-
--
|
|
34
|
-
-- WHAT A REPLAY LOOKS LIKE. On a re-apply the frozen files run again and
|
|
35
|
-
-- `CREATE TABLE IF NOT EXISTS public.plg_x` re-creates the old name EMPTY,
|
|
36
|
-
-- beside the renamed one that holds the data. So each table has exactly three
|
|
37
|
-
-- states and this file handles all three:
|
|
38
|
-
--
|
|
39
|
-
-- 1. old exists, new does not → ALTER TABLE … RENAME TO. The data, indexes,
|
|
40
|
-
-- constraints, policies, grants and dependent FKs all travel with it.
|
|
41
|
-
-- 2. both exist → the old one can only be that empty
|
|
42
|
-
-- re-creation. DROP it when it has zero rows; RAISE EXCEPTION when it has
|
|
43
|
-
-- any, because a pool with a live writer on the old name must fail loudly
|
|
44
|
-
-- rather than lose rows.
|
|
45
|
-
-- 3. neither/only the new one → nothing to do. Re-running is a no-op.
|
|
46
|
-
--
|
|
47
|
-
-- plg_event_log is ALSO declared by plugins/plugin-automations 0002_event_log
|
|
48
|
-
-- ("core table also CREATEd by this plugin" in the census). That file is
|
|
49
|
-
-- applied too and is not edited: its `CREATE TABLE IF NOT EXISTS` is a no-op on
|
|
50
|
-
-- a pool that has 027, and where it does re-create the empty duplicate this
|
|
51
|
-
-- file drops it by branch 2. THE CORE OWNS event_log — 027 is the definition,
|
|
52
|
-
-- automations is a legacy first writer.
|
|
53
|
-
--
|
|
54
|
-
-- SCOPE: TABLES ONLY. The core functions that carry the same prefix
|
|
55
|
-
-- (plg_emit_event, plg_claim_effect, plg_settle_effect, plg_prune_sync_runs,
|
|
56
|
-
-- plg_store_connection_secret, plg_read_connection_secret,
|
|
57
|
-
-- plg_drop_connection_secrets, plg_register_connector_schedule,
|
|
58
|
-
-- plg_set_scheduler_endpoint, plg_sync_tick, plg_emit_row_event,
|
|
59
|
-
-- plg_attach_event_source) are called by name from TypeScript and from edge
|
|
60
|
-
-- functions that are already DEPLOYED, so renaming them is a separate wave —
|
|
61
|
-
-- see docs/data/phase-0/12-core-table-prefix.md. Their BODIES are repointed
|
|
62
|
-
-- here (§3), because a body still naming plg_connections is a runtime error the
|
|
63
|
-
-- moment the table is gone.
|
|
64
|
-
-- ============================================================================
|
|
65
|
-
|
|
66
|
-
-- ── §1 the tables ──────────────────────────────────────────────────────────
|
|
67
|
-
DO $$
|
|
68
|
-
DECLARE
|
|
69
|
-
-- Children before parents, so a duplicate is dropped before the duplicate it
|
|
70
|
-
-- references (plg_connection_secrets / plg_sync_runs / plg_sync_schedule all
|
|
71
|
-
-- FK plg_connections).
|
|
72
|
-
v_pairs text[][] := ARRAY[
|
|
73
|
-
['plg_connection_secrets', 'connection_secrets'],
|
|
74
|
-
['plg_sync_runs', 'sync_runs'],
|
|
75
|
-
['plg_sync_schedule', 'sync_schedule'],
|
|
76
|
-
['plg_connections', 'connections'],
|
|
77
|
-
['plg_connector_schedules', 'connector_schedules'],
|
|
78
|
-
['plg_analytics_read_models', 'analytics_read_models'],
|
|
79
|
-
['plg_entity_fields', 'entity_fields'],
|
|
80
|
-
['plg_event_log', 'event_log']
|
|
81
|
-
];
|
|
82
|
-
i integer;
|
|
83
|
-
v_old text;
|
|
84
|
-
v_new text;
|
|
85
|
-
v_rows bigint;
|
|
86
|
-
BEGIN
|
|
87
|
-
-- Pass A — branch 2: the empty re-creation left by a replay of the frozen file.
|
|
88
|
-
FOR i IN 1 .. array_length(v_pairs, 1) LOOP
|
|
89
|
-
v_old := v_pairs[i][1];
|
|
90
|
-
v_new := v_pairs[i][2];
|
|
91
|
-
CONTINUE WHEN to_regclass('public.' || quote_ident(v_old)) IS NULL;
|
|
92
|
-
CONTINUE WHEN to_regclass('public.' || quote_ident(v_new)) IS NULL;
|
|
93
|
-
|
|
94
|
-
EXECUTE format('SELECT count(*) FROM public.%I', v_old) INTO v_rows;
|
|
95
|
-
IF v_rows > 0 THEN
|
|
96
|
-
RAISE EXCEPTION
|
|
97
|
-
'public.% still holds % row(s) while public.% exists: something is still WRITING the old name. Point that writer at public.% and move the rows across before re-applying 097 — this migration will not drop data.',
|
|
98
|
-
v_old, v_rows, v_new, v_new
|
|
99
|
-
USING ERRCODE = 'raise_exception';
|
|
100
|
-
END IF;
|
|
101
|
-
|
|
102
|
-
EXECUTE format('DROP TABLE public.%I', v_old);
|
|
103
|
-
RAISE NOTICE '097: dropped the empty re-creation public.% (public.% is the table)', v_old, v_new;
|
|
104
|
-
END LOOP;
|
|
105
|
-
|
|
106
|
-
-- Pass B — branch 1: the rename that carries everything.
|
|
107
|
-
FOR i IN 1 .. array_length(v_pairs, 1) LOOP
|
|
108
|
-
v_old := v_pairs[i][1];
|
|
109
|
-
v_new := v_pairs[i][2];
|
|
110
|
-
CONTINUE WHEN to_regclass('public.' || quote_ident(v_old)) IS NULL;
|
|
111
|
-
CONTINUE WHEN to_regclass('public.' || quote_ident(v_new)) IS NOT NULL;
|
|
112
|
-
|
|
113
|
-
EXECUTE format('ALTER TABLE public.%I RENAME TO %I', v_old, v_new);
|
|
114
|
-
RAISE NOTICE '097: renamed public.% → public.%', v_old, v_new;
|
|
115
|
-
END LOOP;
|
|
116
|
-
END $$;
|
|
117
|
-
|
|
118
|
-
-- ── §2 the names bolted to those tables ────────────────────────────────────
|
|
119
|
-
-- A rename leaves every index, constraint, trigger and policy under its old
|
|
120
|
-
-- name, so `\d connections` would still read plg_. Each is renamed by dropping the
|
|
121
|
-
-- `plg_` segment, and only when the target name is free — on a replay the
|
|
122
|
-
-- frozen file has re-created the old names on the (already dropped) duplicate,
|
|
123
|
-
-- so this loop simply finds nothing left to do.
|
|
124
|
-
DO $$
|
|
125
|
-
DECLARE
|
|
126
|
-
v_tables text[] := ARRAY[
|
|
127
|
-
'analytics_read_models', 'connection_secrets', 'connections', 'connector_schedules',
|
|
128
|
-
'entity_fields', 'event_log', 'sync_runs', 'sync_schedule'
|
|
129
|
-
];
|
|
130
|
-
r record;
|
|
131
|
-
v_new text;
|
|
132
|
-
BEGIN
|
|
133
|
-
-- Constraints first: renaming a PRIMARY KEY / UNIQUE constraint renames the
|
|
134
|
-
-- index behind it, so the index loop below then has nothing to say about it.
|
|
135
|
-
FOR r IN
|
|
136
|
-
SELECT c.conname, t.relname AS tbl
|
|
137
|
-
FROM pg_constraint c
|
|
138
|
-
JOIN pg_class t ON t.oid = c.conrelid
|
|
139
|
-
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
140
|
-
WHERE n.nspname = 'public'
|
|
141
|
-
AND t.relname = ANY (v_tables)
|
|
142
|
-
AND c.conname LIKE '%plg\_%'
|
|
143
|
-
ORDER BY t.relname, c.conname
|
|
144
|
-
LOOP
|
|
145
|
-
v_new := replace(r.conname, 'plg_', '');
|
|
146
|
-
CONTINUE WHEN v_new = r.conname OR v_new = '';
|
|
147
|
-
CONTINUE WHEN EXISTS (
|
|
148
|
-
SELECT 1 FROM pg_constraint c2
|
|
149
|
-
JOIN pg_class t2 ON t2.oid = c2.conrelid
|
|
150
|
-
WHERE t2.relname = r.tbl AND c2.conname = v_new);
|
|
151
|
-
EXECUTE format('ALTER TABLE public.%I RENAME CONSTRAINT %I TO %I', r.tbl, r.conname, v_new);
|
|
152
|
-
END LOOP;
|
|
153
|
-
|
|
154
|
-
FOR r IN
|
|
155
|
-
SELECT ic.relname AS idx, t.relname AS tbl
|
|
156
|
-
FROM pg_index i
|
|
157
|
-
JOIN pg_class ic ON ic.oid = i.indexrelid
|
|
158
|
-
JOIN pg_class t ON t.oid = i.indrelid
|
|
159
|
-
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
160
|
-
WHERE n.nspname = 'public'
|
|
161
|
-
AND t.relname = ANY (v_tables)
|
|
162
|
-
AND ic.relname LIKE '%plg\_%'
|
|
163
|
-
ORDER BY t.relname, ic.relname
|
|
164
|
-
LOOP
|
|
165
|
-
v_new := replace(r.idx, 'plg_', '');
|
|
166
|
-
CONTINUE WHEN v_new = r.idx OR v_new = '';
|
|
167
|
-
CONTINUE WHEN to_regclass('public.' || quote_ident(v_new)) IS NOT NULL;
|
|
168
|
-
EXECUTE format('ALTER INDEX public.%I RENAME TO %I', r.idx, v_new);
|
|
169
|
-
END LOOP;
|
|
170
|
-
|
|
171
|
-
FOR r IN
|
|
172
|
-
SELECT g.tgname AS trg, t.relname AS tbl
|
|
173
|
-
FROM pg_trigger g
|
|
174
|
-
JOIN pg_class t ON t.oid = g.tgrelid
|
|
175
|
-
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
176
|
-
WHERE n.nspname = 'public'
|
|
177
|
-
AND NOT g.tgisinternal
|
|
178
|
-
AND t.relname = ANY (v_tables)
|
|
179
|
-
AND g.tgname LIKE '%plg\_%'
|
|
180
|
-
ORDER BY t.relname, g.tgname
|
|
181
|
-
LOOP
|
|
182
|
-
v_new := replace(r.trg, 'plg_', '');
|
|
183
|
-
CONTINUE WHEN v_new = r.trg OR v_new = '';
|
|
184
|
-
CONTINUE WHEN EXISTS (
|
|
185
|
-
SELECT 1 FROM pg_trigger g2
|
|
186
|
-
JOIN pg_class t2 ON t2.oid = g2.tgrelid
|
|
187
|
-
WHERE t2.relname = r.tbl AND g2.tgname = v_new);
|
|
188
|
-
EXECUTE format('ALTER TRIGGER %I ON public.%I RENAME TO %I', r.trg, r.tbl, v_new);
|
|
189
|
-
END LOOP;
|
|
190
|
-
|
|
191
|
-
FOR r IN
|
|
192
|
-
SELECT p.policyname AS pol, p.tablename AS tbl
|
|
193
|
-
FROM pg_policies p
|
|
194
|
-
WHERE p.schemaname = 'public'
|
|
195
|
-
AND p.tablename = ANY (v_tables)
|
|
196
|
-
AND p.policyname LIKE '%plg\_%'
|
|
197
|
-
ORDER BY p.tablename, p.policyname
|
|
198
|
-
LOOP
|
|
199
|
-
v_new := replace(r.pol, 'plg_', '');
|
|
200
|
-
CONTINUE WHEN v_new = r.pol OR v_new = '';
|
|
201
|
-
CONTINUE WHEN EXISTS (
|
|
202
|
-
SELECT 1 FROM pg_policies p2
|
|
203
|
-
WHERE p2.schemaname = 'public' AND p2.tablename = r.tbl AND p2.policyname = v_new);
|
|
204
|
-
EXECUTE format('ALTER POLICY %I ON public.%I RENAME TO %I', r.pol, r.tbl, v_new);
|
|
205
|
-
END LOOP;
|
|
206
|
-
END $$;
|
|
207
|
-
|
|
208
|
-
-- ── §3 every function body that still names an old table ───────────────────
|
|
209
|
-
-- Done the way 096 pinned search_path: read what the database has, rewrite it,
|
|
210
|
-
-- put it back — rather than restating a dozen frozen bodies here and letting
|
|
211
|
-
-- the copy rot. `pg_get_functiondef` returns CREATE OR REPLACE, so the OID,
|
|
212
|
-
-- owner, ACL and every dependency on the function survive; only the eight table
|
|
213
|
-
-- names change, and only where they appear as whole words (`\m…\M`), so
|
|
214
|
-
-- plg_prune_sync_runs, uq_plg_connections_account_key and
|
|
215
|
-
-- plg_connections_member_read are untouched.
|
|
216
|
-
--
|
|
217
|
-
-- Extension-owned functions are skipped, and a function whose text does not
|
|
218
|
-
-- change is not re-created — so a replay finds nothing to do.
|
|
219
|
-
DO $$
|
|
220
|
-
DECLARE
|
|
221
|
-
v_names text[] := ARRAY[
|
|
222
|
-
'analytics_read_models', 'connection_secrets', 'connections', 'connector_schedules',
|
|
223
|
-
'entity_fields', 'event_log', 'sync_runs', 'sync_schedule'
|
|
224
|
-
];
|
|
225
|
-
r record;
|
|
226
|
-
v_def text;
|
|
227
|
-
v_name text;
|
|
228
|
-
i integer;
|
|
229
|
-
v_count integer := 0;
|
|
230
|
-
BEGIN
|
|
231
|
-
FOR r IN
|
|
232
|
-
SELECT p.oid, p.oid::regprocedure AS sig, pg_get_functiondef(p.oid) AS def
|
|
233
|
-
FROM pg_proc p
|
|
234
|
-
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
235
|
-
WHERE n.nspname IN ('public', 'app', 'migration')
|
|
236
|
-
AND p.prokind IN ('f', 'p')
|
|
237
|
-
AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = p.oid AND d.deptype = 'e')
|
|
238
|
-
ORDER BY p.oid
|
|
239
|
-
LOOP
|
|
240
|
-
v_def := r.def;
|
|
241
|
-
FOR i IN 1 .. array_length(v_names, 1) LOOP
|
|
242
|
-
v_name := v_names[i];
|
|
243
|
-
v_def := regexp_replace(v_def, '\mplg_' || v_name || '\M', v_name, 'g');
|
|
244
|
-
END LOOP;
|
|
245
|
-
IF v_def IS DISTINCT FROM r.def THEN
|
|
246
|
-
EXECUTE v_def;
|
|
247
|
-
v_count := v_count + 1;
|
|
248
|
-
END IF;
|
|
249
|
-
END LOOP;
|
|
250
|
-
IF v_count > 0 THEN
|
|
251
|
-
RAISE NOTICE '097: repointed % function(s) at the unprefixed core tables', v_count;
|
|
252
|
-
END IF;
|
|
253
|
-
END $$;
|
|
254
|
-
|
|
255
|
-
-- ── §4 what the tables say about themselves ────────────────────────────────
|
|
256
|
-
-- Guarded per table: the spine bench (packages/db/test/run-migrations.sh)
|
|
257
|
-
-- applies a subset of the chain, so not every one of the eight is present in
|
|
258
|
-
-- every place this file runs.
|
|
259
|
-
DO $$
|
|
260
|
-
DECLARE
|
|
261
|
-
v_comments text[][] := ARRAY[
|
|
262
|
-
['analytics_read_models',
|
|
263
|
-
'Allowlist of read models analytics_run() may query. Core (spine 024): a view that is not registered here cannot be reached, however well-formed the card is.'],
|
|
264
|
-
['connections',
|
|
265
|
-
'One tenant''s connection to one external provider, per instance_key. Core (spine 029): the connector framework''s table, used by every connector and owned by none of them.'],
|
|
266
|
-
['connection_secrets',
|
|
267
|
-
'Credential holder for a connection — reachable only through plg_store_connection_secret / plg_read_connection_secret (spine 032). No client role has any privilege on it.'],
|
|
268
|
-
['connector_schedules',
|
|
269
|
-
'How often each connector needs waking, and which edge function + action to wake. Written by a connector''s own migration through plg_register_connector_schedule (spine 033). Granted to no client role: it decides how much of a provider''s quota the whole pool spends.'],
|
|
270
|
-
['entity_fields',
|
|
271
|
-
'The field registry (spine 035): the merchant-defined keys a tenant may write into an entity''s custom_fields, and the rules they are validated against.'],
|
|
272
|
-
['event_log',
|
|
273
|
-
'The domain event log (spine 027) — CORE, not plugin-automations''. plugins/plugin-automations 0002 also CREATEs it as the legacy first writer; the spine owns the definition and plg_emit_event is the only writer.'],
|
|
274
|
-
['sync_runs',
|
|
275
|
-
'One execution of the sync plane, per connection (spine 029). No INSERT grant for authenticated: a history the application can write is not a history.'],
|
|
276
|
-
['sync_schedule',
|
|
277
|
-
'The clock''s bookkeeping: when each connection is next due, what is in flight, how many consecutive failures. Not a history — sync_runs is. Rows are enrolled lazily by plg_sync_tick, with a deterministic offset so a cold start spreads instead of stampeding.']
|
|
278
|
-
];
|
|
279
|
-
i integer;
|
|
280
|
-
BEGIN
|
|
281
|
-
FOR i IN 1 .. array_length(v_comments, 1) LOOP
|
|
282
|
-
CONTINUE WHEN to_regclass('public.' || quote_ident(v_comments[i][1])) IS NULL;
|
|
283
|
-
EXECUTE format('COMMENT ON TABLE public.%I IS %L', v_comments[i][1], v_comments[i][2]);
|
|
284
|
-
END LOOP;
|
|
285
|
-
END $$;
|
|
286
|
-
|
|
287
|
-
-- ── §5 the access contract, re-asserted on the new names ───────────────────
|
|
288
|
-
-- THE SHARP EDGE OF THIS RENAME, and the reason this section exists.
|
|
289
|
-
--
|
|
290
|
-
-- 007_archetype_grants and 008_grants hand `authenticated` and `service_role`
|
|
291
|
-
-- `GRANT ALL ON ALL TABLES IN SCHEMA public`, and `anon` SELECT on all of them.
|
|
292
|
-
-- Those are blanket statements over whatever exists when they run. Each of the
|
|
293
|
-
-- eight tables then strips itself back in its own file (029/032/033/035) —
|
|
294
|
-
-- `REVOKE ALL … FROM anon, authenticated`, then grants back only what the table
|
|
295
|
-
-- needs.
|
|
296
|
-
--
|
|
297
|
-
-- On a REPLAY that pairing breaks: the blanket GRANT lands on the renamed
|
|
298
|
-
-- table (`connections`), while the REVOKE in the frozen file lands on the empty
|
|
299
|
-
-- `plg_connections` the same replay just re-created — so the tables would come
|
|
300
|
-
-- out of a replay with `authenticated` holding INSERT/UPDATE/DELETE on the run
|
|
301
|
-
-- history, the credential holder and the scheduler, and `anon` holding SELECT
|
|
302
|
-
-- until 009 takes it back. The bench's F3e regression is exactly this, caught.
|
|
303
|
-
--
|
|
304
|
-
-- So the contract is restated here, on the new names, word for word from the
|
|
305
|
-
-- files that own it. Guarded per table: the spine bench applies a subset of the
|
|
306
|
-
-- chain, so not all eight exist everywhere this runs.
|
|
307
|
-
DO $$
|
|
308
|
-
DECLARE
|
|
309
|
-
v_sql text[] := ARRAY[
|
|
310
|
-
-- 024: the read-model allowlist is readable by any member, written by the migration role.
|
|
311
|
-
'GRANT SELECT ON public.analytics_read_models TO authenticated',
|
|
312
|
-
'GRANT ALL ON public.analytics_read_models TO service_role',
|
|
313
|
-
-- 029: a member manages its connections and only READS their history.
|
|
314
|
-
'REVOKE ALL ON public.connections FROM anon, authenticated',
|
|
315
|
-
'REVOKE ALL ON public.sync_runs FROM anon, authenticated',
|
|
316
|
-
'GRANT SELECT, INSERT, UPDATE, DELETE ON public.connections TO authenticated',
|
|
317
|
-
'GRANT SELECT ON public.sync_runs TO authenticated',
|
|
318
|
-
-- 032: the credential holder is reachable through its RPCs and nothing else.
|
|
319
|
-
'REVOKE ALL ON public.connection_secrets FROM PUBLIC',
|
|
320
|
-
'REVOKE ALL ON public.connection_secrets FROM anon, authenticated',
|
|
321
|
-
-- 033: neither clock table is tenant data, and that is why both are shut.
|
|
322
|
-
'REVOKE ALL ON public.connector_schedules FROM PUBLIC',
|
|
323
|
-
'REVOKE ALL ON public.connector_schedules FROM anon, authenticated',
|
|
324
|
-
'REVOKE ALL ON public.sync_schedule FROM PUBLIC',
|
|
325
|
-
'REVOKE ALL ON public.sync_schedule FROM anon, authenticated',
|
|
326
|
-
-- 035: a member edits its own field definitions, never another tenant's.
|
|
327
|
-
'REVOKE ALL ON public.entity_fields FROM anon, authenticated',
|
|
328
|
-
'GRANT SELECT, INSERT, UPDATE, DELETE ON public.entity_fields TO authenticated'
|
|
329
|
-
];
|
|
330
|
-
i integer;
|
|
331
|
-
v_tbl text;
|
|
332
|
-
BEGIN
|
|
333
|
-
FOR i IN 1 .. array_length(v_sql, 1) LOOP
|
|
334
|
-
v_tbl := substring(v_sql[i] from 'public\.[a-z_]+');
|
|
335
|
-
CONTINUE WHEN to_regclass(v_tbl) IS NULL;
|
|
336
|
-
EXECUTE v_sql[i];
|
|
337
|
-
END LOOP;
|
|
338
|
-
END $$;
|
|
339
|
-
|
|
340
|
-
-- ── §6 the one applied file this rename could not fix forward ──────────────
|
|
341
|
-
-- Every consumer of the eight names is repointed either in place (source that
|
|
342
|
-
-- is not an applied migration) or by a NEW migration in the owning package
|
|
343
|
-
-- (plugin-banking-br 004, @fayz-ai/courses 0011, @fayz-ai/shop 0067) — an
|
|
344
|
-
-- applied file is never edited. @fayz-ai/shop 0066_analytics_read_models.sql is
|
|
345
|
-
-- the single exception: its `INSERT INTO public.plg_analytics_read_models` is a
|
|
346
|
-
-- TOP-LEVEL statement, so on a fresh pool it would error before any later file
|
|
347
|
-
-- could repair anything. It is edited, which moves its checksum.
|
|
348
|
-
--
|
|
349
|
-
-- Forget that one ledger row so the pool re-applies the file (it is idempotent:
|
|
350
|
-
-- CREATE OR REPLACE VIEW + INSERT … ON CONFLICT DO UPDATE) instead of stopping
|
|
351
|
-
-- on the drift guard forever. `fayz db pool apply` snapshots the ledger before
|
|
352
|
-
-- the first file runs, so the apply that carries THIS migration still stops on
|
|
353
|
-
-- 0066 and the NEXT one applies it — two passes, both safe, and the operator
|
|
354
|
-
-- sees exactly which file moved. The bench's runner reads the ledger per file
|
|
355
|
-
-- and converges in one.
|
|
356
|
-
DO $$
|
|
357
|
-
BEGIN
|
|
358
|
-
IF to_regclass('public._migrations') IS NOT NULL THEN
|
|
359
|
-
DELETE FROM public._migrations WHERE file_name = '0066_analytics_read_models.sql';
|
|
360
|
-
END IF;
|
|
361
|
-
END $$;
|