@fayz-ai/db 0.12.0 → 0.13.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 +49 -0
- package/migrations/010_migration_ledger.sql +11 -1
- package/migrations/040_resource_grants.sql +2 -2
- package/migrations/041_scoped_columns.sql +2 -2
- package/migrations/042_unit_scope_policies.sql +1 -1
- package/migrations/043_view_invoker.sql +12 -3
- package/migrations/044_unit_member_facts.sql +1 -1
- package/migrations/045_unit_entry.sql +1 -1
- package/migrations/046_membership_visible_to_members.sql +1 -1
- package/migrations/047_tasks.sql +4 -4
- package/migrations/048_every_login_is_a_person.sql +1 -1
- package/migrations/049_bookable_people.sql +1 -1
- package/migrations/050_authz_core.sql +230 -0
- package/migrations/051_authz_helpers.sql +330 -0
- package/migrations/052_authz_fanout.sql +253 -0
- package/migrations/053_authz_catalog_templates.sql +402 -0
- package/migrations/060_tenancy_seam.sql +132 -0
- package/migrations/061_create_tenant.sql +125 -0
- package/migrations/062_invitations.sql +329 -0
- package/migrations/063_legacy_tenancy_retired.sql +568 -0
- package/migrations/064_agent_guard_authz.sql +118 -0
- package/migrations/064b_supersede_unit_scope.sql +205 -0
- package/migrations/065_scaffold_sweep.sql +139 -0
- package/migrations/066_shadow_report.sql +122 -0
- package/migrations/070_permission_catalog.sql +205 -0
- package/migrations/071_v1_permission_map.sql +266 -0
- package/migrations/072_role_rpcs.sql +126 -0
- package/migrations/073_legacy_rbac_retire.sql +471 -0
- package/migrations/075_company_legal_entities.sql +169 -0
- package/migrations/076_tenant_settings.sql +251 -0
- package/migrations/080_people_roles.sql +269 -0
- package/migrations/081_people_identity.sql +271 -0
- package/migrations/082_people_membership_link.sql +142 -0
- package/migrations/083_people_data_migration.sql +75 -0
- package/migrations/084_people_registrations_tokens.sql +282 -0
- package/migrations/085_catalog_products.sql +333 -0
- package/migrations/086_catalog_pricing.sql +194 -0
- package/migrations/087_catalog_packages.sql +134 -0
- package/migrations/090_orders_venda.sql +166 -0
- package/migrations/091_orders_scaffold.sql +92 -0
- package/migrations/095_config_field_rules.sql +261 -0
- package/migrations/096_audit_append_only.sql +256 -0
- package/migrations/097_sequences_authorized.sql +111 -0
- package/migrations/098_holidays.sql +80 -0
- package/migrations/099_access_grants.sql +293 -0
- package/migrations/100_migration_schema.sql +217 -0
- package/migrations/101_migration_allowlist.sql +202 -0
- package/migrations/102_migration_rpcs.sql +933 -0
- package/migrations/103_migration_reconcile_ponr.sql +132 -0
- package/migrations/105_policies_to_authenticated.sql +71 -0
- package/migrations/106_function_search_path.sql +55 -0
- package/migrations/107_core_table_prefix.sql +361 -0
- package/migrations/108_authz_write_guards.sql +149 -0
- package/migrations/109_catalog_migration_promoted_columns.sql +126 -0
- package/migrations/110_people_kind_vocabulary.sql +120 -0
- package/migrations/111_migration_excluded_is_a_decision.sql +259 -0
- package/migrations/111_replacing_a_view_keeps_its_dependents.sql +132 -0
- package/migrations/112_money_precision_standard.sql +132 -0
- package/migrations/113_addresses_units_companies.sql +114 -0
- package/migrations/114_migrations_ledger_immutable.sql +65 -0
- package/migrations/115_registration_honours_field_rules.sql +172 -0
- package/migrations/116_field_rules_check_needs_a_tenant.sql +72 -0
- package/migrations/117_authz_reconciliation_schedule.sql +82 -0
- package/migrations/118_migration_catalog_writers.sql +450 -0
- package/migrations/119_migration_config_writers.sql +193 -0
- package/migrations/120_migration_facet_writer.sql +139 -0
- package/migrations/121_migration_financial_writers.sql +294 -0
- package/migrations/122_migration_inventory_writers.sql +245 -0
- package/migrations/123_migration_order_item_writer.sql +144 -0
- package/migrations/124_migration_ledger_writers.sql +170 -0
- package/migrations/125_migration_authz_people_writers.sql +205 -0
- package/migrations/126_owner_scoping_is_configuration.sql +247 -0
- package/migrations/127_scaffold_audit_hook.sql +98 -0
- package/migrations/128_shadow_tables_enforced.sql +93 -0
- package/migrations/129_order_kind_terminal_states.sql +57 -0
- package/migrations/130_every_login_is_a_person.sql +196 -0
- package/migrations/131_migration_invoice_split.sql +127 -0
- package/migrations/132_audit_prune_scheduled.sql +28 -0
- package/migrations/133_unit_is_a_session_property.sql +155 -0
- package/migrations/134_field_rules_on_read.sql +341 -0
- package/migrations/135_distribution_polarity.sql +158 -0
- package/migrations/136_unit_id_means_locality.sql +358 -0
- package/migrations/137_vendor_named_spine_objects.sql +126 -0
- package/migrations/138_grants_inherit_down_the_tree.sql +178 -0
- package/migrations/139_distributable_is_a_scaffold_option.sql +109 -0
- package/migrations/140_masking_is_a_scaffold_option.sql +85 -0
- package/migrations/141_renamed_objects_keep_a_compat_name.sql +192 -0
- package/migrations/142_a_person_has_addresses.sql +212 -0
- package/migrations/143_a_professional_has_a_rate.sql +75 -0
- package/migrations/144_a_product_can_be_sold_in_sizes.sql +57 -0
- package/migrations/145_the_app_says_which_tenant_it_is_in.sql +123 -0
- package/migrations/146_products_stock_has_one_writer.sql +67 -0
- package/migrations/147_every_archetype_has_a_kind_registry.sql +388 -0
- package/migrations/148_the_legacy_archetype_stops_being_writable.sql +154 -0
- package/migrations/149_the_event_log_gets_a_consumer.sql +331 -0
- package/migrations/150_the_app_is_configured_in_the_database.sql +285 -0
- package/migrations/151_stock_balance_is_an_archetype.sql +144 -0
- package/migrations/152_a_file_is_an_archetype.sql +222 -0
- package/migrations/153_a_plugin_calls_a_service_through_one_door.sql +199 -0
- package/migrations/154_the_plan_is_a_ceiling.sql +258 -0
- package/migrations/155_a_tenant_can_leave.sql +271 -0
- package/migrations/156_the_service_door_has_a_server_side.sql +335 -0
- package/migrations/157_nobody_creates_objects_in_public.sql +74 -0
- package/migrations/158_the_registry_owns_the_status_vocabulary.sql +153 -0
- package/migrations/159_the_plan_catalogue_gets_its_first_rows.sql +132 -0
- package/migrations/160_the_vertical_decides_what_starts_on.sql +246 -0
- package/migrations/161_a_tenant_leaves_with_its_data.sql +363 -0
- package/migrations/162_every_name_has_one_creator_for_real.sql +126 -0
- package/migrations/163_a_species_nobody_declared_goes_to_quarantine.sql +184 -0
- package/migrations/164_the_sequence_table_joins_the_register.sql +26 -0
- package/migrations/165_a_view_is_not_a_way_in.sql +90 -0
- package/migrations/166_the_platform_billing_leftovers_go.sql +72 -0
- package/migrations/167_a_view_gets_back_what_its_table_grants.sql +112 -0
- package/migrations/168_the_grant_census_stops_crying_wolf.sql +33 -0
- package/migrations/169_the_census_asks_postgres_instead_of_guessing.sql +45 -0
- package/package.json +3 -2
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 139_distributable_is_a_scaffold_option.sql — multi-unit distribution stops
|
|
3
|
+
-- being hand-wired, and `people` gets it (#224).
|
|
4
|
+
--
|
|
5
|
+
-- The RESTRICTIVE release policy was written by hand on six catalog tables and
|
|
6
|
+
-- was absent from `people`. So "a provider serves 2 of 15 branches" was covered
|
|
7
|
+
-- when the provider LOGS IN (a membership plus two membership_units rows) and not
|
|
8
|
+
-- covered when the provider is a record — which is most providers.
|
|
9
|
+
--
|
|
10
|
+
-- The mechanism was always generic: `resource_grants` is
|
|
11
|
+
-- (resource_type, resource_id, relation, subject_type, subject_id) and serves
|
|
12
|
+
-- anything. What was not generic is the wiring, and for an SDK meant to carry a
|
|
13
|
+
-- fleet, "does this table support multi-unit distribution" cannot depend on
|
|
14
|
+
-- whether someone remembered to write a policy.
|
|
15
|
+
--
|
|
16
|
+
-- TWO SHAPES, because the six hand-written policies had two:
|
|
17
|
+
--
|
|
18
|
+
-- self-keyed products, services, service_packages
|
|
19
|
+
-- `unit_id IS NOT NULL OR available_in_my_units(rt, id)`
|
|
20
|
+
-- the unit escape is the point: a row that BELONGS to a branch is
|
|
21
|
+
-- governed by has_unit, and only a network row is distributed
|
|
22
|
+
-- (#225 — "Vela B2" is a real, tested case)
|
|
23
|
+
-- parent-keyed product_variants, price_table_items, service_package_items
|
|
24
|
+
-- `available_in_my_units(rt, <parent>_id)`
|
|
25
|
+
-- a variant is released with the product it varies, never on its
|
|
26
|
+
-- own; no escape, because the parent's row is the one that has a
|
|
27
|
+
-- unit
|
|
28
|
+
--
|
|
29
|
+
-- Replay-safe: additive columns, generated policy dropped and recreated.
|
|
30
|
+
-- ============================================================================
|
|
31
|
+
|
|
32
|
+
ALTER TABLE app.scaffold_registry ADD COLUMN IF NOT EXISTS distributable boolean NOT NULL DEFAULT false;
|
|
33
|
+
ALTER TABLE app.scaffold_registry ADD COLUMN IF NOT EXISTS dist_resource_type text;
|
|
34
|
+
ALTER TABLE app.scaffold_registry ADD COLUMN IF NOT EXISTS dist_key_column text;
|
|
35
|
+
ALTER TABLE app.scaffold_registry ADD COLUMN IF NOT EXISTS dist_unit_escape boolean;
|
|
36
|
+
COMMENT ON COLUMN app.scaffold_registry.distributable IS
|
|
37
|
+
'Whether this table carries the generated RESTRICTIVE release policy (#224). dist_resource_type/dist_key_column say WHICH resource the row is released with — a variant is released with its product.';
|
|
38
|
+
|
|
39
|
+
-- ── declare a table distributable, and generate its release policy ──────
|
|
40
|
+
CREATE OR REPLACE FUNCTION app.scaffold_distributable(
|
|
41
|
+
p_table regclass,
|
|
42
|
+
p_resource_type text DEFAULT NULL, -- defaults to the table's own resource_type
|
|
43
|
+
p_key_column text DEFAULT 'id',
|
|
44
|
+
p_unit_escape boolean DEFAULT true,
|
|
45
|
+
p_enabled boolean DEFAULT true
|
|
46
|
+
)
|
|
47
|
+
RETURNS void
|
|
48
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
|
|
49
|
+
AS $$
|
|
50
|
+
DECLARE
|
|
51
|
+
v_schema text; v_name text; v_q text; v_policy text;
|
|
52
|
+
r app.scaffold_registry%ROWTYPE; v_rt text;
|
|
53
|
+
BEGIN
|
|
54
|
+
SELECT n.nspname, c.relname INTO v_schema, v_name
|
|
55
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.oid = p_table;
|
|
56
|
+
v_q := format('%I.%I', v_schema, v_name);
|
|
57
|
+
v_policy := v_name || '_available_in';
|
|
58
|
+
|
|
59
|
+
SELECT * INTO r FROM app.scaffold_registry WHERE table_schema = v_schema AND table_name = v_name;
|
|
60
|
+
IF NOT FOUND THEN
|
|
61
|
+
RAISE EXCEPTION 'scaffold_distributable: %.% is not scaffolded', v_schema, v_name USING ERRCODE = 'P0002';
|
|
62
|
+
END IF;
|
|
63
|
+
v_rt := coalesce(p_resource_type, r.resource_type);
|
|
64
|
+
|
|
65
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON %s', v_policy, v_q);
|
|
66
|
+
|
|
67
|
+
IF p_enabled THEN
|
|
68
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
69
|
+
WHERE table_schema = v_schema AND table_name = v_name AND column_name = p_key_column) THEN
|
|
70
|
+
RAISE EXCEPTION 'scaffold_distributable: %.% has no column %', v_schema, v_name, p_key_column;
|
|
71
|
+
END IF;
|
|
72
|
+
-- RESTRICTIVE: permissive policies OR together, so a release rule that is
|
|
73
|
+
-- meant to REMOVE rows has to AND with the scaffold's select.
|
|
74
|
+
EXECUTE format(
|
|
75
|
+
'CREATE POLICY %I ON %s AS RESTRICTIVE FOR SELECT TO authenticated USING (%s(select app.available_in_my_units(%L, %I)))',
|
|
76
|
+
v_policy, v_q,
|
|
77
|
+
CASE WHEN p_unit_escape THEN 'unit_id IS NOT NULL OR ' ELSE '' END,
|
|
78
|
+
v_rt, p_key_column);
|
|
79
|
+
END IF;
|
|
80
|
+
|
|
81
|
+
UPDATE app.scaffold_registry
|
|
82
|
+
SET distributable = p_enabled,
|
|
83
|
+
dist_resource_type = CASE WHEN p_enabled THEN v_rt END,
|
|
84
|
+
dist_key_column = CASE WHEN p_enabled THEN p_key_column END,
|
|
85
|
+
dist_unit_escape = CASE WHEN p_enabled THEN p_unit_escape END
|
|
86
|
+
WHERE table_schema = v_schema AND table_name = v_name;
|
|
87
|
+
END $$;
|
|
88
|
+
REVOKE ALL ON FUNCTION app.scaffold_distributable(regclass, text, text, boolean, boolean) FROM public, anon, authenticated;
|
|
89
|
+
|
|
90
|
+
COMMENT ON FUNCTION app.scaffold_distributable(regclass, text, text, boolean, boolean) IS
|
|
91
|
+
'Generate (or remove) a table''s RESTRICTIVE release policy (#224). Whether a table supports multi-unit distribution becomes a declaration in the registry rather than a policy somebody remembered to write.';
|
|
92
|
+
|
|
93
|
+
-- ── convert the six hand-written catalog policies ───────────────────────
|
|
94
|
+
-- Same predicates, now generated. 075/077's CREATE POLICY statements stay in
|
|
95
|
+
-- their files (never edited); the scaffold drops and re-creates the same name,
|
|
96
|
+
-- so a replay converges on the generated one.
|
|
97
|
+
SELECT app.scaffold_distributable('public.products', 'catalog.product', 'id', true);
|
|
98
|
+
SELECT app.scaffold_distributable('public.services', 'catalog.product', 'id', true);
|
|
99
|
+
SELECT app.scaffold_distributable('public.service_packages', 'catalog.service_package', 'id', true);
|
|
100
|
+
SELECT app.scaffold_distributable('public.product_variants', 'catalog.product', 'product_id', false);
|
|
101
|
+
SELECT app.scaffold_distributable('public.price_table_items', 'catalog.product', 'product_id', false);
|
|
102
|
+
SELECT app.scaffold_distributable('public.service_package_items', 'catalog.service_package', 'package_id', false);
|
|
103
|
+
|
|
104
|
+
-- ── the case this issue was filed for ───────────────────────────────────
|
|
105
|
+
-- A provider who does not log in is a Person. With this, a Person registered at
|
|
106
|
+
-- no branch (unit_id NULL — the network's) can be released to the two branches
|
|
107
|
+
-- that use them, and a Person registered AT a branch keeps being that branch's
|
|
108
|
+
-- through has_unit. Both readings coexist, which is #225's point.
|
|
109
|
+
SELECT app.scaffold_distributable('public.people', 'people.person', 'id', true);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 140_masking_is_a_scaffold_option.sql — field masking joins scope and
|
|
3
|
+
-- distribution as something a table DECLARES (#222).
|
|
4
|
+
--
|
|
5
|
+
-- 124 generated `v_people` from the catalog, so the view was never hand-written
|
|
6
|
+
-- and a new column is picked up by regenerating. What was missing is the rest of
|
|
7
|
+
-- the scaffold contract: a declaration API shaped like the other two, and a gate
|
|
8
|
+
-- that notices when a table grows a column its view has not.
|
|
9
|
+
--
|
|
10
|
+
-- app.scaffold_scope(table, scope, enforce) #225
|
|
11
|
+
-- app.scaffold_distributable(table, rt, key, escape) #224
|
|
12
|
+
-- app.scaffold_masked(table, context, reference) here
|
|
13
|
+
--
|
|
14
|
+
-- The staleness gate is the point. A generated view is only trustworthy while
|
|
15
|
+
-- something checks that it was regenerated: add `cpf` to `people`, forget to
|
|
16
|
+
-- refresh, and the column is simply absent from the view — which reads as "no
|
|
17
|
+
-- such field" rather than as a mistake. N16d turns that into a red build.
|
|
18
|
+
--
|
|
19
|
+
-- WHAT THIS DELIBERATELY DOES NOT DO: revoke SELECT on the base tables.
|
|
20
|
+
--
|
|
21
|
+
-- That is what turns a masked view from available into mandatory, and it is a
|
|
22
|
+
-- fleet-wide change — ~92 SQL references to `public.people` (most inside
|
|
23
|
+
-- SECURITY DEFINER functions and unaffected), two security_invoker views, 22
|
|
24
|
+
-- client-side `.from('people')` call sites and 7 pgTAP files reading as personas.
|
|
25
|
+
-- It is also no longer required for the compliance goal: the boundary may be the
|
|
26
|
+
-- app's own server, decided per app (DECISIONS.md 2026-08-20). What must hold
|
|
27
|
+
-- either way is the condition, not the mechanism — no path from a browser to the
|
|
28
|
+
-- unmasked column — and that is an app-by-app fact this migration cannot assert.
|
|
29
|
+
--
|
|
30
|
+
-- Replay-safe: CREATE OR REPLACE + an idempotent re-declaration.
|
|
31
|
+
-- ============================================================================
|
|
32
|
+
|
|
33
|
+
CREATE OR REPLACE FUNCTION app.scaffold_masked(
|
|
34
|
+
p_table regclass,
|
|
35
|
+
p_context text,
|
|
36
|
+
p_reference text,
|
|
37
|
+
p_enabled boolean DEFAULT true
|
|
38
|
+
)
|
|
39
|
+
RETURNS void
|
|
40
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
|
|
41
|
+
AS $$
|
|
42
|
+
DECLARE v_schema text; v_name text;
|
|
43
|
+
BEGIN
|
|
44
|
+
SELECT n.nspname, c.relname INTO v_schema, v_name
|
|
45
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.oid = p_table;
|
|
46
|
+
|
|
47
|
+
IF p_enabled THEN
|
|
48
|
+
PERFORM app.masked_view_refresh(p_table, p_context, p_reference);
|
|
49
|
+
ELSE
|
|
50
|
+
EXECUTE format('DROP VIEW IF EXISTS %I.%I', v_schema, 'v_' || v_name);
|
|
51
|
+
DELETE FROM app.masked_views WHERE table_schema = v_schema AND table_name = v_name;
|
|
52
|
+
END IF;
|
|
53
|
+
END $$;
|
|
54
|
+
REVOKE ALL ON FUNCTION app.scaffold_masked(regclass, text, text, boolean) FROM public, anon, authenticated;
|
|
55
|
+
|
|
56
|
+
COMMENT ON FUNCTION app.scaffold_masked(regclass, text, text, boolean) IS
|
|
57
|
+
'Declare that a table reads through a field_rules-masked view, and generate it (#222). The third member of the scaffold''s declaration API, beside scaffold_scope and scaffold_distributable.';
|
|
58
|
+
|
|
59
|
+
-- ── which masked views no longer match their table ──────────────────────
|
|
60
|
+
-- Column SETS, not order: the generator emits ordinal order, and a table that
|
|
61
|
+
-- gained a column mid-list would otherwise read as merely reordered.
|
|
62
|
+
CREATE OR REPLACE FUNCTION app.masked_views_stale()
|
|
63
|
+
RETURNS TABLE (table_name text, missing text[])
|
|
64
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
65
|
+
SET search_path = ''
|
|
66
|
+
AS $$
|
|
67
|
+
SELECT m.table_name,
|
|
68
|
+
array_agg(c.column_name ORDER BY c.column_name)
|
|
69
|
+
FROM app.masked_views m
|
|
70
|
+
JOIN information_schema.columns c
|
|
71
|
+
ON c.table_schema = m.table_schema AND c.table_name = m.table_name
|
|
72
|
+
WHERE NOT EXISTS (
|
|
73
|
+
SELECT 1 FROM information_schema.columns v
|
|
74
|
+
WHERE v.table_schema = m.table_schema AND v.table_name = m.view_name
|
|
75
|
+
AND v.column_name = c.column_name)
|
|
76
|
+
GROUP BY m.table_name;
|
|
77
|
+
$$;
|
|
78
|
+
REVOKE ALL ON FUNCTION app.masked_views_stale() FROM public, anon;
|
|
79
|
+
GRANT EXECUTE ON FUNCTION app.masked_views_stale() TO authenticated, service_role;
|
|
80
|
+
|
|
81
|
+
COMMENT ON FUNCTION app.masked_views_stale() IS
|
|
82
|
+
'Masked views that have fallen behind their table (#222). A column absent from the view reads to a client as "no such field", not as a mistake — 900_verification/001 N16d makes it a red build instead.';
|
|
83
|
+
|
|
84
|
+
-- ── re-declare the pilot through the API ────────────────────────────────
|
|
85
|
+
SELECT app.scaffold_masked('public.people', 'person', 'person');
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 141_renamed_objects_keep_a_compat_name.sql — the old names answer again,
|
|
3
|
+
-- so the rename stops being a synchronised deploy.
|
|
4
|
+
--
|
|
5
|
+
-- 107_core_table_prefix and 137_vendor_named_spine_objects each renamed objects
|
|
6
|
+
-- and each wrote the same paragraph: NO COMPAT VIEW. Both gave one reason, and
|
|
7
|
+
-- it was the same reason:
|
|
8
|
+
--
|
|
9
|
+
-- 107 — "on the bench's force-replay pass the frozen files re-run and execute
|
|
10
|
+
-- ALTER TABLE … ENABLE ROW LEVEL SECURITY … every one of which errors
|
|
11
|
+
-- with 'this operation is not supported for views'."
|
|
12
|
+
-- 137 — "frozen migrations re-run ALTER TABLE and CREATE POLICY against the
|
|
13
|
+
-- old name on a replay, and those fail on a view."
|
|
14
|
+
--
|
|
15
|
+
-- THAT CONSTRAINT WAS THE BENCH, NEVER PRODUCTION. A pool applies each file once
|
|
16
|
+
-- and the ledger skips it forever after; only the force-replay pass re-executes
|
|
17
|
+
-- a frozen file, and replay-from-zero is no longer a gate this repository pays
|
|
18
|
+
-- for. With that dropped, the compat shape those files wanted is available, and
|
|
19
|
+
-- it is the difference between a rename anyone can ship and a rename that needs
|
|
20
|
+
-- every pool's edge functions redeployed in the same minute.
|
|
21
|
+
--
|
|
22
|
+
-- WHAT THE RENAME COSTS WITHOUT THIS FILE. `plg_connections` and `plg_sync_runs`
|
|
23
|
+
-- are read by literal name inside SIX functions that are deployed right now
|
|
24
|
+
-- (bling-sync v20 and shop-shipping-quote v3 on ecommerce, tyxter-template-sync
|
|
25
|
+
-- and tyxter-number-claim on beauty, kiwify-courses-sync on school, plus the
|
|
26
|
+
-- three vindi-* through _shared/vindi-shop.ts). An edge function is a deployed
|
|
27
|
+
-- artifact: publishing the npm packages does not update it. So the instant 107
|
|
28
|
+
-- lands, Bling sync, shipping quotes, Vindi checkout and the WhatsApp channel
|
|
29
|
+
-- answer with "relation does not exist" — and nothing in the SDK reports it.
|
|
30
|
+
--
|
|
31
|
+
-- WHY A VIEW AND NOT A PROJECTION TABLE. 063 needed tables because the legacy
|
|
32
|
+
-- tenancy names had to keep taking writes and carry FKs. Here a simple view over
|
|
33
|
+
-- one table is auto-updatable in Postgres, so INSERT/UPDATE/DELETE through the
|
|
34
|
+
-- old name reach the real table with no trigger; and nothing declares a foreign
|
|
35
|
+
-- key to these eight.
|
|
36
|
+
--
|
|
37
|
+
-- security_invoker = true, deliberately. A definer view would answer with the
|
|
38
|
+
-- owner's rights over a tenant_id table — exactly the leak 043 swept out of
|
|
39
|
+
-- v_documents. The caller's own RLS on the base table decides, which makes the
|
|
40
|
+
-- old name a pure alias and never a second, weaker door.
|
|
41
|
+
--
|
|
42
|
+
-- THIS FILE IS A BRIDGE, NOT A DESTINATION. Every caller moves to the real name
|
|
43
|
+
-- (scripts/check-schema-code-names.mjs fails the PR that adds a new one), and
|
|
44
|
+
-- when the last one has moved these views are dropped. Until then the exemption
|
|
45
|
+
-- list in that script names who is still crossing.
|
|
46
|
+
--
|
|
47
|
+
-- Idempotent and order-independent: each object is guarded on what is actually
|
|
48
|
+
-- in the catalog, so a pool that has not taken 107/137 yet is left completely
|
|
49
|
+
-- alone — the old name is still the real table there, and creating a view over
|
|
50
|
+
-- a name that is already a table would be wrong, not helpful.
|
|
51
|
+
-- ============================================================================
|
|
52
|
+
|
|
53
|
+
-- ── §1 the renamed tables ──────────────────────────────────────────────────
|
|
54
|
+
DO $$
|
|
55
|
+
DECLARE
|
|
56
|
+
pair text[];
|
|
57
|
+
v_old text;
|
|
58
|
+
v_new text;
|
|
59
|
+
g record;
|
|
60
|
+
v_cols text;
|
|
61
|
+
BEGIN
|
|
62
|
+
FOREACH pair SLICE 1 IN ARRAY ARRAY[
|
|
63
|
+
-- 107_core_table_prefix
|
|
64
|
+
['plg_analytics_read_models', 'analytics_read_models'],
|
|
65
|
+
['plg_connections', 'connections'],
|
|
66
|
+
['plg_connection_secrets', 'connection_secrets'],
|
|
67
|
+
['plg_connector_schedules', 'connector_schedules'],
|
|
68
|
+
['plg_entity_fields', 'entity_fields'],
|
|
69
|
+
['plg_event_log', 'event_log'],
|
|
70
|
+
['plg_sync_runs', 'sync_runs'],
|
|
71
|
+
['plg_sync_schedule', 'sync_schedule'],
|
|
72
|
+
-- 137_vendor_named_spine_objects
|
|
73
|
+
['fayz_projects', 'published_sites'],
|
|
74
|
+
['fayz_search_sources', 'search_sources']
|
|
75
|
+
] LOOP
|
|
76
|
+
v_old := pair[1];
|
|
77
|
+
v_new := pair[2];
|
|
78
|
+
|
|
79
|
+
-- The new name has to be there, or there is nothing to alias.
|
|
80
|
+
CONTINUE WHEN to_regclass('public.' || v_new) IS NULL;
|
|
81
|
+
-- The old name being a TABLE means this pool has not taken the rename: it is
|
|
82
|
+
-- the real thing and must stay that way. The old name being a VIEW means it
|
|
83
|
+
-- is an earlier run of THIS file — and that one may be wrong (the first cut
|
|
84
|
+
-- did `SELECT *`, which answers under the new column name). Skipping on
|
|
85
|
+
-- "exists" would make this file unable to correct itself, so a view is
|
|
86
|
+
-- dropped and rebuilt while a table is left alone.
|
|
87
|
+
CONTINUE WHEN EXISTS (
|
|
88
|
+
SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
89
|
+
WHERE n.nspname = 'public' AND c.relname = v_old AND c.relkind = 'r');
|
|
90
|
+
-- Something may read the alias by now (marketing's landing-page view reads
|
|
91
|
+
-- fayz_projects), so the dependents come off first and go back after — 143.
|
|
92
|
+
--
|
|
93
|
+
-- Guarded on the helper EXISTING, because 143 runs after this file: on a
|
|
94
|
+
-- fresh pool there is no alias yet and nothing to drop, so the helper is
|
|
95
|
+
-- never needed there. It is only ever needed on the re-application that
|
|
96
|
+
-- corrects an earlier alias, and by then 143 has long been applied.
|
|
97
|
+
IF to_regproc('app.detach_view_dependents') IS NOT NULL THEN
|
|
98
|
+
PERFORM app.detach_view_dependents('public.' || v_old);
|
|
99
|
+
END IF;
|
|
100
|
+
EXECUTE format('DROP VIEW IF EXISTS public.%I', v_old);
|
|
101
|
+
|
|
102
|
+
-- SELECT * IS NOT ENOUGH WHERE A COLUMN WAS RENAMED TOO. 137 renamed
|
|
103
|
+
-- fayz_projects.fayz_project_id to provider_site_id, so a `SELECT *` alias
|
|
104
|
+
-- answers under the old TABLE name with the new COLUMN name — and every
|
|
105
|
+
-- caller that names the column still breaks. plugin-marketing 008 found it.
|
|
106
|
+
-- A compat alias has to reproduce the old SHAPE, not just the old name.
|
|
107
|
+
SELECT string_agg(
|
|
108
|
+
CASE WHEN v_new = 'published_sites' AND a.attname = 'provider_site_id'
|
|
109
|
+
THEN 'provider_site_id AS fayz_project_id'
|
|
110
|
+
ELSE quote_ident(a.attname) END, ', ' ORDER BY a.attnum)
|
|
111
|
+
INTO v_cols
|
|
112
|
+
FROM pg_attribute a
|
|
113
|
+
WHERE a.attrelid = ('public.' || v_new)::regclass
|
|
114
|
+
AND a.attnum > 0 AND NOT a.attisdropped;
|
|
115
|
+
|
|
116
|
+
EXECUTE format(
|
|
117
|
+
'CREATE VIEW public.%I WITH (security_invoker = true) AS SELECT %s FROM public.%I',
|
|
118
|
+
v_old, v_cols, v_new);
|
|
119
|
+
|
|
120
|
+
-- The alias is exactly as reachable as what it points at — no more (a role
|
|
121
|
+
-- that cannot read the table cannot read it through the old name either)
|
|
122
|
+
-- and no less (the deployed callers keep the access they have today).
|
|
123
|
+
FOR g IN
|
|
124
|
+
SELECT DISTINCT grantee, privilege_type
|
|
125
|
+
FROM information_schema.role_table_grants
|
|
126
|
+
WHERE table_schema = 'public'
|
|
127
|
+
AND table_name = v_new
|
|
128
|
+
AND grantee IN ('anon', 'authenticated', 'service_role')
|
|
129
|
+
LOOP
|
|
130
|
+
EXECUTE format('GRANT %s ON public.%I TO %I', g.privilege_type, v_old, g.grantee);
|
|
131
|
+
END LOOP;
|
|
132
|
+
|
|
133
|
+
EXECUTE format(
|
|
134
|
+
'COMMENT ON VIEW public.%I IS %L',
|
|
135
|
+
v_old,
|
|
136
|
+
format('Compat alias for public.%s (141). Callers move to the real name; '
|
|
137
|
+
|| 'this view is dropped when the last one has.', v_new));
|
|
138
|
+
|
|
139
|
+
IF to_regproc('app.reattach_view_dependents') IS NOT NULL THEN
|
|
140
|
+
PERFORM app.reattach_view_dependents();
|
|
141
|
+
END IF;
|
|
142
|
+
|
|
143
|
+
RAISE NOTICE '141: public.% → compat view over public.%', v_old, v_new;
|
|
144
|
+
END LOOP;
|
|
145
|
+
END $$;
|
|
146
|
+
|
|
147
|
+
-- ── §2 fayz_global_search() ────────────────────────────────────────────────
|
|
148
|
+
-- 137 renamed the function and dropped the old name. The SDK's search engine
|
|
149
|
+
-- degrades rather than breaks (it reads PGRST202 and falls back to the fan-out
|
|
150
|
+
-- path), so this is a performance regression and not an outage — but the
|
|
151
|
+
-- fallback is the slow path on every query until the caller is repointed, and
|
|
152
|
+
-- restoring the name costs one wrapper.
|
|
153
|
+
DO $$
|
|
154
|
+
DECLARE
|
|
155
|
+
v_args text;
|
|
156
|
+
v_ret text;
|
|
157
|
+
BEGIN
|
|
158
|
+
-- By NAME, never by a spelled-out signature: the real one is
|
|
159
|
+
-- (text, uuid, text[], integer, integer) and a guard that hardcodes the
|
|
160
|
+
-- arguments silently misses, then CREATE FUNCTION collides with the alias it
|
|
161
|
+
-- was supposed to detect.
|
|
162
|
+
IF EXISTS (SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
163
|
+
WHERE n.nspname = 'public' AND p.proname = 'fayz_global_search') THEN
|
|
164
|
+
RETURN; -- already aliased
|
|
165
|
+
END IF;
|
|
166
|
+
|
|
167
|
+
SELECT pg_get_function_identity_arguments(p.oid), pg_get_function_result(p.oid)
|
|
168
|
+
INTO v_args, v_ret
|
|
169
|
+
FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
170
|
+
WHERE n.nspname = 'public' AND p.proname = 'global_search'
|
|
171
|
+
LIMIT 1;
|
|
172
|
+
|
|
173
|
+
IF v_args IS NULL THEN
|
|
174
|
+
RETURN; -- pool has not taken 137
|
|
175
|
+
END IF;
|
|
176
|
+
|
|
177
|
+
EXECUTE format($f$
|
|
178
|
+
CREATE OR REPLACE FUNCTION public.fayz_global_search(%s) RETURNS %s
|
|
179
|
+
LANGUAGE sql STABLE SECURITY INVOKER SET search_path = public, pg_temp
|
|
180
|
+
AS 'SELECT * FROM public.global_search(%s)'
|
|
181
|
+
$f$, v_args, v_ret,
|
|
182
|
+
(SELECT string_agg(split_part(trim(a), ' ', 1), ', ')
|
|
183
|
+
FROM unnest(string_to_array(v_args, ',')) AS a));
|
|
184
|
+
|
|
185
|
+
EXECUTE 'REVOKE ALL ON FUNCTION public.fayz_global_search(' || v_args || ') FROM public, anon';
|
|
186
|
+
EXECUTE 'GRANT EXECUTE ON FUNCTION public.fayz_global_search(' || v_args || ') TO authenticated, service_role';
|
|
187
|
+
|
|
188
|
+
COMMENT ON FUNCTION public.fayz_global_search IS
|
|
189
|
+
'Compat alias for public.global_search() (141). Dropped when the last caller has moved.';
|
|
190
|
+
|
|
191
|
+
RAISE NOTICE '141: public.fayz_global_search() → wrapper over public.global_search()';
|
|
192
|
+
END $$;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 142_a_person_has_addresses.sql — a person has addresses, not an address.
|
|
3
|
+
--
|
|
4
|
+
-- `people` carries four inline columns — address, city, state, postal_code —
|
|
5
|
+
-- while `public.addresses` has been the spine's address book all along and 113
|
|
6
|
+
-- already lists 'person' among the owners it accepts. So the shape was never in
|
|
7
|
+
-- question; the inline copy just outlived it.
|
|
8
|
+
--
|
|
9
|
+
-- ONE COLUMN CANNOT HOLD WHAT THE PRODUCT ALREADY ASKS FOR. A client has a home
|
|
10
|
+
-- and a delivery address; a professional has a home and the branch they bill
|
|
11
|
+
-- from; a supplier has a fiscal address and a warehouse. The UI has been
|
|
12
|
+
-- modelling that for a while, and every screen that does it has to keep the two
|
|
13
|
+
-- sides in step by hand because the database only remembers one.
|
|
14
|
+
--
|
|
15
|
+
-- WHAT THIS DOES. Every non-empty inline address becomes a row in `addresses`
|
|
16
|
+
-- owned by the person, marked `is_default` because it is the only one they had.
|
|
17
|
+
-- Idempotent by construction: a person who already owns an address is skipped,
|
|
18
|
+
-- so re-running adds nothing and a pool that was migrated by hand converges.
|
|
19
|
+
--
|
|
20
|
+
-- WHAT THIS DELIBERATELY DOES NOT DO. The columns stay. Dropping them is a
|
|
21
|
+
-- separate wave with the readers moved first — `products.stock` is the same
|
|
22
|
+
-- shape in this chain, kept as a deprecated mirror while the writers migrate.
|
|
23
|
+
-- They are COMMENTed as deprecated so nothing new reaches for them, and the
|
|
24
|
+
-- 900_verification posture suite is where a "no new readers" rule belongs once
|
|
25
|
+
-- the readers are counted.
|
|
26
|
+
--
|
|
27
|
+
-- The street is NOT parsed. `address` is one free-text line and `addresses`
|
|
28
|
+
-- wants street/number/complement/district; splitting "Rua X, 123 - apto 4" with
|
|
29
|
+
-- a regex invents structure the row never had, and a wrong number on a delivery
|
|
30
|
+
-- address costs more than an unsplit one. The line lands in `street` whole, and
|
|
31
|
+
-- whoever edits it next in the UI fills the parts.
|
|
32
|
+
-- ============================================================================
|
|
33
|
+
|
|
34
|
+
INSERT INTO public.addresses (
|
|
35
|
+
tenant_id, owner_type, owner_id, kind, label,
|
|
36
|
+
street, city, state, postal_code, country, is_default
|
|
37
|
+
)
|
|
38
|
+
SELECT
|
|
39
|
+
p.tenant_id,
|
|
40
|
+
'person',
|
|
41
|
+
p.id,
|
|
42
|
+
-- THE VOCABULARY IS 113'S, NOT A NEW ONE. addresses_kind_check allows
|
|
43
|
+
-- shipping|billing|both|main|fiscal, and 113 already chose 'main' for a unit's
|
|
44
|
+
-- principal address and 'fiscal' for a company's. A person's principal address
|
|
45
|
+
-- is the same idea, so it is 'main'. The word this file used before was a
|
|
46
|
+
-- sixth name for a concept that already had one, and the ecommerce pool —
|
|
47
|
+
-- which has no trigger quietly rewriting it — refused the row outright.
|
|
48
|
+
'main',
|
|
49
|
+
'Endereço principal',
|
|
50
|
+
NULLIF(btrim(COALESCE(p.address, '')), ''),
|
|
51
|
+
NULLIF(btrim(COALESCE(p.city, '')), ''),
|
|
52
|
+
NULLIF(btrim(COALESCE(p.state, '')), ''),
|
|
53
|
+
NULLIF(btrim(COALESCE(p.postal_code, '')), ''),
|
|
54
|
+
COALESCE(NULLIF(btrim(COALESCE(p.country, '')), ''), 'BR'),
|
|
55
|
+
true
|
|
56
|
+
FROM public.people p
|
|
57
|
+
-- COMPLETE ONES ONLY. `addresses` requires postal_code, street, city and state,
|
|
58
|
+
-- and 113 already settled why: "inventing a value to satisfy a constraint is how
|
|
59
|
+
-- a fiscal address quietly becomes wrong". resto carries people with a city and
|
|
60
|
+
-- nothing else — those are counted below and left where they are, readable in
|
|
61
|
+
-- the deprecated columns, rather than becoming an address with a made-up CEP.
|
|
62
|
+
WHERE NULLIF(btrim(COALESCE(p.address, '')), '') IS NOT NULL
|
|
63
|
+
AND NULLIF(btrim(COALESCE(p.city, '')), '') IS NOT NULL
|
|
64
|
+
AND NULLIF(btrim(COALESCE(p.state, '')), '') IS NOT NULL
|
|
65
|
+
AND NULLIF(btrim(COALESCE(p.postal_code, '')), '') IS NOT NULL
|
|
66
|
+
-- A person who already owns one has either been migrated or edited since;
|
|
67
|
+
-- either way the address book is ahead of the inline copy and wins.
|
|
68
|
+
AND NOT EXISTS (
|
|
69
|
+
SELECT 1 FROM public.addresses a
|
|
70
|
+
WHERE a.owner_type = 'person' AND a.owner_id = p.id
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
COMMENT ON COLUMN public.people.address IS
|
|
74
|
+
'DEPRECATED (142): a person has many addresses — read public.addresses WHERE owner_type = ''person''. Kept as a mirror until the writers move.';
|
|
75
|
+
COMMENT ON COLUMN public.people.city IS
|
|
76
|
+
'DEPRECATED (142): see public.addresses.';
|
|
77
|
+
COMMENT ON COLUMN public.people.state IS
|
|
78
|
+
'DEPRECATED (142): see public.addresses.';
|
|
79
|
+
COMMENT ON COLUMN public.people.postal_code IS
|
|
80
|
+
'DEPRECATED (142): see public.addresses.';
|
|
81
|
+
|
|
82
|
+
-- The read every screen wants: a person with their addresses, newest default
|
|
83
|
+
-- first, without each one re-deriving the join and the ordering.
|
|
84
|
+
CREATE OR REPLACE VIEW public.v_person_addresses
|
|
85
|
+
WITH (security_invoker = true) AS
|
|
86
|
+
SELECT
|
|
87
|
+
a.id, a.tenant_id, a.owner_id AS person_id, p.name AS person_name,
|
|
88
|
+
a.kind, a.label, a.recipient, a.phone,
|
|
89
|
+
a.postal_code, a.street, a.number, a.complement, a.district,
|
|
90
|
+
a.city, a.state, a.country, a.is_default,
|
|
91
|
+
a.created_at, a.updated_at
|
|
92
|
+
FROM public.addresses a
|
|
93
|
+
JOIN public.people p ON p.id = a.owner_id
|
|
94
|
+
WHERE a.owner_type = 'person';
|
|
95
|
+
|
|
96
|
+
GRANT SELECT ON public.v_person_addresses TO authenticated;
|
|
97
|
+
|
|
98
|
+
COMMENT ON VIEW public.v_person_addresses IS
|
|
99
|
+
'A person and every address they own (142). The default one sorts first via is_default.';
|
|
100
|
+
|
|
101
|
+
-- ── the same, for the Empresa ───────────────────────────────────────────────
|
|
102
|
+
-- 113 already named this: "company_legal_entities (#138) inlines seven address
|
|
103
|
+
-- columns", and it already added 'company_legal_entity' to the owners
|
|
104
|
+
-- `addresses` accepts. What it did not do was move the rows — it declared the
|
|
105
|
+
-- address book the read model and left the write path for later. This is later.
|
|
106
|
+
--
|
|
107
|
+
-- A company genuinely has more than one: the fiscal address on the nota is not
|
|
108
|
+
-- always where the goods ship from, and a change of accountant moves one without
|
|
109
|
+
-- touching the other.
|
|
110
|
+
--
|
|
111
|
+
-- Unlike a person's, this address is ALREADY structured — number, complement and
|
|
112
|
+
-- district are their own columns — so each part lands in its own field and
|
|
113
|
+
-- nothing is guessed.
|
|
114
|
+
INSERT INTO public.addresses (
|
|
115
|
+
tenant_id, owner_type, owner_id, kind, label,
|
|
116
|
+
street, number, complement, district, city, state, postal_code, country,
|
|
117
|
+
phone, is_default
|
|
118
|
+
)
|
|
119
|
+
SELECT
|
|
120
|
+
c.tenant_id,
|
|
121
|
+
'company_legal_entity',
|
|
122
|
+
c.id,
|
|
123
|
+
'fiscal',
|
|
124
|
+
'Endereço fiscal',
|
|
125
|
+
NULLIF(btrim(COALESCE(c.address, '')), ''),
|
|
126
|
+
NULLIF(btrim(COALESCE(c.address_number, '')), ''),
|
|
127
|
+
NULLIF(btrim(COALESCE(c.address_complement, '')), ''),
|
|
128
|
+
NULLIF(btrim(COALESCE(c.district, '')), ''),
|
|
129
|
+
NULLIF(btrim(COALESCE(c.city, '')), ''),
|
|
130
|
+
NULLIF(btrim(COALESCE(c.state, '')), ''),
|
|
131
|
+
NULLIF(btrim(COALESCE(c.postal_code, '')), ''),
|
|
132
|
+
COALESCE(NULLIF(btrim(COALESCE(c.country, '')), ''), 'BR'),
|
|
133
|
+
NULLIF(btrim(COALESCE(c.phone, '')), ''),
|
|
134
|
+
true
|
|
135
|
+
FROM public.company_legal_entities c
|
|
136
|
+
WHERE NULLIF(btrim(COALESCE(c.address, '')), '') IS NOT NULL
|
|
137
|
+
AND NULLIF(btrim(COALESCE(c.city, '')), '') IS NOT NULL
|
|
138
|
+
AND NULLIF(btrim(COALESCE(c.state, '')), '') IS NOT NULL
|
|
139
|
+
AND NULLIF(btrim(COALESCE(c.postal_code, '')), '') IS NOT NULL
|
|
140
|
+
AND NOT EXISTS (
|
|
141
|
+
SELECT 1 FROM public.addresses a
|
|
142
|
+
WHERE a.owner_type = 'company_legal_entity' AND a.owner_id = c.id
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
COMMENT ON COLUMN public.company_legal_entities.address IS
|
|
146
|
+
'DEPRECATED (142): a company has many addresses — read public.addresses WHERE owner_type = ''company_legal_entity''. Kept as a mirror until the writers move.';
|
|
147
|
+
COMMENT ON COLUMN public.company_legal_entities.address_number IS
|
|
148
|
+
'DEPRECATED (142): see public.addresses.number.';
|
|
149
|
+
COMMENT ON COLUMN public.company_legal_entities.address_complement IS
|
|
150
|
+
'DEPRECATED (142): see public.addresses.complement.';
|
|
151
|
+
COMMENT ON COLUMN public.company_legal_entities.district IS
|
|
152
|
+
'DEPRECATED (142): see public.addresses.district.';
|
|
153
|
+
COMMENT ON COLUMN public.company_legal_entities.city IS
|
|
154
|
+
'DEPRECATED (142): see public.addresses.';
|
|
155
|
+
COMMENT ON COLUMN public.company_legal_entities.state IS
|
|
156
|
+
'DEPRECATED (142): see public.addresses.';
|
|
157
|
+
COMMENT ON COLUMN public.company_legal_entities.postal_code IS
|
|
158
|
+
'DEPRECATED (142): see public.addresses.';
|
|
159
|
+
|
|
160
|
+
-- A unit bills through a company (app.units.company_legal_entity_id), so the
|
|
161
|
+
-- branch's fiscal address is the company's — this is the join every fiscal
|
|
162
|
+
-- screen needs and none of them should re-derive.
|
|
163
|
+
CREATE OR REPLACE VIEW public.v_company_addresses
|
|
164
|
+
WITH (security_invoker = true) AS
|
|
165
|
+
SELECT
|
|
166
|
+
a.id, a.tenant_id, a.owner_id AS company_legal_entity_id,
|
|
167
|
+
c.legal_name, c.trade_name, c.document_number,
|
|
168
|
+
a.kind, a.label,
|
|
169
|
+
a.postal_code, a.street, a.number, a.complement, a.district,
|
|
170
|
+
a.city, a.state, a.country, a.phone, a.is_default,
|
|
171
|
+
a.created_at, a.updated_at
|
|
172
|
+
FROM public.addresses a
|
|
173
|
+
JOIN public.company_legal_entities c ON c.id = a.owner_id
|
|
174
|
+
WHERE a.owner_type = 'company_legal_entity';
|
|
175
|
+
|
|
176
|
+
GRANT SELECT ON public.v_company_addresses TO authenticated;
|
|
177
|
+
|
|
178
|
+
COMMENT ON VIEW public.v_company_addresses IS
|
|
179
|
+
'An Empresa and every address it owns (142), with the CNPJ that identifies it.';
|
|
180
|
+
|
|
181
|
+
-- What did not come across, said out loud. A partial address is not a failure to
|
|
182
|
+
-- fix here — it is a person or a company whose address was never fully typed in,
|
|
183
|
+
-- and the place to finish it is the screen, not a migration.
|
|
184
|
+
DO $$
|
|
185
|
+
DECLARE v_people integer; v_co integer;
|
|
186
|
+
BEGIN
|
|
187
|
+
SELECT count(*) INTO v_people FROM public.people p
|
|
188
|
+
WHERE (COALESCE(btrim(p.address),'') <> '' OR COALESCE(btrim(p.city),'') <> ''
|
|
189
|
+
OR COALESCE(btrim(p.state),'') <> '' OR COALESCE(btrim(p.postal_code),'') <> '')
|
|
190
|
+
AND NOT EXISTS (SELECT 1 FROM public.addresses a
|
|
191
|
+
WHERE a.owner_type = 'person' AND a.owner_id = p.id);
|
|
192
|
+
SELECT count(*) INTO v_co FROM public.company_legal_entities c
|
|
193
|
+
WHERE (COALESCE(btrim(c.address),'') <> '' OR COALESCE(btrim(c.city),'') <> '')
|
|
194
|
+
AND NOT EXISTS (SELECT 1 FROM public.addresses a
|
|
195
|
+
WHERE a.owner_type = 'company_legal_entity' AND a.owner_id = c.id);
|
|
196
|
+
IF v_people > 0 OR v_co > 0 THEN
|
|
197
|
+
RAISE NOTICE '142: % person(s) and % company(ies) hold a PARTIAL address — left in the deprecated columns, not invented into public.addresses', v_people, v_co;
|
|
198
|
+
END IF;
|
|
199
|
+
END $$;
|
|
200
|
+
|
|
201
|
+
-- Pools that took an earlier version of this file. It asked for a kind the
|
|
202
|
+
-- CHECK does not list, and a pool with a normalising trigger stored 'both'
|
|
203
|
+
-- instead — a person's one address filed as "shipping and billing at once",
|
|
204
|
+
-- which is not what anyone meant. The label is this migration's own signature,
|
|
205
|
+
-- so only its own rows move.
|
|
206
|
+
UPDATE public.addresses
|
|
207
|
+
SET kind = 'main'
|
|
208
|
+
WHERE owner_type = 'person' AND label = 'Endereço principal' AND kind <> 'main';
|
|
209
|
+
|
|
210
|
+
UPDATE public.addresses
|
|
211
|
+
SET kind = 'fiscal'
|
|
212
|
+
WHERE owner_type = 'company_legal_entity' AND label = 'Endereço fiscal' AND kind <> 'fiscal';
|