- Pin Dockerfile to Go 1.23 to match go.mod - Record README front-door audit findings
434 lines
20 KiB
PL/PgSQL
434 lines
20 KiB
PL/PgSQL
-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
|
-- SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
|
|
|
-- +goose Up
|
|
|
|
-- Doc 41 (product-kind taxonomy dissolution + grant-issuance uniformity),
|
|
-- §9 steps 1-7. Steps 8-10 live in 00005 (conferral functions + enclosure)
|
|
-- and 00006 (position sweep + catalog rename). Data normalization runs
|
|
-- strictly before constraint installation; any row the mechanical bucket
|
|
-- rules cannot classify halts the migration with the offending ids rather
|
|
-- than guessing (halt-and-report). Rollback of the data steps is
|
|
-- snapshot-based; the Down section reverses schema mechanically but does
|
|
-- not resurrect dropped data.
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Step 1 — prices: consumption model is a per-price fact (Decision 135).
|
|
-- This schema predates usage pricing, so usage_type is added here rather
|
|
-- than normalized. A price billed per-use or per-seat only makes sense
|
|
-- against a recurring period; on-demand rate rows carry usage_type NULL and
|
|
-- are discriminated by their own mechanism, never by this flag.
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
ALTER TABLE core.prices ADD COLUMN usage_type VARCHAR(20);
|
|
|
|
ALTER TABLE core.prices ADD CONSTRAINT chk_prices_usage_type_domain
|
|
CHECK (usage_type IS NULL OR usage_type IN ('metered', 'licensed'));
|
|
|
|
ALTER TABLE core.prices ADD CONSTRAINT chk_prices_usage_pricing_requires_recurrence
|
|
CHECK (usage_type IS NULL OR recurring_interval IS NOT NULL);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Step 2 — provision bounds. quantity is already NOT NULL DEFAULT 1 in this
|
|
-- schema; only the positivity rule is missing.
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
ALTER TABLE core.pool_provisions ADD CONSTRAINT chk_pool_provisions_quantity_positive
|
|
CHECK (quantity > 0);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Steps 3+4 — grants: data normalization. Ordering within this block
|
|
-- matters and deliberately interleaves §9's steps 3 and 4: the set-direct
|
|
-- repoint (step 4 data) must run before the reason buckets and CHECKs
|
|
-- (step 3 constraints), because the bare-set grant is system-authored and
|
|
-- its 'default' reason is only trustworthy once it points at a rank-0 tier
|
|
-- product.
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- 3a. 'trial' renames to 'evaluation' preserving semantic identity; expiry
|
|
-- stays on valid_until (Decision 139; 'trial' is a subscription/price word).
|
|
UPDATE core.grants SET grant_reason = 'evaluation' WHERE grant_reason = 'trial';
|
|
|
|
-- 4-data. Set-direct grants (product_id IS NULL): repoint to the
|
|
-- lowest-ranked tier product of the single ladder whose tier products share
|
|
-- the grant's entitlement set, and give live provisions an honest
|
|
-- retroactive ladder attachment (the pool genuinely occupied that position
|
|
-- all along). Zero or multiple candidate ladders → halt.
|
|
-- +goose StatementBegin
|
|
DO $$
|
|
DECLARE
|
|
g RECORD;
|
|
v_ladder_id UUID;
|
|
v_ladder_count INTEGER;
|
|
v_product_id UUID;
|
|
v_rank INTEGER;
|
|
prov RECORD;
|
|
v_conflict UUID;
|
|
BEGIN
|
|
FOR g IN SELECT grant_id, entitlement_set_id FROM core.grants WHERE product_id IS NULL LOOP
|
|
IF g.entitlement_set_id IS NULL THEN
|
|
RAISE EXCEPTION 'doc41 §9 step 4: grant % has neither product_id nor entitlement_set_id', g.grant_id;
|
|
END IF;
|
|
|
|
SELECT COUNT(DISTINCT t.plan_ladder_id) INTO v_ladder_count
|
|
FROM core.plan_ladder_tiers t
|
|
JOIN core.products p ON p.product_id = t.product_id
|
|
WHERE p.entitlement_set_id = g.entitlement_set_id;
|
|
|
|
IF v_ladder_count <> 1 THEN
|
|
RAISE EXCEPTION 'doc41 §9 step 4: set-direct grant % resolves to % ladders via its entitlement set; manual resolution required', g.grant_id, v_ladder_count;
|
|
END IF;
|
|
|
|
SELECT t.plan_ladder_id, t.product_id, t.rank INTO v_ladder_id, v_product_id, v_rank
|
|
FROM core.plan_ladder_tiers t
|
|
JOIN core.products p ON p.product_id = t.product_id
|
|
WHERE p.entitlement_set_id = g.entitlement_set_id
|
|
ORDER BY t.rank ASC
|
|
LIMIT 1;
|
|
|
|
UPDATE core.grants
|
|
SET product_id = v_product_id,
|
|
description = COALESCE(description || E'\n', '') ||
|
|
'[doc41 migration: set-direct grant repointed to tier product ' || v_product_id || ']'
|
|
WHERE grant_id = g.grant_id;
|
|
|
|
FOR prov IN
|
|
SELECT provision_id, pool_id, status, activated_at, ended_at
|
|
FROM core.pool_provisions
|
|
WHERE grant_id = g.grant_id AND status <> 'ended'
|
|
LOOP
|
|
-- Halt rather than collide with the GiST exclusion: an existing
|
|
-- active attachment on this (pool, ladder) means the pool held
|
|
-- two positions at once and needs a human decision.
|
|
SELECT l.provision_id INTO v_conflict
|
|
FROM core.pool_provision_ladders l
|
|
WHERE l.pool_id = prov.pool_id
|
|
AND l.plan_ladder_id = v_ladder_id
|
|
AND l.status = 'active'
|
|
LIMIT 1;
|
|
IF v_conflict IS NOT NULL THEN
|
|
RAISE EXCEPTION 'doc41 §9 step 4: retroactive attachment for grant % (provision %) conflicts with active attachment of provision % on ladder %; manual resolution required',
|
|
g.grant_id, prov.provision_id, v_conflict, v_ladder_id;
|
|
END IF;
|
|
|
|
INSERT INTO core.pool_provision_ladders
|
|
(provision_id, plan_ladder_id, pool_id, status, activated_at, ended_at, product_id)
|
|
VALUES
|
|
(prov.provision_id, v_ladder_id, prov.pool_id, prov.status, prov.activated_at, prov.ended_at, v_product_id);
|
|
|
|
INSERT INTO core.pool_provision_transitions
|
|
(pool_id, provision_id, plan_ladder_id, from_rank, to_rank, transition_type, actor_type, actor_id, reason, effective_at)
|
|
VALUES
|
|
(prov.pool_id, prov.provision_id, v_ladder_id, NULL, v_rank, 'initiate', 'system', NULL,
|
|
'doc41 §9 migration: retroactive position registration for set-direct grant', prov.activated_at);
|
|
END LOOP;
|
|
END LOOP;
|
|
END;
|
|
$$;
|
|
-- +goose StatementEnd
|
|
|
|
-- 3b. Operator-authored grants claiming 'default' are reclassified as
|
|
-- 'manual' with the claim preserved (finding #48 population).
|
|
UPDATE core.grants
|
|
SET description = COALESCE(description || E'\n', '') ||
|
|
'[doc41 migration: grant_reason was ''default'' but grant is operator-authored]',
|
|
grant_reason = 'manual'
|
|
WHERE grant_reason = 'default' AND granted_by_person_id IS NOT NULL;
|
|
|
|
-- 3c. Operator-authored grants with out-of-domain reasons (free text)
|
|
-- reclassify as 'manual', original preserved for the record.
|
|
UPDATE core.grants
|
|
SET description = COALESCE(description || E'\n', '') ||
|
|
'[doc41 migration: original grant_reason ''' || grant_reason || ''']',
|
|
grant_reason = 'manual'
|
|
WHERE grant_reason NOT IN ('manual', 'default', 'evaluation', 'promotional', 'complimentary', 'sponsored', 'board_decision', 'legacy')
|
|
AND granted_by_person_id IS NOT NULL;
|
|
|
|
-- 3d. Assertions before installing CHECKs: anything left over is a row the
|
|
-- mechanical rules cannot classify.
|
|
-- +goose StatementBegin
|
|
DO $$
|
|
DECLARE
|
|
v_ids TEXT;
|
|
BEGIN
|
|
-- System-authored rows must carry 'default' (the biconditional's other
|
|
-- half). A leftover here is a system-authored grant with a free-text or
|
|
-- non-default reason — unclassifiable mechanically.
|
|
SELECT string_agg(grant_id::text, ', ') INTO v_ids
|
|
FROM core.grants
|
|
WHERE granted_by_person_id IS NULL AND grant_reason <> 'default';
|
|
IF v_ids IS NOT NULL THEN
|
|
RAISE EXCEPTION 'doc41 §9 step 3: system-authored grants with non-default reason need manual resolution: %', v_ids;
|
|
END IF;
|
|
|
|
-- A 'default' grant is trusted as a genuine system default only if its
|
|
-- product sits at the lowest rank of some ladder (the shape the
|
|
-- automatic pipeline always produces).
|
|
SELECT string_agg(g.grant_id::text, ', ') INTO v_ids
|
|
FROM core.grants g
|
|
WHERE g.grant_reason = 'default'
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM core.plan_ladder_tiers t
|
|
WHERE t.product_id = g.product_id
|
|
AND t.rank = (SELECT MIN(t2.rank) FROM core.plan_ladder_tiers t2 WHERE t2.plan_ladder_id = t.plan_ladder_id)
|
|
);
|
|
IF v_ids IS NOT NULL THEN
|
|
RAISE EXCEPTION 'doc41 §9 step 3: ''default'' grants whose product is not a rank-0 tier need manual resolution: %', v_ids;
|
|
END IF;
|
|
END;
|
|
$$;
|
|
-- +goose StatementEnd
|
|
|
|
-- 3-constraints. grant_reason is already NOT NULL (load-bearing: both new
|
|
-- CHECKs are NULL-permeable under three-valued logic without it).
|
|
ALTER TABLE core.grants DROP CONSTRAINT chk_grants_system_actor;
|
|
|
|
ALTER TABLE core.grants ADD CONSTRAINT chk_grants_reason_domain
|
|
CHECK (grant_reason IN ('manual', 'default', 'evaluation', 'promotional', 'complimentary', 'sponsored', 'board_decision', 'legacy'));
|
|
|
|
ALTER TABLE core.grants ADD CONSTRAINT chk_grants_default_iff_system_authored
|
|
CHECK ((grant_reason = 'default') = (granted_by_person_id IS NULL));
|
|
|
|
-- 4-constraints. Every grant names a real product; the set-direct escape
|
|
-- closes structurally (Decision 137, partial reversal of Decision 112).
|
|
ALTER TABLE core.grants ALTER COLUMN product_id SET NOT NULL;
|
|
ALTER TABLE core.grants DROP CONSTRAINT chk_grants_entitlement_source;
|
|
ALTER TABLE core.grants DROP CONSTRAINT fk_grants_entitlement_set_id;
|
|
ALTER TABLE core.grants DROP COLUMN entitlement_set_id;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Step 5 — provisions carry product identity. Backfill traces each
|
|
-- provision to its source arc; ambiguity halts.
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
ALTER TABLE core.pool_provisions ADD COLUMN product_id UUID;
|
|
|
|
ALTER TABLE core.pool_provisions ADD CONSTRAINT fk_pool_provisions_product_id
|
|
FOREIGN KEY (product_id) REFERENCES core.products(product_id);
|
|
|
|
-- Grant-sourced: the grant's product.
|
|
UPDATE core.pool_provisions pp
|
|
SET product_id = g.product_id
|
|
FROM core.grants g
|
|
WHERE pp.grant_id = g.grant_id AND pp.product_id IS NULL;
|
|
|
|
-- Subscription-sourced: via subscription items' prices, matched on the
|
|
-- provision's entitlement set; only an unambiguous (single-product) match
|
|
-- is written.
|
|
UPDATE core.pool_provisions pp
|
|
SET product_id = m.product_id
|
|
FROM (
|
|
SELECT si.subscription_id, prod.entitlement_set_id, MIN(prod.product_id::text)::uuid AS product_id
|
|
FROM core.subscription_items si
|
|
JOIN core.prices pr ON pr.price_id = si.price_id
|
|
JOIN core.products prod ON prod.product_id = pr.product_id
|
|
GROUP BY si.subscription_id, prod.entitlement_set_id
|
|
HAVING COUNT(DISTINCT prod.product_id) = 1
|
|
) m
|
|
WHERE pp.subscription_id = m.subscription_id
|
|
AND pp.entitlement_set_id = m.entitlement_set_id
|
|
AND pp.product_id IS NULL;
|
|
|
|
-- Purchase-sourced provisions cannot exist (purchase_id is a loose slot; no
|
|
-- purchases table). Anything still NULL is ambiguous — halt-and-report.
|
|
-- +goose StatementBegin
|
|
DO $$
|
|
DECLARE
|
|
v_ids TEXT;
|
|
BEGIN
|
|
SELECT string_agg(provision_id::text, ', ') INTO v_ids
|
|
FROM core.pool_provisions WHERE product_id IS NULL;
|
|
IF v_ids IS NOT NULL THEN
|
|
RAISE EXCEPTION 'doc41 §9 step 5: provisions with ambiguous product identity need manual resolution: %', v_ids;
|
|
END IF;
|
|
|
|
-- Pre-check the per-source live-uniqueness the indexes below will
|
|
-- enforce, so the failure names rows instead of an index error.
|
|
SELECT string_agg(src, '; ') INTO v_ids FROM (
|
|
SELECT 'grant ' || grant_id::text AS src
|
|
FROM core.pool_provisions
|
|
WHERE grant_id IS NOT NULL AND status <> 'ended'
|
|
GROUP BY grant_id HAVING COUNT(*) > 1
|
|
UNION ALL
|
|
SELECT 'subscription ' || subscription_id::text || ' product ' || product_id::text
|
|
FROM core.pool_provisions
|
|
WHERE subscription_id IS NOT NULL AND status <> 'ended'
|
|
GROUP BY subscription_id, product_id HAVING COUNT(*) > 1
|
|
) dup;
|
|
IF v_ids IS NOT NULL THEN
|
|
RAISE EXCEPTION 'doc41 §9 step 5: sources with multiple live provisions need manual resolution: %', v_ids;
|
|
END IF;
|
|
END;
|
|
$$;
|
|
-- +goose StatementEnd
|
|
|
|
ALTER TABLE core.pool_provisions ALTER COLUMN product_id SET NOT NULL;
|
|
|
|
-- At most one live provision per source; status-partial so ended sources
|
|
-- can be honestly re-provisioned (reapply_defaults) and retries are
|
|
-- idempotent at the index (Invariant 6).
|
|
CREATE UNIQUE INDEX uq_pool_provisions_live_grant
|
|
ON core.pool_provisions (grant_id)
|
|
WHERE grant_id IS NOT NULL AND status <> 'ended';
|
|
|
|
CREATE UNIQUE INDEX uq_pool_provisions_live_purchase
|
|
ON core.pool_provisions (purchase_id)
|
|
WHERE purchase_id IS NOT NULL AND status <> 'ended';
|
|
|
|
CREATE UNIQUE INDEX uq_pool_provisions_live_subscription_product
|
|
ON core.pool_provisions (subscription_id, product_id)
|
|
WHERE subscription_id IS NOT NULL AND status <> 'ended';
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Step 6 — occupancy episodes get permanent identity (Decision 138). The
|
|
-- (provision, ladder) pair stays unique only among live rows, so a rung
|
|
-- lost and later regained is a fresh episode, never a reopened row.
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
ALTER TABLE core.pool_provision_ladders ADD COLUMN provision_ladder_id UUID NOT NULL DEFAULT uuidv7();
|
|
ALTER TABLE core.pool_provision_ladders DROP CONSTRAINT pk_pool_provision_ladders;
|
|
ALTER TABLE core.pool_provision_ladders ADD CONSTRAINT pk_pool_provision_ladders PRIMARY KEY (provision_ladder_id);
|
|
|
|
CREATE UNIQUE INDEX uq_pool_provision_ladders_live
|
|
ON core.pool_provision_ladders (provision_id, plan_ladder_id)
|
|
WHERE status <> 'ended';
|
|
|
|
-- Audit rows name the specific occupancy episode. Backfill is exact: the
|
|
-- old composite PK guaranteed at most one episode per pair, so the join is
|
|
-- unambiguous. Rows predating provision attribution (provision_id IS NULL)
|
|
-- stay NULL; the three constraints below are NOT VALID — they govern new
|
|
-- writes and do not rewrite append-only history.
|
|
ALTER TABLE core.pool_provision_transitions ADD COLUMN provision_ladder_id UUID;
|
|
|
|
UPDATE core.pool_provision_transitions tr
|
|
SET provision_ladder_id = l.provision_ladder_id
|
|
FROM core.pool_provision_ladders l
|
|
WHERE tr.provision_id = l.provision_id
|
|
AND tr.plan_ladder_id = l.plan_ladder_id
|
|
AND tr.provision_id IS NOT NULL;
|
|
|
|
ALTER TABLE core.pool_provision_transitions
|
|
ADD CONSTRAINT fk_pool_provision_transitions_occupancy
|
|
FOREIGN KEY (provision_ladder_id) REFERENCES core.pool_provision_ladders(provision_ladder_id) NOT VALID;
|
|
|
|
ALTER TABLE core.pool_provision_transitions DROP CONSTRAINT chk_pool_provision_transitions_type;
|
|
ALTER TABLE core.pool_provision_transitions
|
|
ADD CONSTRAINT chk_pool_provision_transitions_type
|
|
CHECK (transition_type IN ('initiate', 'upgrade', 'downgrade', 'end', 'transfer')) NOT VALID;
|
|
|
|
ALTER TABLE core.pool_provision_transitions
|
|
ADD CONSTRAINT chk_pool_provision_transitions_occupancy_named
|
|
CHECK (provision_ladder_id IS NOT NULL) NOT VALID;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Step 7 — sync trigger gains the monotonic-end guard; the conferral-shape
|
|
-- view is created. An individually-ended junction row is never reopened by
|
|
-- parent-provision updates: regaining a rung is a fresh episode.
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- +goose StatementBegin
|
|
CREATE OR REPLACE FUNCTION core.sync_pool_provision_ladders()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF NEW.pool_id IS DISTINCT FROM OLD.pool_id
|
|
OR NEW.status IS DISTINCT FROM OLD.status
|
|
OR NEW.activated_at IS DISTINCT FROM OLD.activated_at
|
|
OR NEW.ended_at IS DISTINCT FROM OLD.ended_at THEN
|
|
UPDATE core.pool_provision_ladders
|
|
SET pool_id = NEW.pool_id,
|
|
status = NEW.status,
|
|
activated_at = NEW.activated_at,
|
|
ended_at = NEW.ended_at
|
|
WHERE provision_id = NEW.provision_id
|
|
-- Monotonic-end guard (Doc 41 §6.2): a deliberately-ended episode
|
|
-- stays ended; it never tracks the parent back to life.
|
|
AND status <> 'ended';
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
-- +goose StatementEnd
|
|
|
|
-- The SOLE input the conferral primitive reads (Decision 134): entitlement
|
|
-- set, lifecycle, visibility, and per-ladder (id, rank) with a ladder count.
|
|
-- Off-ladder products appear as one row with NULL ladder columns and
|
|
-- ladder_count 0.
|
|
CREATE VIEW core.product_conferral_shapes AS
|
|
SELECT
|
|
p.product_id,
|
|
p.entitlement_set_id,
|
|
p.lifecycle_status,
|
|
p.is_public,
|
|
t.plan_ladder_id,
|
|
t.rank,
|
|
COUNT(t.plan_ladder_id) OVER (PARTITION BY p.product_id) AS ladder_count
|
|
FROM core.products p
|
|
LEFT JOIN core.plan_ladder_tiers t ON t.product_id = p.product_id;
|
|
|
|
-- The init migration's GRANT ON ALL TABLES was point-in-time; new relations
|
|
-- need explicit grants.
|
|
GRANT SELECT ON core.product_conferral_shapes TO core_reader, core_writer, core_owner;
|
|
|
|
-- +goose Down
|
|
|
|
DROP VIEW core.product_conferral_shapes;
|
|
|
|
-- +goose StatementBegin
|
|
CREATE OR REPLACE FUNCTION core.sync_pool_provision_ladders()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF NEW.pool_id IS DISTINCT FROM OLD.pool_id
|
|
OR NEW.status IS DISTINCT FROM OLD.status
|
|
OR NEW.activated_at IS DISTINCT FROM OLD.activated_at
|
|
OR NEW.ended_at IS DISTINCT FROM OLD.ended_at THEN
|
|
UPDATE core.pool_provision_ladders
|
|
SET pool_id = NEW.pool_id,
|
|
status = NEW.status,
|
|
activated_at = NEW.activated_at,
|
|
ended_at = NEW.ended_at
|
|
WHERE provision_id = NEW.provision_id;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
-- +goose StatementEnd
|
|
|
|
ALTER TABLE core.pool_provision_transitions DROP CONSTRAINT chk_pool_provision_transitions_occupancy_named;
|
|
ALTER TABLE core.pool_provision_transitions DROP CONSTRAINT chk_pool_provision_transitions_type;
|
|
ALTER TABLE core.pool_provision_transitions
|
|
ADD CONSTRAINT chk_pool_provision_transitions_type
|
|
CHECK (transition_type IN ('initiate', 'upgrade', 'downgrade', 'end', 'extend')) NOT VALID;
|
|
ALTER TABLE core.pool_provision_transitions DROP CONSTRAINT fk_pool_provision_transitions_occupancy;
|
|
ALTER TABLE core.pool_provision_transitions DROP COLUMN provision_ladder_id;
|
|
|
|
DROP INDEX core.uq_pool_provision_ladders_live;
|
|
ALTER TABLE core.pool_provision_ladders DROP CONSTRAINT pk_pool_provision_ladders;
|
|
ALTER TABLE core.pool_provision_ladders ADD CONSTRAINT pk_pool_provision_ladders PRIMARY KEY (provision_id, plan_ladder_id);
|
|
ALTER TABLE core.pool_provision_ladders DROP COLUMN provision_ladder_id;
|
|
|
|
DROP INDEX core.uq_pool_provisions_live_subscription_product;
|
|
DROP INDEX core.uq_pool_provisions_live_purchase;
|
|
DROP INDEX core.uq_pool_provisions_live_grant;
|
|
ALTER TABLE core.pool_provisions DROP CONSTRAINT fk_pool_provisions_product_id;
|
|
ALTER TABLE core.pool_provisions DROP COLUMN product_id;
|
|
|
|
-- Data reversals are snapshot-based (design D7): the column returns empty
|
|
-- and the pre-migration reason/product values are not resurrected.
|
|
ALTER TABLE core.grants ADD COLUMN entitlement_set_id UUID;
|
|
ALTER TABLE core.grants ADD CONSTRAINT fk_grants_entitlement_set_id
|
|
FOREIGN KEY (entitlement_set_id) REFERENCES core.entitlement_sets(set_id);
|
|
ALTER TABLE core.grants ALTER COLUMN product_id DROP NOT NULL;
|
|
ALTER TABLE core.grants ADD CONSTRAINT chk_grants_entitlement_source
|
|
CHECK (entitlement_set_id IS NOT NULL OR product_id IS NOT NULL);
|
|
ALTER TABLE core.grants DROP CONSTRAINT chk_grants_default_iff_system_authored;
|
|
ALTER TABLE core.grants DROP CONSTRAINT chk_grants_reason_domain;
|
|
ALTER TABLE core.grants ADD CONSTRAINT chk_grants_system_actor
|
|
CHECK (granted_by_person_id IS NOT NULL OR grant_reason = 'default');
|
|
|
|
ALTER TABLE core.pool_provisions DROP CONSTRAINT chk_pool_provisions_quantity_positive;
|
|
|
|
ALTER TABLE core.prices DROP CONSTRAINT chk_prices_usage_pricing_requires_recurrence;
|
|
ALTER TABLE core.prices DROP CONSTRAINT chk_prices_usage_type_domain;
|
|
ALTER TABLE core.prices DROP COLUMN usage_type;
|