Files
tiku-backend.net/Tiku.Infrastructure/Persistence/Migrations/MigrationBuilderDatabaseObjectExtensions.cs
xiong 3f5957d744
Some checks failed
ci / release-gate (push) Has been cancelled
feat(content): establish v2 learning delivery foundation
2026-08-06 09:52:18 +08:00

306 lines
15 KiB
C#

using Microsoft.EntityFrameworkCore.Migrations;
namespace Tiku.Infrastructure.Persistence.Migrations;
internal static class MigrationBuilderDatabaseObjectExtensions
{
public static void EnsureAuthorizationCacheInvalidationTriggers(this MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
INSERT INTO authorization_scope_versions
(id, realm, tenant_id, version, created_at, updated_at)
VALUES
(gen_random_uuid(), 'platform', NULL, 1, now(), now())
ON CONFLICT DO NOTHING;
INSERT INTO authorization_scope_versions
(id, realm, tenant_id, version, created_at, updated_at)
SELECT gen_random_uuid(), 'tenant', id, 1, now(), now()
FROM tenants
ON CONFLICT DO NOTHING;
CREATE OR REPLACE FUNCTION tiku_bump_tenant_authorization_version()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
scope_tenant_id uuid;
next_version bigint;
BEGIN
IF TG_OP = 'DELETE' THEN
scope_tenant_id := OLD.tenant_id;
ELSE
scope_tenant_id := NEW.tenant_id;
END IF;
UPDATE authorization_scope_versions
SET version = version + 1, updated_at = now()
WHERE realm = 'tenant' AND tenant_id = scope_tenant_id
RETURNING version INTO next_version;
IF next_version IS NOT NULL THEN
INSERT INTO authorization_cache_invalidations
(id, target_type, tenant_id, realm, version, attempt_count, created_at, updated_at)
VALUES
(gen_random_uuid(), 'scope', scope_tenant_id, 'tenant', next_version, 0, now(), now());
END IF;
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
RETURN NEW;
END;
$$;
CREATE OR REPLACE FUNCTION tiku_bump_platform_authorization_version()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE next_version bigint;
BEGIN
UPDATE authorization_scope_versions
SET version = version + 1, updated_at = now()
WHERE realm = 'platform' AND tenant_id IS NULL
RETURNING version INTO next_version;
INSERT INTO authorization_cache_invalidations
(id, target_type, realm, version, attempt_count, created_at, updated_at)
VALUES
(gen_random_uuid(), 'scope', 'platform', next_version, 0, now(), now());
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
RETURN NEW;
END;
$$;
CREATE OR REPLACE FUNCTION tiku_bump_all_authorization_versions()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE authorization_scope_versions SET version = version + 1, updated_at = now();
INSERT INTO authorization_cache_invalidations
(id, target_type, tenant_id, realm, version, attempt_count, created_at, updated_at)
SELECT gen_random_uuid(), 'scope', tenant_id, realm, version, 0, now(), now()
FROM authorization_scope_versions;
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_tenant_backend_roles_authorization_version ON tenant_backend_roles;
DROP TRIGGER IF EXISTS trg_tenant_backend_role_permissions_authorization_version ON tenant_backend_role_permissions;
DROP TRIGGER IF EXISTS trg_tenant_backend_user_roles_authorization_version ON tenant_backend_user_roles;
DROP TRIGGER IF EXISTS trg_platform_backend_roles_authorization_version ON platform_backend_roles;
DROP TRIGGER IF EXISTS trg_platform_backend_role_permissions_authorization_version ON platform_backend_role_permissions;
DROP TRIGGER IF EXISTS trg_platform_backend_user_roles_authorization_version ON platform_backend_user_roles;
DROP TRIGGER IF EXISTS trg_backend_permissions_authorization_version ON backend_permissions;
CREATE TRIGGER trg_tenant_backend_roles_authorization_version
AFTER INSERT OR UPDATE OR DELETE ON tenant_backend_roles
FOR EACH ROW EXECUTE FUNCTION tiku_bump_tenant_authorization_version();
CREATE TRIGGER trg_tenant_backend_role_permissions_authorization_version
AFTER INSERT OR UPDATE OR DELETE ON tenant_backend_role_permissions
FOR EACH ROW EXECUTE FUNCTION tiku_bump_tenant_authorization_version();
CREATE TRIGGER trg_tenant_backend_user_roles_authorization_version
AFTER INSERT OR UPDATE OR DELETE ON tenant_backend_user_roles
FOR EACH ROW EXECUTE FUNCTION tiku_bump_tenant_authorization_version();
CREATE TRIGGER trg_platform_backend_roles_authorization_version
AFTER INSERT OR UPDATE OR DELETE ON platform_backend_roles
FOR EACH ROW EXECUTE FUNCTION tiku_bump_platform_authorization_version();
CREATE TRIGGER trg_platform_backend_role_permissions_authorization_version
AFTER INSERT OR UPDATE OR DELETE ON platform_backend_role_permissions
FOR EACH ROW EXECUTE FUNCTION tiku_bump_platform_authorization_version();
CREATE TRIGGER trg_platform_backend_user_roles_authorization_version
AFTER INSERT OR UPDATE OR DELETE ON platform_backend_user_roles
FOR EACH ROW EXECUTE FUNCTION tiku_bump_platform_authorization_version();
CREATE TRIGGER trg_backend_permissions_authorization_version
AFTER INSERT OR UPDATE OR DELETE ON backend_permissions
FOR EACH STATEMENT EXECUTE FUNCTION tiku_bump_all_authorization_versions();
""");
}
public static void DropAuthorizationCacheInvalidationTriggers(this MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
DROP FUNCTION IF EXISTS tiku_bump_tenant_authorization_version() CASCADE;
DROP FUNCTION IF EXISTS tiku_bump_platform_authorization_version() CASCADE;
DROP FUNCTION IF EXISTS tiku_bump_all_authorization_versions() CASCADE;
""");
}
public static void EnsureQueryOptimizationIndexes(this MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
CREATE INDEX IF NOT EXISTS ix_scoreline_records_tenant_id_year_school_name_major_name_id
ON scoreline_records (tenant_id, year DESC, school_name, major_name, id);
CREATE INDEX IF NOT EXISTS ix_scoreline_records_field_values_jsonb_path
ON scoreline_records USING gin (field_values jsonb_path_ops);
CREATE INDEX IF NOT EXISTS ix_scoreline_records_school_name_trgm
ON scoreline_records USING gin (school_name gin_trgm_ops) WHERE school_name IS NOT NULL;
CREATE INDEX IF NOT EXISTS ix_scoreline_records_major_name_trgm
ON scoreline_records USING gin (major_name gin_trgm_ops) WHERE major_name IS NOT NULL;
CREATE INDEX IF NOT EXISTS ix_background_jobs_pending_due
ON background_jobs (run_after, created_at, id) WHERE status = 'pending';
CREATE INDEX IF NOT EXISTS ix_background_jobs_processing_lease
ON background_jobs (lock_expires_at, created_at, id) WHERE status = 'processing';
""");
}
public static void DropQueryOptimizationIndexes(this MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
DROP INDEX IF EXISTS ix_background_jobs_processing_lease;
DROP INDEX IF EXISTS ix_background_jobs_pending_due;
DROP INDEX IF EXISTS ix_scoreline_records_major_name_trgm;
DROP INDEX IF EXISTS ix_scoreline_records_school_name_trgm;
DROP INDEX IF EXISTS ix_scoreline_records_field_values_jsonb_path;
DROP INDEX IF EXISTS ix_scoreline_records_tenant_id_year_school_name_major_name_id;
""");
}
public static void EnsureContentV2ImmutabilityGuards(this MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
CREATE OR REPLACE FUNCTION tiku_guard_question_revision_immutable()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
RAISE EXCEPTION 'question revisions are immutable';
END;
$$;
DROP TRIGGER IF EXISTS trg_question_revisions_immutable ON question_revisions;
CREATE TRIGGER trg_question_revisions_immutable
BEFORE UPDATE OR DELETE ON question_revisions
FOR EACH ROW EXECUTE FUNCTION tiku_guard_question_revision_immutable();
CREATE OR REPLACE FUNCTION tiku_guard_published_manifest_version_immutable()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF OLD.status = 'published' THEN
RAISE EXCEPTION 'published product access manifest versions are immutable';
END IF;
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_product_manifest_versions_published_immutable
ON product_access_manifest_versions;
CREATE TRIGGER trg_product_manifest_versions_published_immutable
BEFORE UPDATE OR DELETE ON product_access_manifest_versions
FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_manifest_version_immutable();
CREATE OR REPLACE FUNCTION tiku_guard_published_manifest_child_immutable()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
parent_tenant_id uuid;
parent_version_id uuid;
parent_status text;
BEGIN
IF TG_OP = 'DELETE' THEN
parent_tenant_id := OLD.tenant_id;
parent_version_id := OLD.product_access_manifest_version_id;
ELSE
parent_tenant_id := NEW.tenant_id;
parent_version_id := NEW.product_access_manifest_version_id;
END IF;
SELECT status INTO parent_status
FROM product_access_manifest_versions
WHERE tenant_id = parent_tenant_id AND id = parent_version_id;
IF parent_status = 'published' THEN
RAISE EXCEPTION 'children of a published product access manifest version are immutable';
END IF;
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_product_manifest_releases_published_immutable ON product_manifest_releases;
DROP TRIGGER IF EXISTS trg_product_manifest_targets_published_immutable ON product_manifest_targets;
DROP TRIGGER IF EXISTS trg_product_manifest_resources_published_immutable ON product_manifest_resources;
CREATE TRIGGER trg_product_manifest_releases_published_immutable
BEFORE INSERT OR UPDATE OR DELETE ON product_manifest_releases
FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_manifest_child_immutable();
CREATE TRIGGER trg_product_manifest_targets_published_immutable
BEFORE INSERT OR UPDATE OR DELETE ON product_manifest_targets
FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_manifest_child_immutable();
CREATE TRIGGER trg_product_manifest_resources_published_immutable
BEFORE INSERT OR UPDATE OR DELETE ON product_manifest_resources
FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_manifest_child_immutable();
CREATE OR REPLACE FUNCTION tiku_guard_published_content_release_immutable()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF OLD.status = 'published' THEN
RAISE EXCEPTION 'published content releases are immutable';
END IF;
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_content_releases_published_immutable ON content_releases;
CREATE TRIGGER trg_content_releases_published_immutable
BEFORE UPDATE OR DELETE ON content_releases
FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_content_release_immutable();
CREATE OR REPLACE FUNCTION tiku_guard_published_content_release_child_immutable()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
parent_tenant_id uuid;
parent_release_id uuid;
parent_status text;
BEGIN
IF TG_OP = 'DELETE' THEN
parent_tenant_id := OLD.tenant_id;
parent_release_id := OLD.content_release_id;
ELSE
parent_tenant_id := NEW.tenant_id;
parent_release_id := NEW.content_release_id;
END IF;
SELECT status INTO parent_status
FROM content_releases
WHERE tenant_id = parent_tenant_id AND id = parent_release_id;
IF parent_status = 'published' THEN
RAISE EXCEPTION 'children of a published content release are immutable';
END IF;
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_audience_segments_release_immutable ON audience_segments;
DROP TRIGGER IF EXISTS trg_audience_segment_members_release_immutable ON audience_segment_members;
DROP TRIGGER IF EXISTS trg_content_release_questions_release_immutable ON content_release_questions;
CREATE TRIGGER trg_audience_segments_release_immutable
BEFORE INSERT OR UPDATE OR DELETE ON audience_segments
FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_content_release_child_immutable();
CREATE TRIGGER trg_audience_segment_members_release_immutable
BEFORE INSERT OR UPDATE OR DELETE ON audience_segment_members
FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_content_release_child_immutable();
CREATE TRIGGER trg_content_release_questions_release_immutable
BEFORE INSERT OR UPDATE OR DELETE ON content_release_questions
FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_content_release_child_immutable();
""");
}
public static void DropContentV2ImmutabilityGuards(this MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
DROP FUNCTION IF EXISTS tiku_guard_published_content_release_child_immutable() CASCADE;
DROP FUNCTION IF EXISTS tiku_guard_published_content_release_immutable() CASCADE;
DROP FUNCTION IF EXISTS tiku_guard_published_manifest_child_immutable() CASCADE;
DROP FUNCTION IF EXISTS tiku_guard_published_manifest_version_immutable() CASCADE;
DROP FUNCTION IF EXISTS tiku_guard_question_revision_immutable() CASCADE;
""");
}
}