import { literal as literal$1 } from "pg-format";
import { assert } from "../../../../../../shared/src/asserts.js";
import "../../../../../../shared/src/valita.js";
import { upstreamSchema } from "../../../../types/shards.js";
import { id } from "../../../../types/sql.js";
import { publishedSchema, publishedTableQuery, indexDefinitionsQuery } from "./published.js";
import { object, string, literal, union } from "@badrap/valita";
const PROTOCOL_VERSION = 1;
const triggerEvent = object({
  context: object({ query: string() }).rest(string())
});
const ddlEventSchema = triggerEvent.extend({
  version: literal(PROTOCOL_VERSION),
  schema: publishedSchema
});
const ddlStartEventSchema = ddlEventSchema.extend({
  type: literal("ddlStart")
});
const ddlUpdateEventSchema = ddlEventSchema.extend({
  type: literal("ddlUpdate"),
  event: object({ tag: string() })
});
const replicationEventSchema = union(
  ddlStartEventSchema,
  ddlUpdateEventSchema
);
function append(shardNum) {
  return (name) => id(name + "_" + String(shardNum));
}
function createEventFunctionStatements(shard) {
  const { appID, shardNum, publications } = shard;
  const schema = id(upstreamSchema(shard));
  return (
    /*sql*/
    `
CREATE SCHEMA IF NOT EXISTS ${schema};

CREATE OR REPLACE FUNCTION ${schema}.get_trigger_context()
RETURNS record AS $$
DECLARE
  result record;
BEGIN
  SELECT current_query() AS "query" into result;
  RETURN result;
END
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION ${schema}.notice_ignore(tag TEXT, target record)
RETURNS void AS $$
BEGIN
  RAISE NOTICE 'zero(%) ignoring % %', ${literal$1(shardNum)}, tag, row_to_json(target);
END
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION ${schema}.schema_specs()
RETURNS TEXT AS $$
DECLARE
  tables record;
  indexes record;
BEGIN
  ${publishedTableQuery(publications)} INTO tables;
  ${indexDefinitionsQuery(publications)} INTO indexes;
  RETURN json_build_object(
    'tables', tables.tables,
    'indexes', indexes.indexes
  );
END
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION ${schema}.emit_ddl_start()
RETURNS event_trigger AS $$
DECLARE
  schema_specs TEXT;
  message TEXT;
BEGIN
  SELECT ${schema}.schema_specs() INTO schema_specs;

  SELECT json_build_object(
    'type', 'ddlStart',
    'version', ${PROTOCOL_VERSION},
    'schema', schema_specs::json,
    'context', ${schema}.get_trigger_context()
  ) INTO message;

  PERFORM pg_logical_emit_message(true, ${literal$1(
      `${appID}/${shardNum}`
    )}, message);
END
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION ${schema}.emit_ddl_end(tag TEXT)
RETURNS void AS $$
DECLARE
  publications TEXT[];
  target RECORD;
  relevant RECORD;
  schema_specs TEXT;
  message TEXT;
  event TEXT;
BEGIN
  publications := ARRAY[${literal$1(publications)}];

  SELECT objid, object_type, object_identity 
    FROM pg_event_trigger_ddl_commands() 
    LIMIT 1 INTO target;

  -- Filter DDL updates that are not relevant to the shard (i.e. publications) when possible.

  IF target.object_type = 'table' OR target.object_type = 'table column' THEN
    SELECT ns.nspname AS "schema", c.relname AS "name" FROM pg_class AS c
      JOIN pg_namespace AS ns ON c.relnamespace = ns.oid
      JOIN pg_publication_tables AS pb ON pb.schemaname = ns.nspname AND pb.tablename = c.relname
      WHERE c.oid = target.objid AND pb.pubname = ANY (publications)
      INTO relevant;
    IF relevant IS NULL THEN
      PERFORM ${schema}.notice_ignore(tag, target);
      RETURN;
    END IF;

  ELSIF target.object_type = 'index' THEN
    SELECT ns.nspname AS "schema", c.relname AS "name" FROM pg_class AS c
      JOIN pg_namespace AS ns ON c.relnamespace = ns.oid
      JOIN pg_indexes as ind ON ind.schemaname = ns.nspname AND ind.indexname = c.relname
      JOIN pg_publication_tables AS pb ON pb.schemaname = ns.nspname AND pb.tablename = ind.tablename
      WHERE c.oid = target.objid AND pb.pubname = ANY (publications)
      INTO relevant;
    IF relevant IS NULL THEN
      PERFORM ${schema}.notice_ignore(tag, target);
      RETURN;
    END IF;

  ELSIF target.object_type = 'publication relation' THEN
    SELECT pb.pubname FROM pg_publication_rel AS rel
      JOIN pg_publication AS pb ON pb.oid = rel.prpubid
      WHERE rel.oid = target.objid AND pb.pubname = ANY (publications) 
      INTO relevant;
    IF relevant IS NULL THEN
      PERFORM ${schema}.notice_ignore(tag, target);
      RETURN;
    END IF;

  ELSIF target.object_type = 'publication namespace' THEN
    SELECT pb.pubname FROM pg_publication_namespace AS ns
      JOIN pg_publication AS pb ON pb.oid = ns.pnpubid
      WHERE ns.oid = target.objid AND pb.pubname = ANY (publications) 
      INTO relevant;
    IF relevant IS NULL THEN
      PERFORM ${schema}.notice_ignore(tag, target);
      RETURN;
    END IF;

  ELSIF target.object_type = 'schema' THEN
    SELECT ns.nspname AS "schema", c.relname AS "name" FROM pg_class AS c
      JOIN pg_namespace AS ns ON c.relnamespace = ns.oid
      JOIN pg_publication_tables AS pb ON pb.schemaname = ns.nspname AND pb.tablename = c.relname
      WHERE ns.oid = target.objid AND pb.pubname = ANY (publications)
      INTO relevant;
    IF relevant IS NULL THEN
      PERFORM ${schema}.notice_ignore(tag, target);
      RETURN;
    END IF;

  -- no-op CREATE IF NOT EXIST statements
  ELSIF tag LIKE 'CREATE %' AND target.object_type IS NULL THEN
    PERFORM ${schema}.notice_ignore(tag, target);
    RETURN;
  END IF;

  RAISE INFO 'Creating ddlUpdate for % %', tag, row_to_json(target);

  -- Construct and emit the DdlUpdateEvent message.
  SELECT json_build_object('tag', tag) INTO event;
  
  SELECT ${schema}.schema_specs() INTO schema_specs;

  SELECT json_build_object(
    'type', 'ddlUpdate',
    'version', ${PROTOCOL_VERSION},
    'schema', schema_specs::json,
    'event', event::json,
    'context', ${schema}.get_trigger_context()
  ) INTO message;

  PERFORM pg_logical_emit_message(true, ${literal$1(
      `${appID}/${shardNum}`
    )}, message);
END
$$ LANGUAGE plpgsql;
`
  );
}
const TAGS = [
  "CREATE TABLE",
  "ALTER TABLE",
  "CREATE INDEX",
  "DROP TABLE",
  "DROP INDEX",
  "ALTER PUBLICATION",
  "ALTER SCHEMA"
];
function createEventTriggerStatements(shard) {
  assert(shard.publications.length, `shard publications must be non-empty`);
  const { appID, shardNum } = shard;
  const sharded = append(shardNum);
  const schema = id(upstreamSchema(shard));
  const triggers = [
    dropEventTriggerStatements(shard.appID, shard.shardNum),
    createEventFunctionStatements(shard)
  ];
  triggers.push(
    /*sql*/
    `
CREATE EVENT TRIGGER ${sharded(`${appID}_ddl_start`)}
  ON ddl_command_start
  WHEN TAG IN (${literal$1(TAGS)})
  EXECUTE PROCEDURE ${schema}.emit_ddl_start();
`
  );
  for (const tag of TAGS) {
    const tagID = tag.toLowerCase().replace(" ", "_");
    triggers.push(
      /*sql*/
      `
CREATE OR REPLACE FUNCTION ${schema}.emit_${tagID}() 
RETURNS event_trigger AS $$
BEGIN
  PERFORM ${schema}.emit_ddl_end(${literal$1(tag)});
END
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER ${sharded(`${appID}_${tagID}`)}
  ON ddl_command_end
  WHEN TAG IN (${literal$1(tag)})
  EXECUTE PROCEDURE ${schema}.emit_${tagID}();
`
    );
  }
  return triggers.join("");
}
function dropEventTriggerStatements(appID, shardID) {
  const stmts = [];
  stmts.push(
    /*sql*/
    `
    DROP EVENT TRIGGER IF EXISTS ${id(`${appID}_ddl_start_${shardID}`)};
  `
  );
  for (const tag of TAGS) {
    const tagID = tag.toLowerCase().replace(" ", "_");
    stmts.push(
      /*sql*/
      `
      DROP EVENT TRIGGER IF EXISTS ${id(`${appID}_${tagID}_${shardID}`)};
    `
    );
  }
  return stmts.join("");
}
export {
  PROTOCOL_VERSION,
  TAGS,
  createEventTriggerStatements,
  ddlEventSchema,
  ddlStartEventSchema,
  ddlUpdateEventSchema,
  dropEventTriggerStatements,
  replicationEventSchema
};
//# sourceMappingURL=ddl.js.map
