import { assert } from "../../../../../../shared/src/asserts.js";
import { valita_exports } from "../../../../../../shared/src/valita.js";
import { upstreamSchema } from "../../../../types/shards.js";
import { indexDefinitionsQuery, publishedSchema, publishedTableQuery } from "./published.js";
import { id } from "../../../../types/sql.js";
import { literal } from "pg-format";
var ddlEventSchema = valita_exports.object({ context: valita_exports.object({ query: valita_exports.string() }).rest(valita_exports.string()) }).extend({
	version: valita_exports.literal(1),
	schema: publishedSchema
});
var ddlStartEventSchema = ddlEventSchema.extend({ type: valita_exports.literal("ddlStart") });
/**
* The {@link DdlUpdateEvent} contains an updated schema resulting from
* a particular ddl event. The event type provides information
* (i.e. constraints) on the difference from the schema of the preceding
* {@link DdlStartEvent}.
*
* Note that in almost all cases (the exception being `CREATE` events),
* it is possible that there is no relevant difference between the
* ddl-start schema and the ddl-update schema, as many aspects of the
* schema (e.g. column constraints) are not relevant to downstream
* replication.
*/
var ddlUpdateEventSchema = ddlEventSchema.extend({
	type: valita_exports.literal("ddlUpdate"),
	event: valita_exports.object({ tag: valita_exports.string() })
});
/**
* The `schemaSnapshot` message is a snapshot of a schema taken in response to
* a `COMMENT ON PUBLICATION` command, which is a hook recognized by zero
* to manually emit schema snapshots to support detection of schema changes
* from `ALTER PUBLICATION` commands on supabase, which does not fire event
* triggers for them (https://github.com/supabase/supautils/issues/123).
*
* The hook is exercised by bookmarking the publication change with
* `COMMENT ON PUBLICATION` statements within e.g.
*
* ```sql
* BEGIN;
* COMMENT ON PUBLICATION my_publication IS 'whatever';
* ALTER PUBLICATION my_publication ...;
* COMMENT ON PUBLICATION my_publication IS 'whatever';
* COMMIT;
* ```
*
* The `change-source` will perform the diff between a `schemaSnapshot`
* events and its preceding `schemaSnapshot` (or `ddlUpdate`) within the
* transaction.
*
* In the case where event trigger support is missing, this results in
* diffing the `schemaSnapshot`s before and after the `ALTER PUBLICATION`
* statement, thus effecting the same logic that would have been exercised
* between the `ddlStart` and `ddlEvent` events fired by a database with
* fully functional event triggers.
*
* Note that if the same transaction is run on a database that *does*
* support event triggers on `ALTER PUBLICATION` statements, the sequence
* of emitted messages will be:
*
* * `schemaSnapshot`
* * `ddlStart`
* * `ddlUpdate`
* * `schemaSnapshot`
*
* Since `schemaSnapshot` messages are diffed with the preceding
* `schemaSnapshot` or `ddlUpdate` event (if any), there will be no schema
* difference between the `ddlUpdate` and the second `schemaSnapshot`, and
* thus the extra `COMMENT` statements will effectively be no-ops.
*/
var schemaSnapshotEventSchema = ddlEventSchema.extend({
	type: valita_exports.literal("schemaSnapshot"),
	event: valita_exports.object({ tag: valita_exports.string() })
});
var replicationEventSchema = valita_exports.union(ddlStartEventSchema, ddlUpdateEventSchema, schemaSnapshotEventSchema);
function append(shardNum) {
	return (name) => id(name + "_" + String(shardNum));
}
/**
* Event trigger functions contain the core logic that are invoked by triggers.
*
* Note that although many of these functions can theoretically be parameterized and
* shared across shards, it is advantageous to keep the functions in each shard
* isolated from each other in order to avoid the complexity of shared-function
* versioning.
*
* In a sense, shards (and their triggers and functions) should be thought of as
* execution environments that can be updated at different schedules. If per-shard
* triggers called into shared functions, we would have to consider versioning the
* functions when changing their behavior, backwards compatibility, removal of
* unused versions, etc. (not unlike versioning of npm packages).
*
* Instead, we opt for the simplicity and isolation of having each shard
* completely own (and maintain) the entirety of its trigger/function stack.
*/
function createEventFunctionStatements(shard) {
	const { appID, shardNum, publications } = shard;
	const schema = id(upstreamSchema(shard));
	return `
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(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', 1,
    'schema', schema_specs::json,
    'context', ${schema}.get_trigger_context()
  ) INTO message;

  PERFORM pg_logical_emit_message(true, ${literal(`${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;
  event_type TEXT;
  event_prefix TEXT;
BEGIN
  publications := ARRAY[${literal(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.
  SELECT true INTO relevant;

  -- Note: ALTER TABLE statements may *remove* the table from the set of published
  --       tables, and there is no way to determine if the table "used to be" in the
  --       set. Thus, all ALTER TABLE statements must produce a ddl update, similar to
  --       any DROP * statement.
  IF (target.object_type = 'table' AND tag != 'ALTER 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;

  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;

  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;

  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;

  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;

  ELSIF target.object_type = 'publication' THEN
    SELECT 1 WHERE target.object_identity = ANY (publications)
      INTO relevant;

  -- no-op CREATE IF NOT EXIST statements
  ELSIF tag LIKE 'CREATE %' AND target.object_type IS NULL THEN
    relevant := NULL;
  END IF;

  IF relevant IS NULL THEN
    PERFORM ${schema}.notice_ignore(tag, target);
    RETURN;
  END IF;

  IF tag = 'COMMENT' THEN
    -- Only make schemaSnapshots for COMMENT ON PUBLICATION
    IF target.object_type != 'publication' THEN
      PERFORM ${schema}.notice_ignore(tag, target);
      RETURN;
    END IF;
    event_type := 'schemaSnapshot';
    event_prefix := '/ddl';
  ELSE
    event_type := 'ddlUpdate';
    event_prefix := '';  -- TODO: Use '/ddl' for both when rollback safe
  END IF;

  RAISE INFO 'Creating % for % %', event_type, 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', event_type,
    'version', 1,
    'schema', schema_specs::json,
    'event', event::json,
    'context', ${schema}.get_trigger_context()
  ) INTO message;

  PERFORM pg_logical_emit_message(true, ${literal(`${appID}/${shardNum}`)} || event_prefix, message);
END
$$ LANGUAGE plpgsql;
`;
}
var 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(`
CREATE EVENT TRIGGER ${sharded(`${appID}_ddl_start`)}
  ON ddl_command_start
  WHEN TAG IN (${literal(TAGS)})
  EXECUTE PROCEDURE ${schema}.emit_ddl_start();
`);
	for (const tag of [...TAGS, "COMMENT"]) {
		const tagID = tag.toLowerCase().replace(" ", "_");
		triggers.push(`
CREATE OR REPLACE FUNCTION ${schema}.emit_${tagID}() 
RETURNS event_trigger AS $$
BEGIN
  PERFORM ${schema}.emit_ddl_end(${literal(tag)});
END
$$ LANGUAGE plpgsql;

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

//# sourceMappingURL=ddl.js.map