import { AbortError } from "../../../../../shared/src/abort-error.js";
import { equals } from "../../../../../shared/src/set-utils.js";
import { cdcSchema } from "../../../types/shards.js";
import "../../../types/pg.js";
import { runTx } from "../../../db/run-transaction.js";
import "postgres";
import { ident } from "pg-format";
//#region ../zero-cache/src/services/change-streamer/schema/tables.ts
function schema(shard) {
	return ident(cdcSchema(shard));
}
function createSchema(shard) {
	return `CREATE SCHEMA IF NOT EXISTS ${schema(shard)};`;
}
function createChangeLogTable(shard) {
	return `
  CREATE TABLE ${schema(shard)}."changeLog" (
    watermark  TEXT,
    pos        INT8,
    change     JSON NOT NULL,
    precommit  TEXT,  -- Only exists on commit entries. Purely for debugging.
    PRIMARY KEY (watermark, pos)
  );
`;
}
function createReplicationStateTable(shard) {
	return `
  CREATE TABLE ${schema(shard)}."replicationState" (
    "lastWatermark" TEXT NOT NULL,
    "owner" TEXT,
    "ownerAddress" TEXT,
    "lock" INTEGER PRIMARY KEY DEFAULT 1 CHECK (lock=1)
  );
`;
}
async function discoverChangeStreamerAddress(shard, sql) {
	return (await sql`
    SELECT "ownerAddress" FROM ${sql(cdcSchema(shard))}."replicationState"`)[0].ownerAddress;
}
function createReplicationConfigTable(shard) {
	return `
  CREATE TABLE ${schema(shard)}."replicationConfig" (
    "replicaVersion" TEXT NOT NULL,
    "publications" TEXT[] NOT NULL,
    "resetRequired" BOOL,
    "lock" INTEGER PRIMARY KEY DEFAULT 1 CHECK (lock=1)
  );
`;
}
function createBackfillTables(shard) {
	return `
  CREATE TABLE ${schema(shard)}."tableMetadata" (
    "schema" TEXT NOT NULL,
    "table" TEXT NOT NULL,
    "metadata" JSONB NOT NULL,
    PRIMARY KEY("schema", "table")
  );

  CREATE TABLE ${schema(shard)}."backfilling" (
    "schema" TEXT NOT NULL,
    "table" TEXT NOT NULL,
    "column" TEXT NOT NULL,
    "backfill" JSONB NOT NULL,
    PRIMARY KEY("schema", "table", "column")
  );
  `;
}
function createTables(shard) {
	return createSchema(shard) + createChangeLogTable(shard) + createReplicationStateTable(shard) + createReplicationConfigTable(shard) + createBackfillTables(shard);
}
async function setupCDCTables(lc, db, shard) {
	lc.info?.(`Setting up CDC tables`);
	await db.unsafe(createTables(shard));
}
async function markResetRequired(sql, shard) {
	await sql`
  UPDATE ${sql(cdcSchema(shard))}."replicationConfig"
    SET "resetRequired" = true`;
}
async function ensureReplicationConfig(lc, db, subscriptionState, shard, autoReset, setTimeoutFn = setTimeout) {
	const { publications, replicaVersion, watermark } = subscriptionState;
	const replicaConfig = {
		publications,
		replicaVersion
	};
	const replicationState = {
		lastWatermark: replicaVersion,
		owner: null,
		ownerAddress: null
	};
	const schema = cdcSchema(shard);
	await runTx(db, async (sql) => {
		const stmts = [];
		let needsTruncate = false;
		const results = await sql`
    SELECT "replicaVersion", "publications", "resetRequired" 
      FROM ${sql(schema)}."replicationConfig"`;
		if (results.length) {
			const { replicaVersion, publications } = results[0];
			if (replicaVersion !== replicaConfig.replicaVersion || !equals(new Set(publications), new Set(replicaConfig.publications))) {
				if (replicaConfig.replicaVersion !== watermark) throw new AutoResetSignal(`Cannot reset change db@${replicaVersion} to service replica@${replicaConfig.replicaVersion} from watermark ${watermark}`);
				lc.info?.(`Data in cdc tables @${replicaVersion} is incompatible with replica @${replicaConfig.replicaVersion}. Clearing tables.`);
				needsTruncate = true;
				stmts.push(sql`TRUNCATE TABLE ${sql(schema)}."changeLog"`, sql`TRUNCATE TABLE ${sql(schema)}."replicationConfig"`, sql`TRUNCATE TABLE ${sql(schema)}."tableMetadata"`, sql`TRUNCATE TABLE ${sql(schema)}."backfilling"`);
			}
		}
		if (results.length === 0 || needsTruncate) {
			const watermark = replicaConfig.replicaVersion;
			const initialTx = [{
				watermark,
				pos: 0,
				change: { tag: "begin" }
			}, {
				watermark,
				pos: 1,
				change: { tag: "commit" }
			}];
			stmts.push(sql`INSERT INTO ${sql(schema)}."replicationConfig" ${sql(replicaConfig)}`, sql`INSERT INTO ${sql(schema)}."replicationState"  ${sql(replicationState)} 
              ON CONFLICT (lock) DO UPDATE SET ${sql(replicationState)}`, ...initialTx.map((change) => sql`INSERT INTO ${sql(schema)}."changeLog" ${sql(change)}`));
			if (needsTruncate) {
				const timer = setTimeoutFn(async () => {
					lc.info?.("ensureReplicationConfig blocked, terminating lock holders");
					await terminateChangeDBLockHolders(lc, db, shard);
				}, LOCK_HOLDER_TERMINATE_TIMEOUT_MS);
				try {
					return await Promise.all(stmts);
				} finally {
					clearTimeout(timer);
				}
			}
			return Promise.all(stmts);
		}
		const { resetRequired } = results[0];
		if (resetRequired) {
			if (autoReset) throw new AutoResetSignal("reset required by replication stream");
			lc.error?.("\n\n\nReset required but --auto-reset is not enabled.\nThis can happen for upstream databases that do not support event triggers.\nTo correct this, see https://zero.rocicorp.dev/docs/connecting-to-postgres#schema-changes\n\n\n");
		}
		return [];
	});
}
var LOCK_HOLDER_TERMINATE_TIMEOUT_MS = 5e3;
var CHANGE_STREAMER_APP_NAME = "zero-change-streamer";
var AutoResetSignal = class extends AbortError {
	name = "AutoResetSignal";
};
/**
* Terminates zero-cache backends that are blocking the current backend
* from acquiring locks on CDC tables (e.g., during TRUNCATE).
*
* This is used during change-DB takeover when the new replication-manager's
* `ensureReplicationConfig` needs to TRUNCATE tables, but the old
* replication-manager's storer is still reading from them (e.g., large
* catchup cursors).
*
* The function:
* 1. Finds backends waiting for a lock on a TRUNCATE in {schema}
* 2. Uses `pg_blocking_pids()` to identify which backends are blocking them
* 3. Terminates blocking backends that have `application_name = 'zero-change-streamer'`
*
* Must be called on a **separate connection** from the one that is blocked,
* since the blocked connection is inside a pending transaction.
*/
async function terminateChangeDBLockHolders(lc, db, shard) {
	const blocked = await db`
    SELECT pid FROM pg_stat_activity
      WHERE wait_event_type = 'Lock'
        AND application_name = ${CHANGE_STREAMER_APP_NAME}
        AND query LIKE ${"%TRUNCATE%" + cdcSchema(shard) + "%"}`;
	if (blocked.length === 0) {
		lc.info?.("no blocked TRUNCATE backends found");
		return;
	}
	const blockedPids = blocked.map((r) => r.pid);
	lc.info?.(`found blocked TRUNCATE backends: ${JSON.stringify(blockedPids)}`);
	const terminated = await db`
    SELECT pid, application_name as "applicationName", query,
           pg_terminate_backend(pid) as terminated
      FROM pg_stat_activity
      WHERE pid = ANY(
        SELECT unnest(pg_blocking_pids(blocked.pid))
          FROM unnest(${blockedPids}::int[]) AS blocked(pid)
      )
      AND application_name = ${CHANGE_STREAMER_APP_NAME}
      AND pid != ALL(${blockedPids}::int[])`;
	if (terminated.length === 0) lc.info?.(`no ${CHANGE_STREAMER_APP_NAME} blockers found to terminate`);
	else for (const { pid, applicationName, query, terminated: ok } of terminated) lc.info?.(`terminated blocking backend pid=${pid} app=${applicationName} ok=${ok} query=${query.slice(0, 200)}`);
}
//#endregion
export { AutoResetSignal, CHANGE_STREAMER_APP_NAME, createBackfillTables, createReplicationStateTable, discoverChangeStreamerAddress, ensureReplicationConfig, markResetRequired, setupCDCTables };

//# sourceMappingURL=tables.js.map