-- pg_trickle 0.92.0 -> 0.93.0
--
-- v0.93.0 adds durable MANAGED/EXTERNAL orchestration ownership and the
-- versioned Graph V1 contract APIs. Existing stream tables remain MANAGED.

ALTER TABLE pgtrickle.pgt_stream_tables
    ADD COLUMN IF NOT EXISTS orchestration_mode TEXT NOT NULL DEFAULT 'MANAGED';
ALTER TABLE pgtrickle.pgt_stream_tables
    ADD COLUMN IF NOT EXISTS contract_generation BIGINT NOT NULL DEFAULT 1;

UPDATE pgtrickle.pgt_stream_tables
SET orchestration_mode = 'MANAGED'
WHERE orchestration_mode IS NULL
   OR upper(orchestration_mode) NOT IN ('MANAGED', 'EXTERNAL');
UPDATE pgtrickle.pgt_stream_tables
SET contract_generation = 1
WHERE contract_generation IS NULL OR contract_generation < 1;

DO $$
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM pg_catalog.pg_constraint
        WHERE conname = 'pgt_stream_tables_orchestration_mode_check'
          AND conrelid = 'pgtrickle.pgt_stream_tables'::regclass
    ) THEN
        ALTER TABLE pgtrickle.pgt_stream_tables
            ADD CONSTRAINT pgt_stream_tables_orchestration_mode_check
            CHECK (orchestration_mode IN ('MANAGED', 'EXTERNAL'));
    END IF;
    IF NOT EXISTS (
        SELECT 1 FROM pg_catalog.pg_constraint
        WHERE conname = 'pgt_stream_tables_contract_generation_check'
          AND conrelid = 'pgtrickle.pgt_stream_tables'::regclass
    ) THEN
        ALTER TABLE pgtrickle.pgt_stream_tables
            ADD CONSTRAINT pgt_stream_tables_contract_generation_check
            CHECK (contract_generation > 0);
    END IF;
END
$$;

CREATE OR REPLACE FUNCTION pgtrickle._bump_contract_generation()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = pgtrickle, pg_catalog, pg_temp
AS $$
BEGIN
    IF ROW(OLD.pgt_relid, OLD.pgt_name, OLD.pgt_schema, OLD.defining_query,
           OLD.original_query, OLD.refresh_mode, OLD.requested_refresh_mode,
           OLD.functions_used, OLD.topk_limit, OLD.topk_order_by,
           OLD.topk_offset, OLD.diamond_consistency,
           OLD.diamond_schedule_policy, OLD.has_keyless_source,
           OLD.requested_cdc_mode, OLD.is_append_only, OLD.scc_id,
           OLD.st_partition_key, OLD.temporal_mode, OLD.storage_backend,
           OLD.row_identity_version, OLD.row_probe_version,
           OLD.defining_search_path, OLD.orchestration_mode)
       IS DISTINCT FROM
       ROW(NEW.pgt_relid, NEW.pgt_name, NEW.pgt_schema, NEW.defining_query,
           NEW.original_query, NEW.refresh_mode, NEW.requested_refresh_mode,
           NEW.functions_used, NEW.topk_limit, NEW.topk_order_by,
           NEW.topk_offset, NEW.diamond_consistency,
           NEW.diamond_schedule_policy, NEW.has_keyless_source,
           NEW.requested_cdc_mode, NEW.is_append_only, NEW.scc_id,
           NEW.st_partition_key, NEW.temporal_mode, NEW.storage_backend,
           NEW.row_identity_version, NEW.row_probe_version,
           NEW.defining_search_path, NEW.orchestration_mode) THEN
        NEW.contract_generation := OLD.contract_generation + 1;
    END IF;
    RETURN NEW;
END;
$$;

DROP TRIGGER IF EXISTS pgt_stream_tables_contract_generation
    ON pgtrickle.pgt_stream_tables;
CREATE TRIGGER pgt_stream_tables_contract_generation
BEFORE UPDATE OF pgt_relid, pgt_name, pgt_schema, defining_query,
    original_query, refresh_mode, requested_refresh_mode, functions_used,
    topk_limit, topk_order_by, topk_offset, diamond_consistency,
    diamond_schedule_policy, has_keyless_source, requested_cdc_mode,
    is_append_only, scc_id, st_partition_key, temporal_mode, storage_backend,
    row_identity_version, row_probe_version, defining_search_path,
    orchestration_mode
ON pgtrickle.pgt_stream_tables
FOR EACH ROW EXECUTE FUNCTION pgtrickle._bump_contract_generation();

CREATE OR REPLACE FUNCTION pgtrickle._bump_dependency_contract_generation()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = pgtrickle, pg_catalog, pg_temp
AS $$
BEGIN
    UPDATE pgtrickle.pgt_stream_tables
    SET contract_generation = contract_generation + 1
    WHERE pgt_id = CASE WHEN TG_OP = 'DELETE' THEN OLD.pgt_id ELSE NEW.pgt_id END;
    IF TG_OP = 'DELETE' THEN
        RETURN OLD;
    END IF;
    RETURN NEW;
END;
$$;

DROP TRIGGER IF EXISTS pgt_dependencies_contract_generation
    ON pgtrickle.pgt_dependencies;
CREATE TRIGGER pgt_dependencies_contract_generation
AFTER INSERT OR UPDATE OR DELETE ON pgtrickle.pgt_dependencies
FOR EACH ROW EXECUTE FUNCTION pgtrickle._bump_dependency_contract_generation();

-- The create APIs gain an optional orchestration mode. Rename the old C
-- functions instead of dropping them: dependent views and functions retain
-- their OID-bound references, while normal calls resolve to the new API.
ALTER FUNCTION pgtrickle.create_stream_table(
    text, text, text, text, boolean, text, text, text, boolean, boolean,
    text, integer, double precision, text, boolean, text, integer, text
) RENAME TO create_stream_table__v092;
ALTER FUNCTION pgtrickle.create_stream_table_if_not_exists(
    text, text, text, text, boolean, text, text, text, boolean, boolean,
    text, integer, double precision, text, boolean, text, integer, text
) RENAME TO create_stream_table_if_not_exists__v092;
ALTER FUNCTION pgtrickle.create_or_replace_stream_table(
    text, text, text, text, boolean, text, text, text, boolean, boolean,
    text, integer, double precision, text, boolean, text
) RENAME TO create_or_replace_stream_table__v092;

CREATE OR REPLACE FUNCTION pgtrickle."create_stream_table"(
    "name" TEXT,
    "query" TEXT,
    "schedule" TEXT DEFAULT 'calculated',
    "refresh_mode" TEXT DEFAULT 'AUTO',
    "initialize" bool DEFAULT true,
    "diamond_consistency" TEXT DEFAULT NULL,
    "diamond_schedule_policy" TEXT DEFAULT NULL,
    "cdc_mode" TEXT DEFAULT NULL,
    "append_only" bool DEFAULT false,
    "pooler_compatibility_mode" bool DEFAULT false,
    "partition_by" TEXT DEFAULT NULL,
    "max_differential_joins" INT DEFAULT NULL,
    "max_delta_fraction" double precision DEFAULT NULL,
    "output_distribution_column" TEXT DEFAULT NULL,
    "temporal" bool DEFAULT false,
    "storage_backend" TEXT DEFAULT NULL,
    "fillfactor" INT DEFAULT NULL,
    "target_freshness" TEXT DEFAULT NULL,
    "orchestration_mode" TEXT DEFAULT 'MANAGED'
)
RETURNS void
SECURITY DEFINER -- nosemgrep: semgrep.sql.security-definer.present — search_path is pinned immediately below.
SET search_path TO pgtrickle, pg_catalog, pg_temp
LANGUAGE c
AS 'MODULE_PATHNAME', 'create_stream_table_wrapper';

CREATE OR REPLACE FUNCTION pgtrickle."create_stream_table_if_not_exists"(
    "name" TEXT,
    "query" TEXT,
    "schedule" TEXT DEFAULT 'calculated',
    "refresh_mode" TEXT DEFAULT 'AUTO',
    "initialize" bool DEFAULT true,
    "diamond_consistency" TEXT DEFAULT NULL,
    "diamond_schedule_policy" TEXT DEFAULT NULL,
    "cdc_mode" TEXT DEFAULT NULL,
    "append_only" bool DEFAULT false,
    "pooler_compatibility_mode" bool DEFAULT false,
    "partition_by" TEXT DEFAULT NULL,
    "max_differential_joins" INT DEFAULT NULL,
    "max_delta_fraction" double precision DEFAULT NULL,
    "output_distribution_column" TEXT DEFAULT NULL,
    "temporal" bool DEFAULT false,
    "storage_backend" TEXT DEFAULT NULL,
    "fillfactor" INT DEFAULT NULL,
    "target_freshness" TEXT DEFAULT NULL,
    "orchestration_mode" TEXT DEFAULT 'MANAGED'
)
RETURNS void
SECURITY DEFINER -- nosemgrep: semgrep.sql.security-definer.present — search_path is pinned immediately below.
SET search_path TO pgtrickle, pg_catalog, pg_temp
LANGUAGE c
AS 'MODULE_PATHNAME', 'create_stream_table_if_not_exists_wrapper';

CREATE OR REPLACE FUNCTION pgtrickle."create_or_replace_stream_table"(
    "name" TEXT,
    "query" TEXT,
    "schedule" TEXT DEFAULT 'calculated',
    "refresh_mode" TEXT DEFAULT 'AUTO',
    "initialize" bool DEFAULT true,
    "diamond_consistency" TEXT DEFAULT NULL,
    "diamond_schedule_policy" TEXT DEFAULT NULL,
    "cdc_mode" TEXT DEFAULT NULL,
    "append_only" bool DEFAULT false,
    "pooler_compatibility_mode" bool DEFAULT false,
    "partition_by" TEXT DEFAULT NULL,
    "max_differential_joins" INT DEFAULT NULL,
    "max_delta_fraction" double precision DEFAULT NULL,
    "output_distribution_column" TEXT DEFAULT NULL,
    "temporal" bool DEFAULT false,
    "storage_backend" TEXT DEFAULT NULL,
    "orchestration_mode" TEXT DEFAULT NULL
)
RETURNS void
SECURITY DEFINER -- nosemgrep: semgrep.sql.security-definer.present — search_path is pinned immediately below.
SET search_path TO pgtrickle, pg_catalog, pg_temp
LANGUAGE c
AS 'MODULE_PATHNAME', 'create_or_replace_stream_table_wrapper';

CREATE OR REPLACE FUNCTION pgtrickle."integration_capabilities"()
RETURNS TABLE (
    "capability" TEXT,
    "major_version" smallint,
    "minor_version" smallint,
    "enabled" bool,
    "details" jsonb
)
STRICT
LANGUAGE c
AS 'MODULE_PATHNAME', 'integration_capabilities_wrapper';

CREATE OR REPLACE FUNCTION pgtrickle."set_orchestration_mode"(
    "stream_table" regclass,
    "mode" TEXT
)
RETURNS TEXT
STRICT SECURITY DEFINER -- nosemgrep: semgrep.sql.security-definer.present — search_path is pinned immediately below.
SET search_path TO pgtrickle, pg_catalog, pg_temp
LANGUAGE c
AS 'MODULE_PATHNAME', 'set_orchestration_mode_wrapper';

CREATE OR REPLACE FUNCTION pgtrickle."stream_table_contract"(
    "stream_table" regclass
)
RETURNS TABLE (
    "contract_version" smallint,
    "contract_generation" bigint,
    "contract_digest" bytea,
    "contract" jsonb
)
STRICT SECURITY DEFINER -- nosemgrep: semgrep.sql.security-definer.present — search_path is pinned immediately below.
SET search_path TO pgtrickle, pg_catalog, pg_temp
LANGUAGE c
AS 'MODULE_PATHNAME', 'stream_table_contract_wrapper';

CREATE OR REPLACE FUNCTION pgtrickle."graph_contract"(
    "roots" regclass[]
)
RETURNS TABLE (
    "contract_version" smallint,
    "graph_digest" bytea,
    "contract" jsonb
)
STRICT SECURITY DEFINER -- nosemgrep: semgrep.sql.security-definer.present — search_path is pinned immediately below.
SET search_path TO pgtrickle, pg_catalog, pg_temp
LANGUAGE c
AS 'MODULE_PATHNAME', 'graph_contract_wrapper';

GRANT EXECUTE ON FUNCTION pgtrickle.integration_capabilities() TO PUBLIC;
GRANT EXECUTE ON FUNCTION pgtrickle.stream_table_contract(regclass) TO PUBLIC;
GRANT EXECUTE ON FUNCTION pgtrickle.graph_contract(regclass[]) TO PUBLIC;
GRANT EXECUTE ON FUNCTION pgtrickle.set_orchestration_mode(regclass, text) TO PUBLIC;
REVOKE EXECUTE ON FUNCTION pgtrickle.create_stream_table__v092(text, text, text, text, boolean, text, text, text, boolean, boolean, text, integer, double precision, text, boolean, text, integer, text) FROM PUBLIC;
REVOKE EXECUTE ON FUNCTION pgtrickle.create_stream_table_if_not_exists__v092(text, text, text, text, boolean, text, text, text, boolean, boolean, text, integer, double precision, text, boolean, text, integer, text) FROM PUBLIC;
REVOKE EXECUTE ON FUNCTION pgtrickle.create_or_replace_stream_table__v092(text, text, text, text, boolean, text, text, text, boolean, boolean, text, integer, double precision, text, boolean, text) FROM PUBLIC;

INSERT INTO pgtrickle.pgt_schema_version (version, description)
VALUES (
    '0.93.0',
    'Graph V1 contracts and durable external orchestration ownership'
)
ON CONFLICT (version) DO NOTHING;
