@rocicorp/zero 1.12.0-canary.2 → 1.12.0-canary.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/analyze-query/src/analyze-cli.js +2 -2
- package/out/analyze-query/src/analyze-cli.js.map +1 -1
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +4 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +4 -0
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +2 -1
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
- package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source-init.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source-init.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source-init.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +1 -0
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js +5 -5
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +2 -2
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js +8 -8
- package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/published.d.ts +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/published.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/published.js +2 -1
- package/out/zero-cache/src/services/change-source/pg/schema/published.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +2 -2
- package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js +4 -4
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +2 -2
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.d.ts +18 -0
- package/out/zero-cache/src/services/view-syncer/client-handler.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js +46 -20
- package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +4 -3
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js +5 -2
- package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
- package/out/zero-cache/src/workers/connection.js +3 -3
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +2 -2
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-protocol/src/inspect-down.d.ts +81 -81
- package/out/zero-protocol/src/poke.d.ts +6 -6
- package/out/zero-protocol/src/up.d.ts +60 -60
- package/out/zql/src/ivm/cap.js +2 -4
- package/out/zql/src/ivm/cap.js.map +1 -1
- package/out/zql/src/ivm/take.js +2 -4
- package/out/zql/src/ivm/take.js.map +1 -1
- package/out/zqlite/src/database-storage.d.ts.map +1 -1
- package/out/zqlite/src/database-storage.js +38 -6
- package/out/zqlite/src/database-storage.js.map +1 -1
- package/out/zqlite/src/fetch-sql.d.ts +27 -0
- package/out/zqlite/src/fetch-sql.d.ts.map +1 -0
- package/out/zqlite/src/fetch-sql.js +112 -0
- package/out/zqlite/src/fetch-sql.js.map +1 -0
- package/out/zqlite/src/query-builder.d.ts +11 -1
- package/out/zqlite/src/query-builder.d.ts.map +1 -1
- package/out/zqlite/src/query-builder.js +22 -7
- package/out/zqlite/src/query-builder.js.map +1 -1
- package/out/zqlite/src/table-source.d.ts +9 -2
- package/out/zqlite/src/table-source.d.ts.map +1 -1
- package/out/zqlite/src/table-source.js +11 -14
- package/out/zqlite/src/table-source.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/init.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../../../shared/src/asserts.ts';\nimport * as v from '../../../../../../shared/src/valita.ts';\nimport {\n runSchemaMigrations,\n type IncrementalMigrationMap,\n type Migration,\n} from '../../../../db/migration.ts';\nimport type {PostgresDB} from '../../../../types/pg.ts';\nimport {upstreamSchema, type ShardConfig} from '../../../../types/shards.ts';\nimport {id} from '../../../../types/sql.ts';\nimport {AutoResetSignal} from '../../../change-streamer/schema/tables.ts';\nimport {publishedSchema} from './published.ts';\nimport * as ReplicaStage from './replica-stage-enum.ts';\nimport {\n getMutationsTableDefinition,\n legacyReplicationSlot,\n metadataPublicationName,\n setupTablesAndReplication,\n setupTriggers,\n} from './shard.ts';\n\n/**\n * Ensures that a shard is set up for initial sync.\n */\nexport async function ensureShardSchema(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardConfig,\n): Promise<void> {\n const initialSetup: Migration = {\n migrateSchema: (lc, tx) => setupTablesAndReplication(lc, tx, shard),\n minSafeVersion: 1,\n };\n await runSchemaMigrations(\n lc,\n `upstream-shard-${shard.appID}`,\n upstreamSchema(shard),\n db,\n initialSetup,\n // The incremental migration of any existing replicas will be replaced by\n // the incoming replica being synced, so the replicaVersion here is\n // unnecessary.\n getIncrementalMigrations(shard),\n );\n}\n\nfunction getIncrementalMigrations(shard: ShardConfig): IncrementalMigrationMap {\n const shardConfigTable = `${upstreamSchema(shard)}.shardConfig`;\n\n return {\n 6: {\n migrateSchema: () => {\n throw new AutoResetSignal('resetting to upgrade shard schema');\n },\n },\n\n // v7: Updates the DDL event trigger protocol to v2, and adds support for\n // ALTER SCHEMA x RENAME TO y\n // (migration logic subsumbed by v23)\n\n // v8: Adds support for non-disruptive resyncs, which tracks multiple\n // replicas with different slot names.\n 8: {\n migrateSchema: async (lc, sql) => {\n const legacyShardConfigSchema = v.object({\n replicaVersion: v.string().nullable(),\n initialSchema: publishedSchema.nullable(),\n });\n const result = await sql`\n SELECT \"replicaVersion\", \"initialSchema\" FROM ${sql(shardConfigTable)}`;\n assert(\n result.length === 1,\n () => `Expected exactly one shardConfig row, got ${result.length}`,\n );\n const {replicaVersion, initialSchema} = v.parse(\n result[0],\n legacyShardConfigSchema,\n 'passthrough',\n );\n\n await Promise.all([\n sql`\n CREATE TABLE ${sql(upstreamSchema(shard))}.replicas (\n \"slot\" TEXT PRIMARY KEY,\n \"version\" TEXT NOT NULL,\n \"initialSchema\" JSON NOT NULL\n );\n `,\n sql`\n INSERT INTO ${sql(upstreamSchema(shard))}.replicas ${sql({\n slot: legacyReplicationSlot(shard),\n version: replicaVersion,\n initialSchema,\n })}\n `,\n sql`\n ALTER TABLE ${sql(shardConfigTable)} DROP \"replicaVersion\", DROP \"initialSchema\"\n `,\n ]);\n lc.info?.(`Upgraded schema to support non-disruptive resyncs`);\n },\n },\n\n // v9: Fixes field ordering of compound indexes. This incremental migration\n // only fixes indexes resulting from new schema changes. A full resync is\n // required to fix existing indexes.\n //\n // The migration has been subsumed by the identical logic for migrating\n // to v12 (i.e. a trigger upgrade).\n\n // Adds the `mutations` table used to track mutation results.\n 10: {\n migrateSchema: async (lc, sql) => {\n await sql.unsafe(/*sql*/ `\n ${getMutationsTableDefinition(upstreamSchema(shard))}\n ALTER PUBLICATION ${id(metadataPublicationName(shard.appID, shard.shardNum))} ADD TABLE ${id(upstreamSchema(shard))}.\"mutations\";\n `);\n lc.info?.('Upgraded schema with new mutations table');\n },\n },\n\n // v11: Formerly dropped the schemaVersions table, but restored in the v13\n // migration for rollback safety.\n\n // v12: Upgrade DDL trigger to query schemaOID, needed information for auto-backfill.\n // (subsumed by v14)\n\n // Recreates the legacy schemaVersions table that was prematurely dropped\n // in the (former) v11 migration. It needs to remain present for at least one\n // release in order to be rollback safe.\n //\n // TODO: Drop the table once a release that no longer reads the table has\n // been rolled out.\n 13: {\n migrateSchema: async (_, sql) => {\n await sql`\n CREATE TABLE IF NOT EXISTS ${sql(upstreamSchema(shard))}.\"schemaVersions\" (\n \"minSupportedVersion\" INT4,\n \"maxSupportedVersion\" INT4,\n \"lock\" BOOL PRIMARY KEY DEFAULT true CHECK (lock)\n );`;\n await sql`\n INSERT INTO ${sql(upstreamSchema(shard))}.\"schemaVersions\" \n (\"lock\", \"minSupportedVersion\", \"maxSupportedVersion\")\n VALUES (true, 1, 1)\n ON CONFLICT DO NOTHING;\n `;\n },\n },\n\n // v14: Upgrade DDL trigger to log more info to PG logs.\n // (subsumed by v16)\n\n // Add initialSyncContext column to replicas table.\n 15: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas\n ADD COLUMN \"initialSyncContext\" JSON,\n ADD COLUMN \"subscriberContext\" JSON\n `;\n },\n },\n\n // v16: Upgrade DDL trigger to fire on all ALTER TABLE statements\n // to catch the *removal* of a table from the published set.\n // v17 (1.0.0): Upgrade DDL triggers to support the COMMENT ON PUBLICATION hook for\n // working around the lack of event trigger support for PUBLICATION\n // changes in supabase.\n // This also adds forwards-compatible support for hierarchical logical\n // message prefixes and unknown ddl event types.\n // v18: Pure refactoring of event trigger code.\n // v19 (1.4.0): Correctly handle concurrently issued DDL statements.\n // v20 (1.4.0): Handle nested DDL triggers\n // v21 (1.5.0): Handle cross-transaction DDL operations (i.e. concurrent\n // index operations), and support manual invocation of update_schemas().\n\n 22: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ALTER \"initialSchema\" DROP NOT NULL;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n DROP CONSTRAINT replicas_pkey;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN id TEXT PRIMARY KEY DEFAULT replace(gen_random_uuid()::text, '-', '');\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN rank BIGSERIAL;\n `;\n },\n },\n\n // v23 (1.6.0): Event triggers: Handle the case where current_query()\n // returns NULL. Add more logging to debug unexpected messages\n //\n // Note: This is retained (despite v31 also upgrading the triggers) because\n // the v28 migration depends on the update_schemas() function.\n 23: {\n migrateSchema: async (lc, sql) => {\n const [{publications}] = await sql<{publications: string[]}[]>`\n SELECT publications FROM ${sql(shardConfigTable)}`;\n await setupTriggers(lc, sql, {...shard, publications});\n lc.info?.(`Upgraded DDL event triggers`);\n },\n },\n\n // v24 (1.10.0):\n // - adds the \"backupPath\" column for per-replica backups.\n // - adds the \"generation\" column to replace \"version\".\n 24: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN \"backupPath\" TEXT;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN \"generation\" TEXT;\n `;\n },\n\n migrateData: async (_, sql) => {\n // Backfill the \"generation\" column to eventually replace \"version\"\n await sql`\n UPDATE ${sql(upstreamSchema(shard))}.replicas SET \"generation\" = \"version\";\n `;\n },\n },\n\n // v25 (1.10.0):\n // - adds the \"backupV5\" column to signal the current semantics\n // of the replication slot's confirmed_flush_lsn with respect to what\n // has been backed up.\n 25: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN \"backupV5\" BOOL DEFAULT false;\n `;\n },\n },\n\n // v27 (1.11.0):\n // - make `generation` NOT NULL and stop reading `version`\n 27: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ALTER \"generation\" SET NOT NULL;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ALTER \"version\" DROP NOT NULL;\n `;\n },\n },\n\n 28: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ADD \"epoch\" INT4 DEFAULT 0;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ADD \"stage\" INT4 DEFAULT 0; -- ReplicaStage.InitialSync\n `;\n\n const metapub = metadataPublicationName(shard.appID, shard.shardNum);\n await sql`\n ALTER PUBLICATION ${sql(metapub)} ADD TABLE ${sql(upstreamSchema(shard))}.replicas (id);\n `;\n // Make sure supabase gets the ALTER PUBLICATION change\n await sql`\n SELECT ${sql(upstreamSchema(shard))}.update_schemas();\n `;\n },\n\n migrateData: async (_, sql) => {\n await sql`\n UPDATE ${sql(upstreamSchema(shard))}.replicas SET \"stage\" = ${ReplicaStage.Replicate}\n WHERE \"subscriberContext\" IS NOT NULL;\n `;\n },\n },\n\n // v30: Adds the \"pgChangeLog\" column, which records whether the\n // replication-manager that took over a replica's slot acks it only after\n // storing transactions in the PG change log (RMv1.5), in which case a\n // slot position past the change log's head reflects acked keepalives\n // rather than missing transactions.\n //\n // The column defaults to false (i.e. unknown), so that a slot is assumed\n // to be possibly ahead of the change log until it is taken over by a\n // replication-manager that sets it. Existing replicas that have been taken\n // over are backfilled as true (see migrateData). Note that a replication-manager that\n // predates this column does not reset it when taking over a slot, so\n // once the column is set, RMv2 (in which acks are not gated on the PG\n // change log) must run at least this version.\n 30: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN IF NOT EXISTS \"pgChangeLog\" BOOL DEFAULT false;\n `;\n },\n\n // Existing replicas whose slots have been taken over (i.e. that have a\n // subscriberContext) were acked by RMv1 or RMv1.5, which store\n // transactions in the PG change log before acking them. This assumes\n // that no RMv2 is running when the migration is run. Replicas that have\n // not been taken over (e.g. fork destinations) remain unknown, as their\n // slots have not been acked.\n migrateData: async (_, sql) => {\n await sql`\n UPDATE ${sql(upstreamSchema(shard))}.replicas SET \"pgChangeLog\" = true\n WHERE \"subscriberContext\" IS NOT NULL;\n `;\n },\n },\n\n // v31: Upgrade the DDL event triggers to include partial indexes in\n // schema snapshots. Note that setupTriggers() also refreshes the\n // stored \"publishedSchema\" so that the change in format does not\n // manifest as a spurious schema change.\n 31: {\n migrateSchema: async (lc, sql) => {\n const [{publications}] = await sql<{publications: string[]}[]>`\n SELECT publications FROM ${sql(shardConfigTable)}`;\n await setupTriggers(lc, sql, {...shard, publications});\n lc.info?.(`Upgraded DDL event triggers`);\n },\n },\n };\n}\n\n// Referenced in tests.\nexport const CURRENT_SCHEMA_VERSION = Object.keys(\n getIncrementalMigrations({\n appID: 'unused',\n shardNum: 0,\n publications: ['foo'],\n }),\n).reduce((prev, curr) => Math.max(prev, parseInt(curr)), 0);\n"],"mappings":";;;;;;;;;;;;;AAyBA,eAAsB,kBACpB,IACA,IACA,OACe;CAKf,MAAM,oBACJ,IACA,kBAAkB,MAAM,SACxB,eAAe,KAAK,GACpB,IACA;EARA,gBAAgB,IAAI,OAAO,0BAA0B,IAAI,IAAI,KAAK;EAClE,gBAAgB;CAOhB,GAIA,yBAAyB,KAAK,CAChC;AACF;AAEA,SAAS,yBAAyB,OAA6C;CAC7E,MAAM,mBAAmB,GAAG,eAAe,KAAK,EAAE;CAElD,OAAO;EACL,GAAG,EACD,qBAAqB;GACnB,MAAM,IAAI,gBAAgB,mCAAmC;EAC/D,EACF;EAQA,GAAG,EACD,eAAe,OAAO,IAAI,QAAQ;GAChC,MAAM,0BAA0B,eAAE,OAAO;IACvC,gBAAgB,eAAE,OAAO,EAAE,SAAS;IACpC,eAAe,gBAAgB,SAAS;GAC1C,CAAC;GACD,MAAM,SAAS,MAAM,GAAG;0DAC0B,IAAI,gBAAgB;GACtE,OACE,OAAO,WAAW,SACZ,6CAA6C,OAAO,QAC5D;GACA,MAAM,EAAC,gBAAgB,kBAAiB,MACtC,OAAO,IACP,yBACA,aACF;GAEA,MAAM,QAAQ,IAAI;IAChB,GAAG;yBACY,IAAI,eAAe,KAAK,CAAC,EAAE;;;;;;IAM1C,GAAG;wBACW,IAAI,eAAe,KAAK,CAAC,EAAE,YAAY,IAAI;KACvD,MAAM,sBAAsB,KAAK;KACjC,SAAS;KACT;IACF,CAAC,EAAE;;IAEH,GAAG;wBACW,IAAI,gBAAgB,EAAE;;GAEtC,CAAC;GACD,GAAG,OAAO,mDAAmD;EAC/D,EACF;EAUA,IAAI,EACF,eAAe,OAAO,IAAI,QAAQ;GAChC,MAAM,IAAI,OAAe;YACrB,4BAA4B,eAAe,KAAK,CAAC,EAAE;8BACjC,GAAG,wBAAwB,MAAM,OAAO,MAAM,QAAQ,CAAC,EAAE,aAAa,GAAG,eAAe,KAAK,CAAC,EAAE;SACrH;GACD,GAAG,OAAO,0CAA0C;EACtD,EACF;EAcA,IAAI,EACF,eAAe,OAAO,GAAG,QAAQ;GAC/B,MAAM,GAAG;uCACsB,IAAI,eAAe,KAAK,CAAC,EAAE;;;;;GAK1D,MAAM,GAAG;wBACO,IAAI,eAAe,KAAK,CAAC,EAAE;;;;;EAK7C,EACF;EAMA,IAAI,EACF,eAAe,OAAO,GAAG,QAAQ;GAC/B,MAAM,GAAG;wBACO,IAAI,eAAe,KAAK,CAAC,EAAE;;;;EAI7C,EACF;EAeA,IAAI,EACF,eAAe,OAAO,GAAG,QAAQ;GAC/B,MAAM,GAAG;wBACO,IAAI,eAAe,KAAK,CAAC,EAAE;;;GAG3C,MAAM,GAAG;wBACO,IAAI,eAAe,KAAK,CAAC,EAAE;;;GAG3C,MAAM,GAAG;wBACO,IAAI,eAAe,KAAK,CAAC,EAAE;;;GAG3C,MAAM,GAAG;wBACO,IAAI,eAAe,KAAK,CAAC,EAAE;;;EAG7C,EACF;EAOA,IAAI,EACF,eAAe,OAAO,IAAI,QAAQ;GAChC,MAAM,CAAC,EAAC,kBAAiB,MAAM,GAA+B;qCACjC,IAAI,gBAAgB;GACjD,MAAM,cAAc,IAAI,KAAK;IAAC,GAAG;IAAO;GAAY,CAAC;GACrD,GAAG,OAAO,6BAA6B;EACzC,EACF;EAKA,IAAI;GACF,eAAe,OAAO,GAAG,QAAQ;IAC/B,MAAM,GAAG;wBACO,IAAI,eAAe,KAAK,CAAC,EAAE;;;IAG3C,MAAM,GAAG;wBACO,IAAI,eAAe,KAAK,CAAC,EAAE;;;GAG7C;GAEA,aAAa,OAAO,GAAG,QAAQ;IAE7B,MAAM,GAAG;mBACE,IAAI,eAAe,KAAK,CAAC,EAAE;;GAExC;EACF;EAMA,IAAI,EACF,eAAe,OAAO,GAAG,QAAQ;GAC/B,MAAM,GAAG;wBACO,IAAI,eAAe,KAAK,CAAC,EAAE;;;EAG7C,EACF;EAIA,IAAI,EACF,eAAe,OAAO,GAAG,QAAQ;GAC/B,MAAM,GAAG;wBACO,IAAI,eAAe,KAAK,CAAC,EAAE;;GAE3C,MAAM,GAAG;wBACO,IAAI,eAAe,KAAK,CAAC,EAAE;;EAE7C,EACF;EAEA,IAAI;GACF,eAAe,OAAO,GAAG,QAAQ;IAC/B,MAAM,GAAG;wBACO,IAAI,eAAe,KAAK,CAAC,EAAE;;IAE3C,MAAM,GAAG;wBACO,IAAI,eAAe,KAAK,CAAC,EAAE;;IAI3C,MAAM,GAAG;8BACa,IAFN,wBAAwB,MAAM,OAAO,MAAM,QAEjC,CAAO,EAAE,aAAa,IAAI,eAAe,KAAK,CAAC,EAAE;;IAG3E,MAAM,GAAG;mBACE,IAAI,eAAe,KAAK,CAAC,EAAE;;GAExC;GAEA,aAAa,OAAO,GAAG,QAAQ;IAC7B,MAAM,GAAG;mBACE,IAAI,eAAe,KAAK,CAAC,EAAE,0BAA0B,EAAuB;;;GAGzF;EACF;EAeA,IAAI;GACF,eAAe,OAAO,GAAG,QAAQ;IAC/B,MAAM,GAAG;wBACO,IAAI,eAAe,KAAK,CAAC,EAAE;;;GAG7C;GAQA,aAAa,OAAO,GAAG,QAAQ;IAC7B,MAAM,GAAG;mBACE,IAAI,eAAe,KAAK,CAAC,EAAE;;;GAGxC;EACF;EAMA,IAAI,EACF,eAAe,OAAO,IAAI,QAAQ;GAChC,MAAM,CAAC,EAAC,kBAAiB,MAAM,GAA+B;qCACjC,IAAI,gBAAgB;GACjD,MAAM,cAAc,IAAI,KAAK;IAAC,GAAG;IAAO;GAAY,CAAC;GACrD,GAAG,OAAO,6BAA6B;EACzC,EACF;CACF;AACF;AAGsC,OAAO,KAC3C,yBAAyB;CACvB,OAAO;CACP,UAAU;CACV,cAAc,CAAC,KAAK;AACtB,CAAC,CACH,EAAE,QAAQ,MAAM,SAAS,KAAK,IAAI,MAAM,SAAS,IAAI,CAAC,GAAG,CAAC"}
|
|
1
|
+
{"version":3,"file":"init.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/init.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../../../shared/src/asserts.ts';\nimport * as v from '../../../../../../shared/src/valita.ts';\nimport {\n runSchemaMigrations,\n type IncrementalMigrationMap,\n type Migration,\n} from '../../../../db/migration.ts';\nimport type {PostgresDB} from '../../../../types/pg.ts';\nimport {upstreamSchema, type ShardConfig} from '../../../../types/shards.ts';\nimport {id} from '../../../../types/sql.ts';\nimport {AutoResetSignal} from '../../../change-streamer/schema/tables.ts';\nimport {publishedSchema} from './published.ts';\nimport * as ReplicaStage from './replica-stage-enum.ts';\nimport {\n getMutationsTableDefinition,\n legacyReplicationSlot,\n metadataPublicationName,\n setupTablesAndReplication,\n setupTriggers,\n} from './shard.ts';\n\n/**\n * Ensures that a shard is set up for initial sync.\n */\nexport async function ensureShardSchema(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardConfig,\n installPartialIndexTriggers = true,\n): Promise<void> {\n const initialSetup: Migration = {\n migrateSchema: (lc, tx) =>\n setupTablesAndReplication(lc, tx, shard, installPartialIndexTriggers),\n minSafeVersion: 1,\n };\n await runSchemaMigrations(\n lc,\n `upstream-shard-${shard.appID}`,\n upstreamSchema(shard),\n db,\n initialSetup,\n // The incremental migration of any existing replicas will be replaced by\n // the incoming replica being synced, so the replicaVersion here is\n // unnecessary.\n getIncrementalMigrations(shard, installPartialIndexTriggers),\n );\n}\n\nfunction getIncrementalMigrations(\n shard: ShardConfig,\n installPartialIndexTriggers: boolean,\n): IncrementalMigrationMap {\n const shardConfigTable = `${upstreamSchema(shard)}.shardConfig`;\n\n return {\n 6: {\n migrateSchema: () => {\n throw new AutoResetSignal('resetting to upgrade shard schema');\n },\n },\n\n // v7: Updates the DDL event trigger protocol to v2, and adds support for\n // ALTER SCHEMA x RENAME TO y\n // (migration logic subsumbed by v23)\n\n // v8: Adds support for non-disruptive resyncs, which tracks multiple\n // replicas with different slot names.\n 8: {\n migrateSchema: async (lc, sql) => {\n const legacyShardConfigSchema = v.object({\n replicaVersion: v.string().nullable(),\n initialSchema: publishedSchema.nullable(),\n });\n const result = await sql`\n SELECT \"replicaVersion\", \"initialSchema\" FROM ${sql(shardConfigTable)}`;\n assert(\n result.length === 1,\n () => `Expected exactly one shardConfig row, got ${result.length}`,\n );\n const {replicaVersion, initialSchema} = v.parse(\n result[0],\n legacyShardConfigSchema,\n 'passthrough',\n );\n\n await Promise.all([\n sql`\n CREATE TABLE ${sql(upstreamSchema(shard))}.replicas (\n \"slot\" TEXT PRIMARY KEY,\n \"version\" TEXT NOT NULL,\n \"initialSchema\" JSON NOT NULL\n );\n `,\n sql`\n INSERT INTO ${sql(upstreamSchema(shard))}.replicas ${sql({\n slot: legacyReplicationSlot(shard),\n version: replicaVersion,\n initialSchema,\n })}\n `,\n sql`\n ALTER TABLE ${sql(shardConfigTable)} DROP \"replicaVersion\", DROP \"initialSchema\"\n `,\n ]);\n lc.info?.(`Upgraded schema to support non-disruptive resyncs`);\n },\n },\n\n // v9: Fixes field ordering of compound indexes. This incremental migration\n // only fixes indexes resulting from new schema changes. A full resync is\n // required to fix existing indexes.\n //\n // The migration has been subsumed by the identical logic for migrating\n // to v12 (i.e. a trigger upgrade).\n\n // Adds the `mutations` table used to track mutation results.\n 10: {\n migrateSchema: async (lc, sql) => {\n await sql.unsafe(/*sql*/ `\n ${getMutationsTableDefinition(upstreamSchema(shard))}\n ALTER PUBLICATION ${id(metadataPublicationName(shard.appID, shard.shardNum))} ADD TABLE ${id(upstreamSchema(shard))}.\"mutations\";\n `);\n lc.info?.('Upgraded schema with new mutations table');\n },\n },\n\n // v11: Formerly dropped the schemaVersions table, but restored in the v13\n // migration for rollback safety.\n\n // v12: Upgrade DDL trigger to query schemaOID, needed information for auto-backfill.\n // (subsumed by v14)\n\n // Recreates the legacy schemaVersions table that was prematurely dropped\n // in the (former) v11 migration. It needs to remain present for at least one\n // release in order to be rollback safe.\n //\n // TODO: Drop the table once a release that no longer reads the table has\n // been rolled out.\n 13: {\n migrateSchema: async (_, sql) => {\n await sql`\n CREATE TABLE IF NOT EXISTS ${sql(upstreamSchema(shard))}.\"schemaVersions\" (\n \"minSupportedVersion\" INT4,\n \"maxSupportedVersion\" INT4,\n \"lock\" BOOL PRIMARY KEY DEFAULT true CHECK (lock)\n );`;\n await sql`\n INSERT INTO ${sql(upstreamSchema(shard))}.\"schemaVersions\" \n (\"lock\", \"minSupportedVersion\", \"maxSupportedVersion\")\n VALUES (true, 1, 1)\n ON CONFLICT DO NOTHING;\n `;\n },\n },\n\n // v14: Upgrade DDL trigger to log more info to PG logs.\n // (subsumed by v16)\n\n // Add initialSyncContext column to replicas table.\n 15: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas\n ADD COLUMN \"initialSyncContext\" JSON,\n ADD COLUMN \"subscriberContext\" JSON\n `;\n },\n },\n\n // v16: Upgrade DDL trigger to fire on all ALTER TABLE statements\n // to catch the *removal* of a table from the published set.\n // v17 (1.0.0): Upgrade DDL triggers to support the COMMENT ON PUBLICATION hook for\n // working around the lack of event trigger support for PUBLICATION\n // changes in supabase.\n // This also adds forwards-compatible support for hierarchical logical\n // message prefixes and unknown ddl event types.\n // v18: Pure refactoring of event trigger code.\n // v19 (1.4.0): Correctly handle concurrently issued DDL statements.\n // v20 (1.4.0): Handle nested DDL triggers\n // v21 (1.5.0): Handle cross-transaction DDL operations (i.e. concurrent\n // index operations), and support manual invocation of update_schemas().\n\n 22: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ALTER \"initialSchema\" DROP NOT NULL;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n DROP CONSTRAINT replicas_pkey;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN id TEXT PRIMARY KEY DEFAULT replace(gen_random_uuid()::text, '-', '');\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN rank BIGSERIAL;\n `;\n },\n },\n\n // v23 (1.6.0): Event triggers: Handle the case where current_query()\n // returns NULL. Add more logging to debug unexpected messages\n 23: {\n migrateSchema: async (lc, sql) => {\n const [{publications}] = await sql<{publications: string[]}[]>`\n SELECT publications FROM ${sql(shardConfigTable)}`;\n await setupTriggers(lc, sql, {...shard, publications});\n lc.info?.(`Upgraded DDL event triggers`);\n },\n },\n\n // v24 (1.10.0):\n // - adds the \"backupPath\" column for per-replica backups.\n // - adds the \"generation\" column to replace \"version\".\n 24: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN \"backupPath\" TEXT;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN \"generation\" TEXT;\n `;\n },\n\n migrateData: async (_, sql) => {\n // Backfill the \"generation\" column to eventually replace \"version\"\n await sql`\n UPDATE ${sql(upstreamSchema(shard))}.replicas SET \"generation\" = \"version\";\n `;\n },\n },\n\n // v25 (1.10.0):\n // - adds the \"backupV5\" column to signal the current semantics\n // of the replication slot's confirmed_flush_lsn with respect to what\n // has been backed up.\n 25: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN \"backupV5\" BOOL DEFAULT false;\n `;\n },\n },\n\n // v27 (1.11.0):\n // - make `generation` NOT NULL and stop reading `version`\n 27: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ALTER \"generation\" SET NOT NULL;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ALTER \"version\" DROP NOT NULL;\n `;\n },\n },\n\n 28: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ADD \"epoch\" INT4 DEFAULT 0;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ADD \"stage\" INT4 DEFAULT 0; -- ReplicaStage.InitialSync\n `;\n\n const metapub = metadataPublicationName(shard.appID, shard.shardNum);\n await sql`\n ALTER PUBLICATION ${sql(metapub)} ADD TABLE ${sql(upstreamSchema(shard))}.replicas (id);\n `;\n // Make sure supabase gets the ALTER PUBLICATION change\n await sql`\n SELECT ${sql(upstreamSchema(shard))}.update_schemas();\n `;\n },\n\n migrateData: async (_, sql) => {\n await sql`\n UPDATE ${sql(upstreamSchema(shard))}.replicas SET \"stage\" = ${ReplicaStage.Replicate}\n WHERE \"subscriberContext\" IS NOT NULL;\n `;\n },\n },\n\n // v30: Adds the \"pgChangeLog\" column, which records whether the\n // replication-manager that took over a replica's slot acks it only after\n // storing transactions in the PG change log (RMv1.5), in which case a\n // slot position past the change log's head reflects acked keepalives\n // rather than missing transactions.\n //\n // The column defaults to false (i.e. unknown), so that a slot is assumed\n // to be possibly ahead of the change log until it is taken over by a\n // replication-manager that sets it. Existing replicas that have been taken\n // over are backfilled as true (see migrateData). Note that a replication-manager that\n // predates this column does not reset it when taking over a slot, so\n // once the column is set, RMv2 (in which acks are not gated on the PG\n // change log) must run at least this version.\n 30: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN IF NOT EXISTS \"pgChangeLog\" BOOL DEFAULT false;\n `;\n },\n\n // Existing replicas whose slots have been taken over (i.e. that have a\n // subscriberContext) were acked by RMv1 or RMv1.5, which store\n // transactions in the PG change log before acking them. This assumes\n // that no RMv2 is running when the migration is run. Replicas that have\n // not been taken over (e.g. fork destinations) remain unknown, as their\n // slots have not been acked.\n migrateData: async (_, sql) => {\n await sql`\n UPDATE ${sql(upstreamSchema(shard))}.replicas SET \"pgChangeLog\" = true\n WHERE \"subscriberContext\" IS NOT NULL;\n `;\n },\n },\n\n // Note: While this is conditional, it always has to be bumped to the last\n // version. (It was v29, and is re-run for shards at v29 when it is bumped,\n // which is harmless as setupTriggers() is idempotent.)\n ...(installPartialIndexTriggers\n ? {\n // v31: Upgrade the DDL event triggers to include partial indexes in\n // schema snapshots. Note that setupTriggers() also refreshes the\n // stored \"publishedSchema\" so that the change in format does not\n // manifest as a spurious schema change.\n 31: {\n migrateSchema: async (lc, sql) => {\n const [{publications}] = await sql<{publications: string[]}[]>`\n SELECT publications FROM ${sql(shardConfigTable)}`;\n await setupTriggers(lc, sql, {...shard, publications});\n lc.info?.(`Upgraded DDL event triggers`);\n },\n },\n }\n : {}),\n };\n}\n\n// Referenced in tests.\nexport const CURRENT_SCHEMA_VERSION = Object.keys(\n getIncrementalMigrations(\n {\n appID: 'unused',\n shardNum: 0,\n publications: ['foo'],\n },\n true,\n ),\n).reduce((prev, curr) => Math.max(prev, parseInt(curr)), 0);\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import type { LogContext } from '@rocicorp/logger';
|
|
|
2
2
|
import type postgres from 'postgres';
|
|
3
3
|
import * as v from '../../../../../../shared/src/valita.ts';
|
|
4
4
|
import { type UnsupportedPredicateReason } from './index-predicate.ts';
|
|
5
|
-
export declare function publishedSchemaQuery(publications: readonly string[]): string;
|
|
5
|
+
export declare function publishedSchemaQuery(publications: readonly string[], includePartialIndexes?: boolean): string;
|
|
6
6
|
export type SkippedIndexDiagnostic = {
|
|
7
7
|
readonly schema: string;
|
|
8
8
|
readonly index: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"published.d.ts","sourceRoot":"","sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/published.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;AAIrC,OAAO,KAAK,CAAC,MAAM,wCAAwC,CAAC;AAc5D,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,sBAAsB,CAAC;AAE9B,wBAAgB,oBAAoB,
|
|
1
|
+
{"version":3,"file":"published.d.ts","sourceRoot":"","sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/published.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;AAIrC,OAAO,KAAK,CAAC,MAAM,wCAAwC,CAAC;AAc5D,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,sBAAsB,CAAC;AAE9B,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,qBAAqB,UAAO,UA+K7B;AAcD,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAC;CAC7C,CAAC;AAMF,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,eAAe,GACtB,SAAS,sBAAsB,EAAE,CAEnC;AAED,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,eAAe,EACvB,MAAM,GAAE,GAAG,CAAC,MAAM,CAAa,GAC9B,IAAI,CAWN;AAED,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4FxB,CAAC;AAEL,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,MAAM,MAAM,iCAAiC,GAC3C,eAAe,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAUpC,QAAA,MAAM,wBAAwB;;;;;;cAA6B,CAAC;AAE5D,MAAM,MAAM,eAAe,GAAG,eAAe,GAAG;IAC9C,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;CACxD,CAAC;AAEF;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,QAAQ,CAAC,GAAG,EACjB,YAAY,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC,eAAe,CAAC,CAsD1B"}
|
|
@@ -10,7 +10,7 @@ import { getOrCreateCounter } from "../../../../observability/metrics.js";
|
|
|
10
10
|
import { translatePostgresIndexPredicate } from "./index-predicate.js";
|
|
11
11
|
import { literal } from "pg-format";
|
|
12
12
|
//#region ../zero-cache/src/services/change-source/pg/schema/published.ts
|
|
13
|
-
function publishedSchemaQuery(publications) {
|
|
13
|
+
function publishedSchemaQuery(publications, includePartialIndexes = true) {
|
|
14
14
|
return `
|
|
15
15
|
WITH published_columns AS (SELECT
|
|
16
16
|
pc.oid::int8 AS "oid",
|
|
@@ -129,6 +129,7 @@ tables AS (SELECT json_build_object(
|
|
|
129
129
|
LEFT JOIN pg_constraint ON pg_constraint.conindid = pc.oid
|
|
130
130
|
WHERE pb.pubname IN (${literal(publications)})
|
|
131
131
|
AND pg_index.indexprs IS NULL
|
|
132
|
+
AND (${includePartialIndexes} OR pg_index.indpred IS NULL)
|
|
132
133
|
AND (pg_constraint.contype IS NULL OR pg_constraint.contype IN ('p', 'u'))
|
|
133
134
|
AND indexed.attnames <@ pb.attnames
|
|
134
135
|
AND (current_setting('server_version_num')::int >= 160000 OR false = ALL(indexed.generated))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"published.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/published.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {literal} from 'pg-format';\nimport type postgres from 'postgres';\nimport {assert} from '../../../../../../shared/src/asserts.ts';\nimport {BigIntJSON} from '../../../../../../shared/src/bigint-json.ts';\nimport {equals} from '../../../../../../shared/src/set-utils.ts';\nimport * as v from '../../../../../../shared/src/valita.ts';\nimport {computeZqlSpecsFromLiteSpecs} from '../../../../db/lite-tables.ts';\nimport {\n mapPostgresToLite,\n mapPostgresToLiteIndex,\n} from '../../../../db/pg-to-lite.ts';\nimport {\n publishedColumnSpec,\n publishedIndexSpec,\n publishedTableSpec,\n type PublishedIndexSpec,\n} from '../../../../db/specs.ts';\nimport {getOrCreateCounter} from '../../../../observability/metrics.ts';\nimport {liteTableName} from '../../../../types/names.ts';\nimport {\n translatePostgresIndexPredicate,\n type UnsupportedPredicateReason,\n} from './index-predicate.ts';\n\nexport function publishedSchemaQuery(publications: readonly string[]) {\n // Notes:\n // * There's a bug in PG15 in which generated columns are incorrectly\n // included in pg_publication_tables.attnames, (even though the generated\n // column values are not be included in the replication stream).\n // The WHERE condition `attgenerated = ''` fixes this by explicitly excluding\n // generated columns from the list.\n return (\n /*sql*/ `\nWITH published_columns AS (SELECT \n pc.oid::int8 AS \"oid\",\n nspname AS \"schema\",\n pc.relnamespace::int8 AS \"schemaOID\" ,\n pc.relname AS \"name\", \n pc.relreplident AS \"replicaIdentity\",\n attnum AS \"pos\", \n attname AS \"col\", \n pt.typname AS \"type\", \n atttypid::int8 AS \"typeOID\", \n pt.typtype,\n elem_pt.typtype AS \"elemTyptype\",\n coll.collisdeterministic AS \"collationIsDeterministic\",\n NULLIF(atttypmod, -1) AS \"maxLen\", \n attndims \"arrayDims\", \n attnotnull AS \"notNull\",\n pg_get_expr(pd.adbin, pd.adrelid) as \"dflt\",\n NULLIF(ARRAY_POSITION(conkey, attnum), -1) AS \"keyPos\", \n pb.rowfilter as \"rowFilter\",\n pb.pubname as \"publication\"\nFROM pg_attribute\nJOIN pg_class pc ON pc.oid = attrelid\nJOIN pg_namespace pns ON pns.oid = relnamespace\nJOIN pg_type pt ON atttypid = pt.oid\nLEFT JOIN pg_type elem_pt ON elem_pt.oid = pt.typelem\nLEFT JOIN pg_collation coll ON coll.oid = pg_attribute.attcollation\nJOIN pg_publication_tables as pb ON \n pb.schemaname = nspname AND \n pb.tablename = pc.relname AND\n attname = ANY(pb.attnames)\nLEFT JOIN pg_constraint pk ON pk.contype = 'p' AND pk.connamespace = relnamespace AND pk.conrelid = attrelid\nLEFT JOIN pg_attrdef pd ON pd.adrelid = attrelid AND pd.adnum = attnum\nWHERE pb.pubname IN (${literal(publications)}) AND \n (current_setting('server_version_num')::int >= 160000 OR attgenerated = '')\nORDER BY nspname, pc.relname),\n\ntables AS (SELECT json_build_object(\n 'oid', \"oid\",\n 'schema', \"schema\", \n 'schemaOID', \"schemaOID\",\n 'name', \"name\", \n 'replicaIdentity', \"replicaIdentity\",\n 'columns', json_object_agg(\n DISTINCT\n col,\n jsonb_build_object(\n 'pos', \"pos\",\n 'dataType', CASE WHEN \"arrayDims\" = 0 \n THEN \"type\" \n ELSE substring(\"type\" from 2) || repeat('[]', \"arrayDims\") END,\n 'pgTypeClass', \"typtype\",\n 'elemPgTypeClass', \"elemTyptype\",\n 'typeOID', \"typeOID\",\n 'collationIsDeterministic', \"collationIsDeterministic\",\n -- https://stackoverflow.com/a/52376230\n 'characterMaximumLength', CASE WHEN \"typeOID\" = 1043 OR \"typeOID\" = 1042 \n THEN \"maxLen\" - 4 \n ELSE \"maxLen\" END,\n 'notNull', \"notNull\",\n 'dflt', \"dflt\"\n )\n ),\n 'primaryKey', ARRAY( SELECT json_object_keys(\n json_strip_nulls(\n json_object_agg(\n DISTINCT \"col\", \"keyPos\" ORDER BY \"keyPos\"\n )\n )\n )),\n 'publications', json_object_agg(\n DISTINCT \n \"publication\", \n jsonb_build_object('rowFilter', \"rowFilter\")\n )\n) AS \"table\" FROM published_columns \n GROUP BY \"schema\", \"schemaOID\", \"name\", \"oid\", \"replicaIdentity\"),\n ` +\n // Note: pg_attribute contains column names for tables and for indexes.\n // However, the latter does not get updated when a column in a table is\n // renamed.\n //\n // https://www.postgresql.org/message-id/5860814f-c91d-4ab0-b771-ded90d7b9c55%40www.fastmail.com\n //\n // To address this, the pg_attribute rows are looked up for the index's\n // table rather than the index itself, using the pg_index.indkey array\n // to determine the set and order of columns to include.\n //\n // Notes:\n // * The first bit of indoption is 1 for DESC and 0 for ASC:\n // https://github.com/postgres/postgres/blob/4e1fad37872e49a711adad5d9870516e5c71a375/src/include/catalog/pg_index.h#L89\n // * pg_index.indkey is an int2vector which is 0-based instead of 1-based.\n // * The additional check for attgenerated is required for the aforementioned\n // (in publishedTableQuery) bug in PG15 in which generated columns are\n // incorrectly included in pg_publication_tables.attnames\n /*sql*/ `\n indexed_columns AS (SELECT\n pg_indexes.schemaname as \"schema\",\n pg_indexes.tablename as \"tableName\",\n pg_indexes.indexname as \"name\",\n index_column.name as \"col\",\n CASE WHEN pg_index.indoption[index_column.pos-1] & 1 = 1 THEN 'DESC' ELSE 'ASC' END as \"dir\",\n -- Partial indexes are always reported as non-unique. Nothing on the\n -- replica relies on the uniqueness of a partial index (it cannot serve\n -- as a row key), and reporting it as non-unique keeps the ddl event\n -- format compatible with older versions of the zero-cache, which\n -- ignore \"predicateSQL\" and would otherwise create a full UNIQUE index\n -- on the replica.\n (pg_index.indisunique AND pg_index.indpred IS NULL) as \"unique\",\n pg_index.indisprimary as \"isPrimaryKey\",\n pg_index.indisreplident as \"isReplicaIdentity\",\n pg_index.indimmediate as \"isImmediate\",\n pg_get_expr(pg_index.indpred, pg_index.indrelid, false) as \"predicateSQL\"\n FROM pg_indexes\n JOIN pg_namespace ON pg_indexes.schemaname = pg_namespace.nspname\n JOIN pg_class pc ON\n pc.relname = pg_indexes.indexname\n AND pc.relnamespace = pg_namespace.oid\n JOIN pg_publication_tables as pb ON \n pb.schemaname = pg_indexes.schemaname AND \n pb.tablename = pg_indexes.tablename\n JOIN pg_index ON pg_index.indexrelid = pc.oid\n JOIN LATERAL (\n SELECT array_agg(attname) as attnames, array_agg(attgenerated != '') as generated FROM pg_attribute\n WHERE attrelid = pg_index.indrelid\n AND attnum = ANY( (pg_index.indkey::smallint[] )[:pg_index.indnkeyatts - 1] )\n ) as indexed ON true\n JOIN LATERAL (\n SELECT pg_attribute.attname as name, col.index_pos as pos\n FROM UNNEST( (pg_index.indkey::smallint[])[:pg_index.indnkeyatts - 1] ) \n WITH ORDINALITY as col(table_pos, index_pos)\n JOIN pg_attribute ON attrelid = pg_index.indrelid AND attnum = col.table_pos\n ) AS index_column ON true\n LEFT JOIN pg_constraint ON pg_constraint.conindid = pc.oid\n WHERE pb.pubname IN (${literal(publications)})\n AND pg_index.indexprs IS NULL\n AND (pg_constraint.contype IS NULL OR pg_constraint.contype IN ('p', 'u'))\n AND indexed.attnames <@ pb.attnames\n AND (current_setting('server_version_num')::int >= 160000 OR false = ALL(indexed.generated))\n ORDER BY\n pg_indexes.schemaname,\n pg_indexes.tablename,\n pg_indexes.indexname,\n index_column.pos ASC),\n \n indexes AS (SELECT json_build_object(\n 'schema', \"schema\",\n 'tableName', \"tableName\",\n 'name', \"name\",\n 'unique', \"unique\",\n 'isPrimaryKey', \"isPrimaryKey\",\n 'isReplicaIdentity', \"isReplicaIdentity\",\n 'isImmediate', \"isImmediate\",\n 'predicateSQL', \"predicateSQL\",\n 'columns', json_object_agg(\"col\", \"dir\")\n ) AS index FROM indexed_columns \n GROUP BY \"schema\", \"tableName\", \"name\", \"unique\", \n \"isPrimaryKey\", \"isReplicaIdentity\", \"isImmediate\", \"predicateSQL\")\n\n SELECT json_build_object(\n 'tables', COALESCE((SELECT json_agg(\"table\") FROM tables), '[]'::json),\n 'indexes', COALESCE((SELECT json_agg(\"index\") FROM indexes), '[]'::json)\n ) as \"publishedSchema\"\n `\n );\n}\n\nconst rawPublishedColumnSpec = publishedColumnSpec.extend({\n collationIsDeterministic: v.boolean().nullable().optional(),\n});\n\nconst rawPublishedTableSpec = publishedTableSpec.extend({\n columns: v.record(rawPublishedColumnSpec),\n});\n\nconst rawPublishedIndexSpec = publishedIndexSpec.extend({\n predicateSQL: v.string().nullable().optional(),\n});\n\nexport type SkippedIndexDiagnostic = {\n readonly schema: string;\n readonly index: string;\n readonly predicate: string;\n readonly reason: UnsupportedPredicateReason;\n};\n\nconst skippedIndexDiagnostics = new WeakMap<\n object,\n readonly SkippedIndexDiagnostic[]\n>();\nexport function getSkippedIndexDiagnostics(\n schema: PublishedSchema,\n): readonly SkippedIndexDiagnostic[] {\n return skippedIndexDiagnostics.get(schema) ?? [];\n}\n\nexport function warnForSkippedIndexes(\n lc: LogContext,\n schema: PublishedSchema,\n warned: Set<string> = new Set(),\n): void {\n for (const diagnostic of getSkippedIndexDiagnostics(schema)) {\n const key = JSON.stringify(diagnostic);\n if (warned.has(key)) continue;\n warned.add(key);\n lc.warn?.('Skipping unsupported PostgreSQL partial index', diagnostic);\n getOrCreateCounter('replication', 'skipped_index', {\n description: 'Number of PostgreSQL indexes skipped during replication.',\n unit: '{index}',\n }).add(1, {reason: diagnostic.reason, partial: true});\n }\n}\n\nexport const publishedSchema = v\n .object({\n tables: v.array(rawPublishedTableSpec),\n indexes: v.array(rawPublishedIndexSpec),\n })\n .map(({tables: rawTables, indexes: rawIndexes}) => {\n const diagnostics: SkippedIndexDiagnostic[] = [];\n const indexes: PublishedIndexSpec[] = [];\n for (const rawIndex of rawIndexes) {\n const {predicateSQL, ...index} = rawIndex;\n if (predicateSQL !== null && predicateSQL !== undefined) {\n const table = rawTables.find(\n candidate =>\n candidate.schema === index.schema &&\n candidate.name === index.tableName,\n );\n const translation = table\n ? translatePostgresIndexPredicate(predicateSQL, table)\n : {supported: false as const, reason: 'unpublished-column' as const};\n if (!translation.supported) {\n diagnostics.push({\n schema: index.schema,\n index: index.name,\n predicate: predicateSQL,\n reason: translation.reason,\n });\n continue;\n }\n indexes.push({...index, predicate: translation.predicate});\n } else {\n indexes.push(index);\n }\n }\n\n const tables = rawTables.map(table => ({\n ...table,\n columns: Object.fromEntries(\n Object.entries(table.columns).map(\n ([name, {collationIsDeterministic: _, ...column}]) => [name, column],\n ),\n ),\n }));\n const zqlSpecs = computeZqlSpecsFromLiteSpecs(\n tables.map(t => mapPostgresToLite(t)),\n indexes.map(mapPostgresToLiteIndex),\n {includeBackfillingColumns: true},\n );\n const result = {\n indexes,\n\n // Denormalize the schema such that each `table` includes the\n // `replicaIdentityColumns` corresponding to the table's\n // replica identity and associated primary key or index.\n tables: tables.map(table => {\n const replicaIdentityColumns: string[] = [];\n switch (table.replicaIdentity) {\n case 'd':\n replicaIdentityColumns.push(...(table.primaryKey ?? []));\n break;\n case 'i':\n replicaIdentityColumns.push(\n ...Object.keys(\n indexes.find(\n ind =>\n ind.schema === table.schema &&\n ind.tableName === table.name &&\n ind.isReplicaIdentity,\n )?.columns ?? {},\n ),\n );\n break;\n case 'f': {\n // For the key columns of replica identity \"full\", use the columns\n // that the replicator/change-processor will end up using\n // (in #getKey()) as the row key.\n const zqlTable = zqlSpecs.get(liteTableName(table));\n // Note: There zql spec will be absent if the table is not synced,\n // e.g. if it has no suitable unique indexes.\n if (zqlTable) {\n replicaIdentityColumns.push(...zqlTable.tableSpec.primaryKey);\n }\n break;\n }\n }\n return {\n ...table,\n replicaIdentityColumns,\n };\n }),\n };\n skippedIndexDiagnostics.set(result, diagnostics);\n return result;\n });\n\nexport type PublishedSchema = v.Infer<typeof publishedSchema>;\n\nexport type PublishedTableWithReplicaIdentity =\n PublishedSchema['tables'][number];\n\nconst publicationSchema = v.object({\n pubname: v.string(),\n pubinsert: v.boolean(),\n pubupdate: v.boolean(),\n pubdelete: v.boolean(),\n pubtruncate: v.boolean(),\n});\n\nconst publicationsResultSchema = v.array(publicationSchema);\n\nexport type PublicationInfo = PublishedSchema & {\n publications: v.Infer<typeof publicationsResultSchema>;\n};\n\n/**\n * Retrieves published tables and columns.\n */\nexport async function getPublicationInfo(\n sql: postgres.Sql,\n publications: string[],\n): Promise<PublicationInfo> {\n const result = await sql.unsafe(/*sql*/ `\n SELECT \n schemaname AS \"schema\",\n tablename AS \"table\", \n json_object_agg(pubname, attnames) AS \"publications\"\n FROM pg_publication_tables pb\n WHERE pb.pubname IN (${literal(publications)})\n GROUP BY schemaname, tablename;\n\n SELECT ${Object.keys(publicationSchema.shape).join(\n ',',\n )} FROM pg_publication pb\n WHERE pb.pubname IN (${literal(publications)})\n ORDER BY pubname;\n\n ${publishedSchemaQuery(publications)};\n`);\n\n // The first query is used to check that tables in multiple publications\n // always publish the same set of columns.\n const publishedColumns = result[0] as {\n schema: string;\n table: string;\n publications: Record<string, string[]>;\n }[];\n for (const {table, publications} of publishedColumns) {\n let expected: Set<string>;\n Object.entries(publications).forEach(([_, columns], i) => {\n const cols = new Set(columns);\n if (i === 0) {\n expected = cols;\n } else if (!equals(expected, cols)) {\n throw new Error(\n `Table ${table} is exported with different columns: [${[\n ...expected,\n ]}] vs [${[...cols]}]`,\n );\n }\n });\n }\n\n assert(\n result[2][0].publishedSchema,\n () => `Invalid publishedSchema result ${BigIntJSON.stringify(result[2])}`,\n );\n\n const schema = v.parse(result[2][0].publishedSchema, publishedSchema);\n const info = {\n publications: v.parse(result[1], publicationsResultSchema),\n ...schema,\n };\n skippedIndexDiagnostics.set(info, getSkippedIndexDiagnostics(schema));\n return info;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"published.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/published.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {literal} from 'pg-format';\nimport type postgres from 'postgres';\nimport {assert} from '../../../../../../shared/src/asserts.ts';\nimport {BigIntJSON} from '../../../../../../shared/src/bigint-json.ts';\nimport {equals} from '../../../../../../shared/src/set-utils.ts';\nimport * as v from '../../../../../../shared/src/valita.ts';\nimport {computeZqlSpecsFromLiteSpecs} from '../../../../db/lite-tables.ts';\nimport {\n mapPostgresToLite,\n mapPostgresToLiteIndex,\n} from '../../../../db/pg-to-lite.ts';\nimport {\n publishedColumnSpec,\n publishedIndexSpec,\n publishedTableSpec,\n type PublishedIndexSpec,\n} from '../../../../db/specs.ts';\nimport {getOrCreateCounter} from '../../../../observability/metrics.ts';\nimport {liteTableName} from '../../../../types/names.ts';\nimport {\n translatePostgresIndexPredicate,\n type UnsupportedPredicateReason,\n} from './index-predicate.ts';\n\nexport function publishedSchemaQuery(\n publications: readonly string[],\n includePartialIndexes = true,\n) {\n // Notes:\n // * There's a bug in PG15 in which generated columns are incorrectly\n // included in pg_publication_tables.attnames, (even though the generated\n // column values are not be included in the replication stream).\n // The WHERE condition `attgenerated = ''` fixes this by explicitly excluding\n // generated columns from the list.\n return (\n /*sql*/ `\nWITH published_columns AS (SELECT \n pc.oid::int8 AS \"oid\",\n nspname AS \"schema\",\n pc.relnamespace::int8 AS \"schemaOID\" ,\n pc.relname AS \"name\", \n pc.relreplident AS \"replicaIdentity\",\n attnum AS \"pos\", \n attname AS \"col\", \n pt.typname AS \"type\", \n atttypid::int8 AS \"typeOID\", \n pt.typtype,\n elem_pt.typtype AS \"elemTyptype\",\n coll.collisdeterministic AS \"collationIsDeterministic\",\n NULLIF(atttypmod, -1) AS \"maxLen\", \n attndims \"arrayDims\", \n attnotnull AS \"notNull\",\n pg_get_expr(pd.adbin, pd.adrelid) as \"dflt\",\n NULLIF(ARRAY_POSITION(conkey, attnum), -1) AS \"keyPos\", \n pb.rowfilter as \"rowFilter\",\n pb.pubname as \"publication\"\nFROM pg_attribute\nJOIN pg_class pc ON pc.oid = attrelid\nJOIN pg_namespace pns ON pns.oid = relnamespace\nJOIN pg_type pt ON atttypid = pt.oid\nLEFT JOIN pg_type elem_pt ON elem_pt.oid = pt.typelem\nLEFT JOIN pg_collation coll ON coll.oid = pg_attribute.attcollation\nJOIN pg_publication_tables as pb ON \n pb.schemaname = nspname AND \n pb.tablename = pc.relname AND\n attname = ANY(pb.attnames)\nLEFT JOIN pg_constraint pk ON pk.contype = 'p' AND pk.connamespace = relnamespace AND pk.conrelid = attrelid\nLEFT JOIN pg_attrdef pd ON pd.adrelid = attrelid AND pd.adnum = attnum\nWHERE pb.pubname IN (${literal(publications)}) AND \n (current_setting('server_version_num')::int >= 160000 OR attgenerated = '')\nORDER BY nspname, pc.relname),\n\ntables AS (SELECT json_build_object(\n 'oid', \"oid\",\n 'schema', \"schema\", \n 'schemaOID', \"schemaOID\",\n 'name', \"name\", \n 'replicaIdentity', \"replicaIdentity\",\n 'columns', json_object_agg(\n DISTINCT\n col,\n jsonb_build_object(\n 'pos', \"pos\",\n 'dataType', CASE WHEN \"arrayDims\" = 0 \n THEN \"type\" \n ELSE substring(\"type\" from 2) || repeat('[]', \"arrayDims\") END,\n 'pgTypeClass', \"typtype\",\n 'elemPgTypeClass', \"elemTyptype\",\n 'typeOID', \"typeOID\",\n 'collationIsDeterministic', \"collationIsDeterministic\",\n -- https://stackoverflow.com/a/52376230\n 'characterMaximumLength', CASE WHEN \"typeOID\" = 1043 OR \"typeOID\" = 1042 \n THEN \"maxLen\" - 4 \n ELSE \"maxLen\" END,\n 'notNull', \"notNull\",\n 'dflt', \"dflt\"\n )\n ),\n 'primaryKey', ARRAY( SELECT json_object_keys(\n json_strip_nulls(\n json_object_agg(\n DISTINCT \"col\", \"keyPos\" ORDER BY \"keyPos\"\n )\n )\n )),\n 'publications', json_object_agg(\n DISTINCT \n \"publication\", \n jsonb_build_object('rowFilter', \"rowFilter\")\n )\n) AS \"table\" FROM published_columns \n GROUP BY \"schema\", \"schemaOID\", \"name\", \"oid\", \"replicaIdentity\"),\n ` +\n // Note: pg_attribute contains column names for tables and for indexes.\n // However, the latter does not get updated when a column in a table is\n // renamed.\n //\n // https://www.postgresql.org/message-id/5860814f-c91d-4ab0-b771-ded90d7b9c55%40www.fastmail.com\n //\n // To address this, the pg_attribute rows are looked up for the index's\n // table rather than the index itself, using the pg_index.indkey array\n // to determine the set and order of columns to include.\n //\n // Notes:\n // * The first bit of indoption is 1 for DESC and 0 for ASC:\n // https://github.com/postgres/postgres/blob/4e1fad37872e49a711adad5d9870516e5c71a375/src/include/catalog/pg_index.h#L89\n // * pg_index.indkey is an int2vector which is 0-based instead of 1-based.\n // * The additional check for attgenerated is required for the aforementioned\n // (in publishedTableQuery) bug in PG15 in which generated columns are\n // incorrectly included in pg_publication_tables.attnames\n /*sql*/ `\n indexed_columns AS (SELECT\n pg_indexes.schemaname as \"schema\",\n pg_indexes.tablename as \"tableName\",\n pg_indexes.indexname as \"name\",\n index_column.name as \"col\",\n CASE WHEN pg_index.indoption[index_column.pos-1] & 1 = 1 THEN 'DESC' ELSE 'ASC' END as \"dir\",\n -- Partial indexes are always reported as non-unique. Nothing on the\n -- replica relies on the uniqueness of a partial index (it cannot serve\n -- as a row key), and reporting it as non-unique keeps the ddl event\n -- format compatible with older versions of the zero-cache, which\n -- ignore \"predicateSQL\" and would otherwise create a full UNIQUE index\n -- on the replica.\n (pg_index.indisunique AND pg_index.indpred IS NULL) as \"unique\",\n pg_index.indisprimary as \"isPrimaryKey\",\n pg_index.indisreplident as \"isReplicaIdentity\",\n pg_index.indimmediate as \"isImmediate\",\n pg_get_expr(pg_index.indpred, pg_index.indrelid, false) as \"predicateSQL\"\n FROM pg_indexes\n JOIN pg_namespace ON pg_indexes.schemaname = pg_namespace.nspname\n JOIN pg_class pc ON\n pc.relname = pg_indexes.indexname\n AND pc.relnamespace = pg_namespace.oid\n JOIN pg_publication_tables as pb ON \n pb.schemaname = pg_indexes.schemaname AND \n pb.tablename = pg_indexes.tablename\n JOIN pg_index ON pg_index.indexrelid = pc.oid\n JOIN LATERAL (\n SELECT array_agg(attname) as attnames, array_agg(attgenerated != '') as generated FROM pg_attribute\n WHERE attrelid = pg_index.indrelid\n AND attnum = ANY( (pg_index.indkey::smallint[] )[:pg_index.indnkeyatts - 1] )\n ) as indexed ON true\n JOIN LATERAL (\n SELECT pg_attribute.attname as name, col.index_pos as pos\n FROM UNNEST( (pg_index.indkey::smallint[])[:pg_index.indnkeyatts - 1] ) \n WITH ORDINALITY as col(table_pos, index_pos)\n JOIN pg_attribute ON attrelid = pg_index.indrelid AND attnum = col.table_pos\n ) AS index_column ON true\n LEFT JOIN pg_constraint ON pg_constraint.conindid = pc.oid\n WHERE pb.pubname IN (${literal(publications)})\n AND pg_index.indexprs IS NULL\n AND (${includePartialIndexes} OR pg_index.indpred IS NULL)\n AND (pg_constraint.contype IS NULL OR pg_constraint.contype IN ('p', 'u'))\n AND indexed.attnames <@ pb.attnames\n AND (current_setting('server_version_num')::int >= 160000 OR false = ALL(indexed.generated))\n ORDER BY\n pg_indexes.schemaname,\n pg_indexes.tablename,\n pg_indexes.indexname,\n index_column.pos ASC),\n \n indexes AS (SELECT json_build_object(\n 'schema', \"schema\",\n 'tableName', \"tableName\",\n 'name', \"name\",\n 'unique', \"unique\",\n 'isPrimaryKey', \"isPrimaryKey\",\n 'isReplicaIdentity', \"isReplicaIdentity\",\n 'isImmediate', \"isImmediate\",\n 'predicateSQL', \"predicateSQL\",\n 'columns', json_object_agg(\"col\", \"dir\")\n ) AS index FROM indexed_columns \n GROUP BY \"schema\", \"tableName\", \"name\", \"unique\", \n \"isPrimaryKey\", \"isReplicaIdentity\", \"isImmediate\", \"predicateSQL\")\n\n SELECT json_build_object(\n 'tables', COALESCE((SELECT json_agg(\"table\") FROM tables), '[]'::json),\n 'indexes', COALESCE((SELECT json_agg(\"index\") FROM indexes), '[]'::json)\n ) as \"publishedSchema\"\n `\n );\n}\n\nconst rawPublishedColumnSpec = publishedColumnSpec.extend({\n collationIsDeterministic: v.boolean().nullable().optional(),\n});\n\nconst rawPublishedTableSpec = publishedTableSpec.extend({\n columns: v.record(rawPublishedColumnSpec),\n});\n\nconst rawPublishedIndexSpec = publishedIndexSpec.extend({\n predicateSQL: v.string().nullable().optional(),\n});\n\nexport type SkippedIndexDiagnostic = {\n readonly schema: string;\n readonly index: string;\n readonly predicate: string;\n readonly reason: UnsupportedPredicateReason;\n};\n\nconst skippedIndexDiagnostics = new WeakMap<\n object,\n readonly SkippedIndexDiagnostic[]\n>();\nexport function getSkippedIndexDiagnostics(\n schema: PublishedSchema,\n): readonly SkippedIndexDiagnostic[] {\n return skippedIndexDiagnostics.get(schema) ?? [];\n}\n\nexport function warnForSkippedIndexes(\n lc: LogContext,\n schema: PublishedSchema,\n warned: Set<string> = new Set(),\n): void {\n for (const diagnostic of getSkippedIndexDiagnostics(schema)) {\n const key = JSON.stringify(diagnostic);\n if (warned.has(key)) continue;\n warned.add(key);\n lc.warn?.('Skipping unsupported PostgreSQL partial index', diagnostic);\n getOrCreateCounter('replication', 'skipped_index', {\n description: 'Number of PostgreSQL indexes skipped during replication.',\n unit: '{index}',\n }).add(1, {reason: diagnostic.reason, partial: true});\n }\n}\n\nexport const publishedSchema = v\n .object({\n tables: v.array(rawPublishedTableSpec),\n indexes: v.array(rawPublishedIndexSpec),\n })\n .map(({tables: rawTables, indexes: rawIndexes}) => {\n const diagnostics: SkippedIndexDiagnostic[] = [];\n const indexes: PublishedIndexSpec[] = [];\n for (const rawIndex of rawIndexes) {\n const {predicateSQL, ...index} = rawIndex;\n if (predicateSQL !== null && predicateSQL !== undefined) {\n const table = rawTables.find(\n candidate =>\n candidate.schema === index.schema &&\n candidate.name === index.tableName,\n );\n const translation = table\n ? translatePostgresIndexPredicate(predicateSQL, table)\n : {supported: false as const, reason: 'unpublished-column' as const};\n if (!translation.supported) {\n diagnostics.push({\n schema: index.schema,\n index: index.name,\n predicate: predicateSQL,\n reason: translation.reason,\n });\n continue;\n }\n indexes.push({...index, predicate: translation.predicate});\n } else {\n indexes.push(index);\n }\n }\n\n const tables = rawTables.map(table => ({\n ...table,\n columns: Object.fromEntries(\n Object.entries(table.columns).map(\n ([name, {collationIsDeterministic: _, ...column}]) => [name, column],\n ),\n ),\n }));\n const zqlSpecs = computeZqlSpecsFromLiteSpecs(\n tables.map(t => mapPostgresToLite(t)),\n indexes.map(mapPostgresToLiteIndex),\n {includeBackfillingColumns: true},\n );\n const result = {\n indexes,\n\n // Denormalize the schema such that each `table` includes the\n // `replicaIdentityColumns` corresponding to the table's\n // replica identity and associated primary key or index.\n tables: tables.map(table => {\n const replicaIdentityColumns: string[] = [];\n switch (table.replicaIdentity) {\n case 'd':\n replicaIdentityColumns.push(...(table.primaryKey ?? []));\n break;\n case 'i':\n replicaIdentityColumns.push(\n ...Object.keys(\n indexes.find(\n ind =>\n ind.schema === table.schema &&\n ind.tableName === table.name &&\n ind.isReplicaIdentity,\n )?.columns ?? {},\n ),\n );\n break;\n case 'f': {\n // For the key columns of replica identity \"full\", use the columns\n // that the replicator/change-processor will end up using\n // (in #getKey()) as the row key.\n const zqlTable = zqlSpecs.get(liteTableName(table));\n // Note: There zql spec will be absent if the table is not synced,\n // e.g. if it has no suitable unique indexes.\n if (zqlTable) {\n replicaIdentityColumns.push(...zqlTable.tableSpec.primaryKey);\n }\n break;\n }\n }\n return {\n ...table,\n replicaIdentityColumns,\n };\n }),\n };\n skippedIndexDiagnostics.set(result, diagnostics);\n return result;\n });\n\nexport type PublishedSchema = v.Infer<typeof publishedSchema>;\n\nexport type PublishedTableWithReplicaIdentity =\n PublishedSchema['tables'][number];\n\nconst publicationSchema = v.object({\n pubname: v.string(),\n pubinsert: v.boolean(),\n pubupdate: v.boolean(),\n pubdelete: v.boolean(),\n pubtruncate: v.boolean(),\n});\n\nconst publicationsResultSchema = v.array(publicationSchema);\n\nexport type PublicationInfo = PublishedSchema & {\n publications: v.Infer<typeof publicationsResultSchema>;\n};\n\n/**\n * Retrieves published tables and columns.\n */\nexport async function getPublicationInfo(\n sql: postgres.Sql,\n publications: string[],\n): Promise<PublicationInfo> {\n const result = await sql.unsafe(/*sql*/ `\n SELECT \n schemaname AS \"schema\",\n tablename AS \"table\", \n json_object_agg(pubname, attnames) AS \"publications\"\n FROM pg_publication_tables pb\n WHERE pb.pubname IN (${literal(publications)})\n GROUP BY schemaname, tablename;\n\n SELECT ${Object.keys(publicationSchema.shape).join(\n ',',\n )} FROM pg_publication pb\n WHERE pb.pubname IN (${literal(publications)})\n ORDER BY pubname;\n\n ${publishedSchemaQuery(publications)};\n`);\n\n // The first query is used to check that tables in multiple publications\n // always publish the same set of columns.\n const publishedColumns = result[0] as {\n schema: string;\n table: string;\n publications: Record<string, string[]>;\n }[];\n for (const {table, publications} of publishedColumns) {\n let expected: Set<string>;\n Object.entries(publications).forEach(([_, columns], i) => {\n const cols = new Set(columns);\n if (i === 0) {\n expected = cols;\n } else if (!equals(expected, cols)) {\n throw new Error(\n `Table ${table} is exported with different columns: [${[\n ...expected,\n ]}] vs [${[...cols]}]`,\n );\n }\n });\n }\n\n assert(\n result[2][0].publishedSchema,\n () => `Invalid publishedSchema result ${BigIntJSON.stringify(result[2])}`,\n );\n\n const schema = v.parse(result[2][0].publishedSchema, publishedSchema);\n const info = {\n publications: v.parse(result[1], publicationsResultSchema),\n ...schema,\n };\n skippedIndexDiagnostics.set(info, getSkippedIndexDiagnostics(schema));\n return info;\n}\n"],"mappings":"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"}
|
|
@@ -196,8 +196,8 @@ export declare function getInternalShardConfig(sql: PostgresDB, shard: ShardID):
|
|
|
196
196
|
* Sets up and returns all publications (including internal ones) for
|
|
197
197
|
* the given shard.
|
|
198
198
|
*/
|
|
199
|
-
export declare function setupTablesAndReplication(lc: LogContext, sql: PostgresTransaction, requested: ShardConfig): Promise<void>;
|
|
200
|
-
export declare function setupTriggers(lc: LogContext, tx: PostgresTransaction, shard: ShardConfig): Promise<void>;
|
|
199
|
+
export declare function setupTablesAndReplication(lc: LogContext, sql: PostgresTransaction, requested: ShardConfig, includePartialIndexes?: boolean): Promise<void>;
|
|
200
|
+
export declare function setupTriggers(lc: LogContext, tx: PostgresTransaction, shard: ShardConfig, includePartialIndexes?: boolean): Promise<void>;
|
|
201
201
|
export declare function validatePublications(lc: LogContext, published: PublicationInfo): void;
|
|
202
202
|
type ReplicaIdentities = {
|
|
203
203
|
apply(lc: LogContext, db: PostgresDB): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shard.d.ts","sourceRoot":"","sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/shard.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"shard.d.ts","sourceRoot":"","sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/shard.ts"],"names":[],"mappings":"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"}
|
|
@@ -401,7 +401,7 @@ async function getInternalShardConfig(sql, shard) {
|
|
|
401
401
|
* Sets up and returns all publications (including internal ones) for
|
|
402
402
|
* the given shard.
|
|
403
403
|
*/
|
|
404
|
-
async function setupTablesAndReplication(lc, sql, requested) {
|
|
404
|
+
async function setupTablesAndReplication(lc, sql, requested, includePartialIndexes = true) {
|
|
405
405
|
const { publications } = requested;
|
|
406
406
|
for (const pub of publications) {
|
|
407
407
|
validatePublicationName(pub);
|
|
@@ -431,12 +431,12 @@ async function setupTablesAndReplication(lc, sql, requested) {
|
|
|
431
431
|
};
|
|
432
432
|
await sql.unsafe(globalSetup(shard) + shardSetup(shard, metadataPublication));
|
|
433
433
|
await replicaIdentitiesForTablesWithoutPrimaryKeys(await getPublicationInfo(sql, allPublications))?.apply(lc, sql);
|
|
434
|
-
await setupTriggers(lc, sql, shard);
|
|
434
|
+
await setupTriggers(lc, sql, shard, includePartialIndexes);
|
|
435
435
|
}
|
|
436
|
-
async function setupTriggers(lc, tx, shard) {
|
|
436
|
+
async function setupTriggers(lc, tx, shard, includePartialIndexes = true) {
|
|
437
437
|
const [{ ddlDetection }] = await tx`
|
|
438
438
|
SELECT "ddlDetection" FROM ${tx(upstreamSchema(shard))}."shardConfig"`;
|
|
439
|
-
await tx.unsafe(createEventFunctionStatements(shard));
|
|
439
|
+
await tx.unsafe(createEventFunctionStatements(shard, includePartialIndexes));
|
|
440
440
|
try {
|
|
441
441
|
await tx.savepoint((sub) => sub.unsafe(triggerSetup(shard)));
|
|
442
442
|
} catch (e) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shard.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/shard.ts"],"sourcesContent":["import {PG_INSUFFICIENT_PRIVILEGE} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {literal} from 'pg-format';\nimport postgres from 'postgres';\nimport {assert} from '../../../../../../shared/src/asserts.ts';\nimport {\n jsonObjectSchema,\n stringify,\n type JSONObject,\n} from '../../../../../../shared/src/bigint-json.ts';\nimport * as v from '../../../../../../shared/src/valita.ts';\nimport {Default} from '../../../../db/postgres-replica-identity-enum.ts';\nimport type {PostgresDB, PostgresTransaction} from '../../../../types/pg.ts';\nimport type {AppID, ShardConfig, ShardID} from '../../../../types/shards.ts';\nimport {appSchema, check, upstreamSchema} from '../../../../types/shards.ts';\nimport {id} from '../../../../types/sql.ts';\nimport {\n createEventFunctionStatements,\n createEventTriggerStatements,\n} from './ddl.ts';\nimport {\n getPublicationInfo,\n publishedSchema,\n type PublicationInfo,\n type PublishedSchema,\n} from './published.ts';\nimport {\n InitialSync,\n replicaStageSchema,\n type ReplicaStage,\n} from './replica-stage-enum.ts';\nimport {validate} from './validation.ts';\n\n/**\n * PostgreSQL unquoted identifiers must start with a letter or underscore\n * and contain only letters, digits, and underscores.\n */\nconst VALID_PUBLICATION_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/**\n * Validates that a publication name is a valid PostgreSQL identifier.\n * This provides defense-in-depth against SQL injection when publication\n * names are used in replication commands.\n */\nexport function validatePublicationName(name: string): void {\n if (!VALID_PUBLICATION_NAME.test(name)) {\n throw new Error(\n `Invalid publication name \"${name}\". Publication names must start with a letter or underscore ` +\n `and contain only letters, digits, and underscores.`,\n );\n }\n if (name.length > 63) {\n throw new Error(\n `Publication name \"${name}\" exceeds PostgreSQL's 63-character identifier limit.`,\n );\n }\n}\n\nexport function internalPublicationPrefix({appID}: AppID) {\n return `_${appID}_`;\n}\n\nexport function legacyReplicationSlot({appID, shardNum}: ShardID) {\n return `${appID}_${shardNum}`;\n}\n\nexport function replicationSlotPrefix(shard: ShardID) {\n const {appID, shardNum} = check(shard);\n return `${appID}_${shardNum}_`;\n}\n\n/**\n * An expression used to match replication slots in the shard\n * in a Postgres `LIKE` operator.\n */\nexport function replicationSlotExpression(shard: ShardID) {\n // Underscores have a special meaning in LIKE values\n // so they have to be escaped.\n return `${replicationSlotPrefix(shard)}%`.replaceAll('_', '\\\\_');\n}\n\nfunction defaultPublicationName(appID: string, shardID: string | number) {\n return `_${appID}_public_${shardID}`;\n}\n\nexport function metadataPublicationName(\n appID: string,\n shardID: string | number,\n) {\n return `_${appID}_metadata_${shardID}`;\n}\n\n// The GLOBAL_SETUP must be idempotent as it can be run multiple times for different shards.\nfunction globalSetup(appID: AppID): string {\n const app = id(appSchema(appID));\n\n return /*sql*/ `\n CREATE SCHEMA IF NOT EXISTS ${app};\n\n CREATE TABLE IF NOT EXISTS ${app}.permissions (\n \"permissions\" JSONB,\n \"hash\" TEXT,\n\n -- Ensure that there is only a single row in the table.\n -- Application code can be agnostic to this column, and\n -- simply invoke UPDATE statements on the version columns.\n \"lock\" BOOL PRIMARY KEY DEFAULT true CHECK (lock)\n );\n\n CREATE OR REPLACE FUNCTION ${app}.set_permissions_hash()\n RETURNS TRIGGER AS $$\n BEGIN\n NEW.hash = md5(NEW.permissions::text);\n RETURN NEW;\n END;\n $$ LANGUAGE plpgsql;\n\n CREATE OR REPLACE TRIGGER on_set_permissions \n BEFORE INSERT OR UPDATE ON ${app}.permissions\n FOR EACH ROW\n EXECUTE FUNCTION ${app}.set_permissions_hash();\n\n INSERT INTO ${app}.permissions (permissions) VALUES (NULL) ON CONFLICT DO NOTHING;\n`;\n}\n\nexport async function ensureGlobalTables(db: PostgresDB, appID: AppID) {\n await db.unsafe(globalSetup(appID));\n}\n\nexport function getClientsTableDefinition(schema: string) {\n return /*sql*/ `\n CREATE TABLE ${schema}.\"clients\" (\n \"clientGroupID\" TEXT NOT NULL,\n \"clientID\" TEXT NOT NULL,\n \"lastMutationID\" BIGINT NOT NULL,\n \"userID\" TEXT,\n PRIMARY KEY(\"clientGroupID\", \"clientID\")\n );`;\n}\n\n/**\n * Tracks the results of mutations.\n * 1. It is an error for the same mutation ID to be used twice.\n * 2. The result is JSONB to allow for arbitrary results.\n *\n * The tables must be cleaned up as the clients\n * receive the mutation responses and as clients are removed.\n */\nexport function getMutationsTableDefinition(schema: string) {\n return /*sql*/ `\n CREATE TABLE ${schema}.\"mutations\" (\n \"clientGroupID\" TEXT NOT NULL,\n \"clientID\" TEXT NOT NULL,\n \"mutationID\" BIGINT NOT NULL,\n \"result\" JSON NOT NULL,\n PRIMARY KEY(\"clientGroupID\", \"clientID\", \"mutationID\")\n );`;\n}\n\nexport const SHARD_CONFIG_TABLE = 'shardConfig';\n\nexport function shardSetup(\n shardConfig: ShardConfig,\n metadataPublication: string,\n): string {\n const app = id(appSchema(shardConfig));\n const shard = id(upstreamSchema(shardConfig));\n\n const pubs = shardConfig.publications.toSorted();\n assert(\n pubs.includes(metadataPublication),\n () => `Publications must include ${metadataPublication}`,\n );\n\n return /*sql*/ `\n CREATE SCHEMA IF NOT EXISTS ${shard};\n\n ${getClientsTableDefinition(shard)}\n ${getMutationsTableDefinition(shard)}\n\n CREATE TABLE ${shard}.\"${SHARD_CONFIG_TABLE}\" (\n \"publications\" TEXT[] NOT NULL,\n \"ddlDetection\" BOOL NOT NULL,\n\n -- Ensure that there is only a single row in the table.\n \"lock\" BOOL PRIMARY KEY DEFAULT true CHECK (lock)\n );\n\n INSERT INTO ${shard}.\"${SHARD_CONFIG_TABLE}\" (\n \"publications\",\n \"ddlDetection\" \n ) VALUES (\n ARRAY[${literal(pubs)}], \n false -- set in SAVEPOINT with triggerSetup() statements\n );\n\n CREATE TABLE ${shard}.replicas (\n -- The DEFAULT exists purely for backwards compatibility support.\n -- New code always specifies a value based on Date.now().\n \"id\" TEXT PRIMARY KEY DEFAULT replace(gen_random_uuid()::text, '-', ''),\n \"rank\" BIGSERIAL,\n \"slot\" TEXT NOT NULL,\n \"version\" TEXT, -- replaced by generation, slated for deletion\n \"epoch\" INT4 DEFAULT 0,\n \"generation\" TEXT NOT NULL,\n \"stage\" INT4 DEFAULT ${InitialSync},\n \"backupPath\" TEXT, -- subpath within the litestream backup URL\n \"backupV5\" BOOL DEFAULT false,\n -- Whether the slot is acked only after storing in the PG change log.\n \"pgChangeLog\" BOOL DEFAULT false,\n \"initialSchema\" JSON, -- set after initial sync\n \"initialSyncContext\" JSON,\n \"subscriberContext\" JSON\n );\n\n DROP PUBLICATION IF EXISTS ${id(metadataPublication)};\n CREATE PUBLICATION ${id(metadataPublication)}\n FOR TABLE ${app}.\"permissions\", \n TABLE ${shard}.\"clients\", \n ${shard}.\"mutations\", \n ${shard}.\"replicas\" (id); -- advance the LSN/version with new replicas\n `;\n}\n\nexport function dropShard(appID: string, shardID: string | number): string {\n const schema = `${appID}_${shardID}`;\n const metadataPublication = metadataPublicationName(appID, shardID);\n const defaultPublication = defaultPublicationName(appID, shardID);\n\n // DROP SCHEMA ... CASCADE does not drop dependent PUBLICATIONS,\n // so PUBLICATIONs must be dropped explicitly.\n return /*sql*/ `\n DROP PUBLICATION IF EXISTS ${id(defaultPublication)};\n DROP PUBLICATION IF EXISTS ${id(metadataPublication)};\n DROP SCHEMA IF EXISTS ${id(schema)} CASCADE;\n `;\n}\n\nconst internalShardConfigSchema = v.object({\n publications: v.array(v.string()),\n ddlDetection: v.boolean(),\n});\n\nexport type InternalShardConfig = v.Infer<typeof internalShardConfigSchema>;\n\nconst replicaInfoSchema = v.object({\n id: v.string(),\n rank: v.bigint(),\n slot: v.string(),\n epoch: v.number(),\n generation: v.string(),\n stage: replicaStageSchema,\n backupPath: v.string().nullable(),\n backupV5: v.boolean(),\n /**\n * Whether the replication-manager that took over the replica's slot acks it\n * only after storing transactions in the PG change log (i.e. RMv1.5). The\n * slot's position past the change log's head then reflects acked\n * keepalives, rather than transactions missing from the change log.\n * False if unknown.\n */\n pgChangeLog: v.boolean(),\n});\n\nconst fullReplicaRowSchema = replicaInfoSchema.extend({\n initialSchema: publishedSchema,\n initialSyncContext: jsonObjectSchema.nullable(),\n subscriberContext: jsonObjectSchema.nullable(),\n});\n\nconst replicaSchema = internalShardConfigSchema.extend(\n fullReplicaRowSchema.shape,\n);\n\nexport type Replica = v.Infer<typeof replicaSchema>;\n\nconst replicationSlotStateSchema = v.object({\n active: v.boolean(),\n confirmedFlushLsn: v.string(),\n});\n\nconst replicaStateSchema = replicaInfoSchema.extend(\n replicationSlotStateSchema.shape,\n);\n\nexport type ReplicaState = v.Infer<typeof replicaStateSchema>;\n\n// triggerSetup is run separately in a sub-transaction (i.e. SAVEPOINT) so\n// that a failure (e.g. due to lack of superuser permissions) can be handled\n// by continuing in a degraded mode (ddlDetection = false).\nfunction triggerSetup(shard: ShardConfig): string {\n const schema = id(upstreamSchema(shard));\n return (\n createEventTriggerStatements(shard) +\n /*sql*/ `UPDATE ${schema}.\"shardConfig\" SET \"ddlDetection\" = true;`\n );\n}\n\nexport type BackupOptions = {\n backupPath: string | null;\n backupV5: boolean;\n /** See `pgChangeLog` in {@link ReplicaState}. */\n pgChangeLog: boolean;\n};\n\n/**\n * Creates a new replica to mark it as the owner of a specified `slot`.\n * This should be done with an advisory lock for replica/slot management\n * to ensure that the slot does not get dropped by concurrent cleanup\n * logic.\n *\n * Once initial sync is complete, {@link initInitialSyncReplica} should be called to\n * make the replica usable for incremental sync.\n */\nexport async function createReplica(\n sql: PostgresDB,\n shard: ShardID,\n id: string,\n slot: string,\n epoch: number,\n replicaVersion: string,\n {backupPath, backupV5, pgChangeLog}: BackupOptions,\n stage: ReplicaStage,\n) {\n const schema = upstreamSchema(shard);\n const values: Partial<v.Infer<typeof fullReplicaRowSchema>> & {\n version: string;\n } = {\n id,\n slot,\n epoch,\n generation: replicaVersion,\n // No longer read, but zero-cache 1.10 fails to start on a NULL \"version\",\n // so it is still written for rollbacks to 1.10.\n version: replicaVersion,\n backupPath,\n backupV5,\n pgChangeLog,\n stage,\n };\n await sql`INSERT INTO ${sql(schema)}.replicas ${sql(values)}`;\n}\n\n// Called in initial-sync to store the exact schema that was initially synced.\nexport async function initInitialSyncReplica(\n sql: PostgresDB,\n shard: ShardID,\n id: string,\n {tables, indexes}: PublishedSchema,\n initialSyncContext: JSONObject,\n) {\n const schema = upstreamSchema(shard);\n const synced: PublishedSchema = {tables, indexes};\n const values = {initialSchema: synced, initialSyncContext};\n await sql`UPDATE ${sql(schema)}.replicas SET ${sql(values)} WHERE id = ${id}`;\n}\n\n// Called to initialize a Restore replica (`destID`) with the generation\n// and initial sync context of the `sourceID` replica.\nexport async function initRestoreReplica(\n sql: PostgresDB,\n shard: ShardID,\n {sourceID, destID}: {sourceID: string; destID: string},\n): Promise<ReplicaState | null> {\n const schema = upstreamSchema(shard);\n await sql`\n UPDATE ${sql(schema)}.replicas dest \n SET \"generation\" = source.\"generation\",\n \"version\" = source.\"generation\",\n \"initialSchema\" = source.\"initialSchema\",\n \"initialSyncContext\" = source.\"initialSyncContext\"\n FROM ${sql(schema)}.replicas source\n WHERE source.id = ${sourceID} AND dest.id = ${destID};\n `;\n return getReplicaState(sql, shard, destID);\n}\n\n/**\n * Gets at the given version and optional ID. If no ID is specified, gets\n * the latest (i.e. highest rank) replica with that version.\n */\nexport async function getReplicaAtVersion(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n replicaVersion: string,\n id: string | null = null,\n context?: JSONObject,\n): Promise<Replica | null> {\n const schema = sql(upstreamSchema(shard));\n const result = await sql`\n SELECT\n replicas.\"id\",\n replicas.\"rank\",\n replicas.\"slot\",\n replicas.\"epoch\",\n replicas.\"generation\",\n replicas.\"stage\",\n replicas.\"backupPath\",\n replicas.\"backupV5\",\n replicas.\"pgChangeLog\",\n replicas.\"initialSchema\",\n replicas.\"initialSyncContext\",\n replicas.\"subscriberContext\",\n \"shardConfig\".\"publications\",\n \"shardConfig\".\"ddlDetection\"\n FROM ${schema}.replicas JOIN ${schema}.\"shardConfig\" ON true\n WHERE generation = ${replicaVersion} AND \"initialSyncContext\" IS NOT NULL\n AND id = COALESCE(${id}, id)\n ORDER BY rank DESC LIMIT 1;\n `;\n if (result.length === 0) {\n // log out all the replicas and the joined shardConfig\n const allReplicas = await sql`\n SELECT id, slot, generation, \"initialSyncContext\", \"subscriberContext\" \n FROM ${schema}.replicas`;\n lc.info?.(\n `Replica ${id}@${replicaVersion}` +\n (context ? `(context: ${stringify(context)}) ` : '') +\n `not found in: ${stringify(allReplicas)}`,\n );\n return null;\n }\n return v.parse(result[0], replicaSchema, 'passthrough');\n}\n\nexport async function getActiveReplicas(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n): Promise<ReplicaState[]> {\n const schema = sql(upstreamSchema(shard));\n const results = await sql`\n SELECT\n replicas.\"id\",\n replicas.\"rank\",\n replicas.\"slot\",\n replicas.\"epoch\",\n replicas.\"generation\",\n replicas.\"stage\",\n replicas.\"backupPath\",\n replicas.\"backupV5\",\n replicas.\"pgChangeLog\",\n slots.\"active\",\n slots.\"confirmed_flush_lsn\" as \"confirmedFlushLsn\"\n FROM ${schema}.replicas JOIN pg_replication_slots slots ON slot = slot_name\n WHERE \"initialSyncContext\" IS NOT NULL\n -- Exclude replicas whose slot has been invalidated (e.g. for exceeding\n -- max_slot_wal_keep_size). Such a replica can never be resumed, and\n -- restoring it just results in an AutoResetSignal on every attempt.\n AND slots.restart_lsn IS NOT NULL\n AND slots.wal_status IS DISTINCT FROM 'lost'\n ORDER BY generation DESC, confirmed_flush_lsn DESC;\n `;\n const replicas = v.parse(results, v.array(replicaStateSchema), 'passthrough');\n lc.info?.(`current replicas`, {replicas});\n return replicas;\n}\n\n/**\n * Returns candidates for restoring a new replica from. These include\n * all replicas in the newest `generation` of the given `epoch`, in order\n * of earliest available backup:\n * - An active InitialSync replica, which will be the only replica in\n * its generation by definition. Orphaned/inactive initial syncs are skipped.\n * - Replicas in the Replicate stage, active first.\n * - Replicas in the Restore stage, potentially available for resumption\n * as a last resort.\n */\nexport async function getRestoreCandidates(\n sql: PostgresDB,\n shard: ShardID,\n epoch: number,\n): Promise<ReplicaState[]> {\n const schema = sql(upstreamSchema(shard));\n const results = await sql`\n SELECT\n replicas.\"id\",\n replicas.\"rank\",\n replicas.\"slot\",\n replicas.\"epoch\",\n replicas.\"generation\",\n replicas.\"stage\",\n replicas.\"backupPath\",\n replicas.\"backupV5\",\n replicas.\"pgChangeLog\",\n slots.\"active\",\n slots.\"confirmed_flush_lsn\" as \"confirmedFlushLsn\"\n FROM ${schema}.replicas JOIN pg_replication_slots slots ON slot = slot_name\n\n -- coalesce on the newest generation in the epoch\n WHERE generation = (\n SELECT MAX(r.generation)\n FROM ${schema}.replicas r\n JOIN pg_replication_slots s ON r.slot = s.slot_name\n WHERE r.\"epoch\" = ${epoch} AND r.\"backupV5\"\n -- ignore orphaned InitialSyncs, which cannot be restored from\n AND (s.\"active\" OR r.\"stage\" != ${InitialSync})\n AND s.restart_lsn IS NOT NULL\n AND s.wal_status IS DISTINCT FROM 'lost'\n )\n AND \"epoch\" = ${epoch} AND \"backupV5\"\n AND (slots.active OR \"stage\" != ${InitialSync})\n AND slots.restart_lsn IS NOT NULL\n AND slots.wal_status IS DISTINCT FROM 'lost'\n\n ORDER BY \n stage ASC, -- ordered by backup availalibility\n active DESC, -- prefer active slots (fork over resume)\n confirmed_flush_lsn DESC; -- prefer replicas further ahead\n `;\n return v.parse(results, v.array(replicaStateSchema), 'passthrough');\n}\n\nexport async function getReplicaState(\n sql: PostgresDB,\n shard: ShardID,\n id: string,\n): Promise<ReplicaState | null> {\n const schema = sql(upstreamSchema(shard));\n const results = await sql`\n SELECT\n replicas.\"id\",\n replicas.\"rank\",\n replicas.\"slot\",\n replicas.\"epoch\",\n replicas.\"generation\",\n replicas.\"stage\",\n replicas.\"backupPath\",\n replicas.\"backupV5\",\n replicas.\"pgChangeLog\",\n slots.\"active\",\n slots.\"confirmed_flush_lsn\" as \"confirmedFlushLsn\"\n FROM ${schema}.replicas JOIN pg_replication_slots slots ON slot = slot_name\n WHERE \"id\" = ${id};\n `;\n return results.length\n ? v.parse(results[0], replicaStateSchema, 'passthrough')\n : null;\n}\n\nexport async function getInternalShardConfig(\n sql: PostgresDB,\n shard: ShardID,\n): Promise<InternalShardConfig> {\n const result = await sql`\n SELECT \"publications\", \"ddlDetection\"\n FROM ${sql(upstreamSchema(shard))}.\"shardConfig\";\n `;\n assert(\n result.length === 1,\n () => `Expected exactly one shardConfig row, got ${result.length}`,\n );\n return v.parse(result[0], internalShardConfigSchema, 'passthrough');\n}\n\n/**\n * Sets up and returns all publications (including internal ones) for\n * the given shard.\n */\nexport async function setupTablesAndReplication(\n lc: LogContext,\n sql: PostgresTransaction,\n requested: ShardConfig,\n) {\n const {publications} = requested;\n // Validate requested publications.\n for (const pub of publications) {\n validatePublicationName(pub);\n if (pub.startsWith('_')) {\n throw new Error(\n `Publication names starting with \"_\" are reserved for internal use.\\n` +\n `Please use a different name for publication \"${pub}\".`,\n );\n }\n }\n const allPublications: string[] = [];\n\n // Setup application publications.\n if (publications.length) {\n const results = await sql<{pubname: string}[]>`\n SELECT pubname from pg_publication WHERE pubname IN ${sql(\n publications,\n )}`.values();\n\n if (results.length !== publications.length) {\n throw new Error(\n `Unknown or invalid publications. Specified: [${publications}]. Found: [${results.flat()}]`,\n );\n }\n allPublications.push(...publications);\n } else {\n const defaultPublication = defaultPublicationName(\n requested.appID,\n requested.shardNum,\n );\n await sql`\n DROP PUBLICATION IF EXISTS ${sql(defaultPublication)}`;\n await sql`\n CREATE PUBLICATION ${sql(defaultPublication)} \n FOR TABLES IN SCHEMA public\n WITH (publish_via_partition_root = true)`;\n allPublications.push(defaultPublication);\n }\n\n const metadataPublication = metadataPublicationName(\n requested.appID,\n requested.shardNum,\n );\n allPublications.push(metadataPublication);\n\n const shard = {...requested, publications: allPublications};\n\n // Setup the global tables and shard tables / publications.\n await sql.unsafe(globalSetup(shard) + shardSetup(shard, metadataPublication));\n\n const pubs = await getPublicationInfo(sql, allPublications);\n await replicaIdentitiesForTablesWithoutPrimaryKeys(pubs)?.apply(lc, sql);\n\n await setupTriggers(lc, sql, shard);\n}\n\nexport async function setupTriggers(\n lc: LogContext,\n tx: PostgresTransaction,\n shard: ShardConfig,\n) {\n const schema = upstreamSchema(shard);\n const [{ddlDetection}] = await tx<InternalShardConfig[]> /*sql*/ `\n SELECT \"ddlDetection\" FROM ${tx(schema)}.\"shardConfig\"`;\n\n // The functions invoked by event triggers are installed even if\n // event triggers are not supported/allowed by the db provider.\n // This allows users to manually invoke the update_schemas() function\n // as a workaround.\n await tx.unsafe(createEventFunctionStatements(shard));\n try {\n await tx.savepoint(sub => sub.unsafe(triggerSetup(shard)));\n } catch (e) {\n if (ddlDetection) {\n // If ddlDetection has already been enabled, subsequent failures to\n // upgrade the trigger should be propagated rather than swallowed.\n throw e;\n }\n if (\n !(\n e instanceof postgres.PostgresError &&\n e.code === PG_INSUFFICIENT_PRIVILEGE\n )\n ) {\n throw e;\n }\n // If triggerSetup() fails, replication continues in ddlDetection=false mode.\n lc.warn?.(\n `Unable to create event triggers for schema change detection:\\n\\n` +\n `\"${e.hint ?? e.message}\"\\n\\n` +\n `Proceeding in degraded mode: schema changes will halt replication,\\n` +\n `requiring the replica to be reset (manually or with --auto-reset).`,\n );\n }\n}\n\nexport function validatePublications(\n lc: LogContext,\n published: PublicationInfo,\n) {\n // Verify that all publications export the proper events.\n published.publications.forEach(pub => {\n if (\n !pub.pubinsert ||\n !pub.pubupdate ||\n !pub.pubdelete ||\n !pub.pubtruncate\n ) {\n // TODO: Make APIError?\n throw new Error(\n `PUBLICATION ${pub.pubname} must publish insert, update, delete, and truncate`,\n );\n }\n });\n\n published.tables.forEach(table => validate(lc, table));\n}\n\ntype ReplicaIdentities = {\n apply(lc: LogContext, db: PostgresDB): Promise<void>;\n};\n\nexport function replicaIdentitiesForTablesWithoutPrimaryKeys(\n pubs: PublishedSchema,\n): ReplicaIdentities | undefined {\n const replicaIdentities: {\n schema: string;\n tableName: string;\n indexName: string;\n }[] = [];\n for (const table of pubs.tables) {\n if (!table.primaryKey?.length && table.replicaIdentity === Default) {\n // Look for an index that can serve as the REPLICA IDENTITY USING INDEX. It must be:\n // - UNIQUE\n // - NOT NULL columns\n // - not deferrable (i.e. isImmediate)\n // - not partial\n //\n // https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-REPLICA-IDENTITY\n const {schema, name: tableName} = table;\n for (const {columns, name: indexName} of pubs.indexes.filter(\n idx =>\n idx.schema === schema &&\n idx.tableName === tableName &&\n idx.unique &&\n idx.isImmediate &&\n idx.predicate === undefined,\n )) {\n if (Object.keys(columns).some(col => !table.columns[col].notNull)) {\n continue; // Only indexes with all NOT NULL columns are suitable.\n }\n replicaIdentities.push({schema, tableName, indexName});\n break;\n }\n }\n }\n\n if (replicaIdentities.length === 0) {\n return undefined;\n }\n return {\n apply: async (lc: LogContext, sql: PostgresDB) => {\n for (const {schema, tableName, indexName} of replicaIdentities) {\n lc.info?.(\n `setting \"${indexName}\" as the REPLICA IDENTITY for \"${tableName}\"`,\n );\n await sql`\n ALTER TABLE ${sql(schema)}.${sql(tableName)} \n REPLICA IDENTITY USING INDEX ${sql(indexName)}`;\n }\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"shard.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/shard.ts"],"sourcesContent":["import {PG_INSUFFICIENT_PRIVILEGE} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {literal} from 'pg-format';\nimport postgres from 'postgres';\nimport {assert} from '../../../../../../shared/src/asserts.ts';\nimport {\n jsonObjectSchema,\n stringify,\n type JSONObject,\n} from '../../../../../../shared/src/bigint-json.ts';\nimport * as v from '../../../../../../shared/src/valita.ts';\nimport {Default} from '../../../../db/postgres-replica-identity-enum.ts';\nimport type {PostgresDB, PostgresTransaction} from '../../../../types/pg.ts';\nimport type {AppID, ShardConfig, ShardID} from '../../../../types/shards.ts';\nimport {appSchema, check, upstreamSchema} from '../../../../types/shards.ts';\nimport {id} from '../../../../types/sql.ts';\nimport {\n createEventFunctionStatements,\n createEventTriggerStatements,\n} from './ddl.ts';\nimport {\n getPublicationInfo,\n publishedSchema,\n type PublicationInfo,\n type PublishedSchema,\n} from './published.ts';\nimport {\n InitialSync,\n replicaStageSchema,\n type ReplicaStage,\n} from './replica-stage-enum.ts';\nimport {validate} from './validation.ts';\n\n/**\n * PostgreSQL unquoted identifiers must start with a letter or underscore\n * and contain only letters, digits, and underscores.\n */\nconst VALID_PUBLICATION_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/**\n * Validates that a publication name is a valid PostgreSQL identifier.\n * This provides defense-in-depth against SQL injection when publication\n * names are used in replication commands.\n */\nexport function validatePublicationName(name: string): void {\n if (!VALID_PUBLICATION_NAME.test(name)) {\n throw new Error(\n `Invalid publication name \"${name}\". Publication names must start with a letter or underscore ` +\n `and contain only letters, digits, and underscores.`,\n );\n }\n if (name.length > 63) {\n throw new Error(\n `Publication name \"${name}\" exceeds PostgreSQL's 63-character identifier limit.`,\n );\n }\n}\n\nexport function internalPublicationPrefix({appID}: AppID) {\n return `_${appID}_`;\n}\n\nexport function legacyReplicationSlot({appID, shardNum}: ShardID) {\n return `${appID}_${shardNum}`;\n}\n\nexport function replicationSlotPrefix(shard: ShardID) {\n const {appID, shardNum} = check(shard);\n return `${appID}_${shardNum}_`;\n}\n\n/**\n * An expression used to match replication slots in the shard\n * in a Postgres `LIKE` operator.\n */\nexport function replicationSlotExpression(shard: ShardID) {\n // Underscores have a special meaning in LIKE values\n // so they have to be escaped.\n return `${replicationSlotPrefix(shard)}%`.replaceAll('_', '\\\\_');\n}\n\nfunction defaultPublicationName(appID: string, shardID: string | number) {\n return `_${appID}_public_${shardID}`;\n}\n\nexport function metadataPublicationName(\n appID: string,\n shardID: string | number,\n) {\n return `_${appID}_metadata_${shardID}`;\n}\n\n// The GLOBAL_SETUP must be idempotent as it can be run multiple times for different shards.\nfunction globalSetup(appID: AppID): string {\n const app = id(appSchema(appID));\n\n return /*sql*/ `\n CREATE SCHEMA IF NOT EXISTS ${app};\n\n CREATE TABLE IF NOT EXISTS ${app}.permissions (\n \"permissions\" JSONB,\n \"hash\" TEXT,\n\n -- Ensure that there is only a single row in the table.\n -- Application code can be agnostic to this column, and\n -- simply invoke UPDATE statements on the version columns.\n \"lock\" BOOL PRIMARY KEY DEFAULT true CHECK (lock)\n );\n\n CREATE OR REPLACE FUNCTION ${app}.set_permissions_hash()\n RETURNS TRIGGER AS $$\n BEGIN\n NEW.hash = md5(NEW.permissions::text);\n RETURN NEW;\n END;\n $$ LANGUAGE plpgsql;\n\n CREATE OR REPLACE TRIGGER on_set_permissions \n BEFORE INSERT OR UPDATE ON ${app}.permissions\n FOR EACH ROW\n EXECUTE FUNCTION ${app}.set_permissions_hash();\n\n INSERT INTO ${app}.permissions (permissions) VALUES (NULL) ON CONFLICT DO NOTHING;\n`;\n}\n\nexport async function ensureGlobalTables(db: PostgresDB, appID: AppID) {\n await db.unsafe(globalSetup(appID));\n}\n\nexport function getClientsTableDefinition(schema: string) {\n return /*sql*/ `\n CREATE TABLE ${schema}.\"clients\" (\n \"clientGroupID\" TEXT NOT NULL,\n \"clientID\" TEXT NOT NULL,\n \"lastMutationID\" BIGINT NOT NULL,\n \"userID\" TEXT,\n PRIMARY KEY(\"clientGroupID\", \"clientID\")\n );`;\n}\n\n/**\n * Tracks the results of mutations.\n * 1. It is an error for the same mutation ID to be used twice.\n * 2. The result is JSONB to allow for arbitrary results.\n *\n * The tables must be cleaned up as the clients\n * receive the mutation responses and as clients are removed.\n */\nexport function getMutationsTableDefinition(schema: string) {\n return /*sql*/ `\n CREATE TABLE ${schema}.\"mutations\" (\n \"clientGroupID\" TEXT NOT NULL,\n \"clientID\" TEXT NOT NULL,\n \"mutationID\" BIGINT NOT NULL,\n \"result\" JSON NOT NULL,\n PRIMARY KEY(\"clientGroupID\", \"clientID\", \"mutationID\")\n );`;\n}\n\nexport const SHARD_CONFIG_TABLE = 'shardConfig';\n\nexport function shardSetup(\n shardConfig: ShardConfig,\n metadataPublication: string,\n): string {\n const app = id(appSchema(shardConfig));\n const shard = id(upstreamSchema(shardConfig));\n\n const pubs = shardConfig.publications.toSorted();\n assert(\n pubs.includes(metadataPublication),\n () => `Publications must include ${metadataPublication}`,\n );\n\n return /*sql*/ `\n CREATE SCHEMA IF NOT EXISTS ${shard};\n\n ${getClientsTableDefinition(shard)}\n ${getMutationsTableDefinition(shard)}\n\n CREATE TABLE ${shard}.\"${SHARD_CONFIG_TABLE}\" (\n \"publications\" TEXT[] NOT NULL,\n \"ddlDetection\" BOOL NOT NULL,\n\n -- Ensure that there is only a single row in the table.\n \"lock\" BOOL PRIMARY KEY DEFAULT true CHECK (lock)\n );\n\n INSERT INTO ${shard}.\"${SHARD_CONFIG_TABLE}\" (\n \"publications\",\n \"ddlDetection\" \n ) VALUES (\n ARRAY[${literal(pubs)}], \n false -- set in SAVEPOINT with triggerSetup() statements\n );\n\n CREATE TABLE ${shard}.replicas (\n -- The DEFAULT exists purely for backwards compatibility support.\n -- New code always specifies a value based on Date.now().\n \"id\" TEXT PRIMARY KEY DEFAULT replace(gen_random_uuid()::text, '-', ''),\n \"rank\" BIGSERIAL,\n \"slot\" TEXT NOT NULL,\n \"version\" TEXT, -- replaced by generation, slated for deletion\n \"epoch\" INT4 DEFAULT 0,\n \"generation\" TEXT NOT NULL,\n \"stage\" INT4 DEFAULT ${InitialSync},\n \"backupPath\" TEXT, -- subpath within the litestream backup URL\n \"backupV5\" BOOL DEFAULT false,\n -- Whether the slot is acked only after storing in the PG change log.\n \"pgChangeLog\" BOOL DEFAULT false,\n \"initialSchema\" JSON, -- set after initial sync\n \"initialSyncContext\" JSON,\n \"subscriberContext\" JSON\n );\n\n DROP PUBLICATION IF EXISTS ${id(metadataPublication)};\n CREATE PUBLICATION ${id(metadataPublication)}\n FOR TABLE ${app}.\"permissions\", \n TABLE ${shard}.\"clients\", \n ${shard}.\"mutations\", \n ${shard}.\"replicas\" (id); -- advance the LSN/version with new replicas\n `;\n}\n\nexport function dropShard(appID: string, shardID: string | number): string {\n const schema = `${appID}_${shardID}`;\n const metadataPublication = metadataPublicationName(appID, shardID);\n const defaultPublication = defaultPublicationName(appID, shardID);\n\n // DROP SCHEMA ... CASCADE does not drop dependent PUBLICATIONS,\n // so PUBLICATIONs must be dropped explicitly.\n return /*sql*/ `\n DROP PUBLICATION IF EXISTS ${id(defaultPublication)};\n DROP PUBLICATION IF EXISTS ${id(metadataPublication)};\n DROP SCHEMA IF EXISTS ${id(schema)} CASCADE;\n `;\n}\n\nconst internalShardConfigSchema = v.object({\n publications: v.array(v.string()),\n ddlDetection: v.boolean(),\n});\n\nexport type InternalShardConfig = v.Infer<typeof internalShardConfigSchema>;\n\nconst replicaInfoSchema = v.object({\n id: v.string(),\n rank: v.bigint(),\n slot: v.string(),\n epoch: v.number(),\n generation: v.string(),\n stage: replicaStageSchema,\n backupPath: v.string().nullable(),\n backupV5: v.boolean(),\n /**\n * Whether the replication-manager that took over the replica's slot acks it\n * only after storing transactions in the PG change log (i.e. RMv1.5). The\n * slot's position past the change log's head then reflects acked\n * keepalives, rather than transactions missing from the change log.\n * False if unknown.\n */\n pgChangeLog: v.boolean(),\n});\n\nconst fullReplicaRowSchema = replicaInfoSchema.extend({\n initialSchema: publishedSchema,\n initialSyncContext: jsonObjectSchema.nullable(),\n subscriberContext: jsonObjectSchema.nullable(),\n});\n\nconst replicaSchema = internalShardConfigSchema.extend(\n fullReplicaRowSchema.shape,\n);\n\nexport type Replica = v.Infer<typeof replicaSchema>;\n\nconst replicationSlotStateSchema = v.object({\n active: v.boolean(),\n confirmedFlushLsn: v.string(),\n});\n\nconst replicaStateSchema = replicaInfoSchema.extend(\n replicationSlotStateSchema.shape,\n);\n\nexport type ReplicaState = v.Infer<typeof replicaStateSchema>;\n\n// triggerSetup is run separately in a sub-transaction (i.e. SAVEPOINT) so\n// that a failure (e.g. due to lack of superuser permissions) can be handled\n// by continuing in a degraded mode (ddlDetection = false).\nfunction triggerSetup(shard: ShardConfig): string {\n const schema = id(upstreamSchema(shard));\n return (\n createEventTriggerStatements(shard) +\n /*sql*/ `UPDATE ${schema}.\"shardConfig\" SET \"ddlDetection\" = true;`\n );\n}\n\nexport type BackupOptions = {\n backupPath: string | null;\n backupV5: boolean;\n /** See `pgChangeLog` in {@link ReplicaState}. */\n pgChangeLog: boolean;\n};\n\n/**\n * Creates a new replica to mark it as the owner of a specified `slot`.\n * This should be done with an advisory lock for replica/slot management\n * to ensure that the slot does not get dropped by concurrent cleanup\n * logic.\n *\n * Once initial sync is complete, {@link initInitialSyncReplica} should be called to\n * make the replica usable for incremental sync.\n */\nexport async function createReplica(\n sql: PostgresDB,\n shard: ShardID,\n id: string,\n slot: string,\n epoch: number,\n replicaVersion: string,\n {backupPath, backupV5, pgChangeLog}: BackupOptions,\n stage: ReplicaStage,\n) {\n const schema = upstreamSchema(shard);\n const values: Partial<v.Infer<typeof fullReplicaRowSchema>> & {\n version: string;\n } = {\n id,\n slot,\n epoch,\n generation: replicaVersion,\n // No longer read, but zero-cache 1.10 fails to start on a NULL \"version\",\n // so it is still written for rollbacks to 1.10.\n version: replicaVersion,\n backupPath,\n backupV5,\n pgChangeLog,\n stage,\n };\n await sql`INSERT INTO ${sql(schema)}.replicas ${sql(values)}`;\n}\n\n// Called in initial-sync to store the exact schema that was initially synced.\nexport async function initInitialSyncReplica(\n sql: PostgresDB,\n shard: ShardID,\n id: string,\n {tables, indexes}: PublishedSchema,\n initialSyncContext: JSONObject,\n) {\n const schema = upstreamSchema(shard);\n const synced: PublishedSchema = {tables, indexes};\n const values = {initialSchema: synced, initialSyncContext};\n await sql`UPDATE ${sql(schema)}.replicas SET ${sql(values)} WHERE id = ${id}`;\n}\n\n// Called to initialize a Restore replica (`destID`) with the generation\n// and initial sync context of the `sourceID` replica.\nexport async function initRestoreReplica(\n sql: PostgresDB,\n shard: ShardID,\n {sourceID, destID}: {sourceID: string; destID: string},\n): Promise<ReplicaState | null> {\n const schema = upstreamSchema(shard);\n await sql`\n UPDATE ${sql(schema)}.replicas dest \n SET \"generation\" = source.\"generation\",\n \"version\" = source.\"generation\",\n \"initialSchema\" = source.\"initialSchema\",\n \"initialSyncContext\" = source.\"initialSyncContext\"\n FROM ${sql(schema)}.replicas source\n WHERE source.id = ${sourceID} AND dest.id = ${destID};\n `;\n return getReplicaState(sql, shard, destID);\n}\n\n/**\n * Gets at the given version and optional ID. If no ID is specified, gets\n * the latest (i.e. highest rank) replica with that version.\n */\nexport async function getReplicaAtVersion(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n replicaVersion: string,\n id: string | null = null,\n context?: JSONObject,\n): Promise<Replica | null> {\n const schema = sql(upstreamSchema(shard));\n const result = await sql`\n SELECT\n replicas.\"id\",\n replicas.\"rank\",\n replicas.\"slot\",\n replicas.\"epoch\",\n replicas.\"generation\",\n replicas.\"stage\",\n replicas.\"backupPath\",\n replicas.\"backupV5\",\n replicas.\"pgChangeLog\",\n replicas.\"initialSchema\",\n replicas.\"initialSyncContext\",\n replicas.\"subscriberContext\",\n \"shardConfig\".\"publications\",\n \"shardConfig\".\"ddlDetection\"\n FROM ${schema}.replicas JOIN ${schema}.\"shardConfig\" ON true\n WHERE generation = ${replicaVersion} AND \"initialSyncContext\" IS NOT NULL\n AND id = COALESCE(${id}, id)\n ORDER BY rank DESC LIMIT 1;\n `;\n if (result.length === 0) {\n // log out all the replicas and the joined shardConfig\n const allReplicas = await sql`\n SELECT id, slot, generation, \"initialSyncContext\", \"subscriberContext\" \n FROM ${schema}.replicas`;\n lc.info?.(\n `Replica ${id}@${replicaVersion}` +\n (context ? `(context: ${stringify(context)}) ` : '') +\n `not found in: ${stringify(allReplicas)}`,\n );\n return null;\n }\n return v.parse(result[0], replicaSchema, 'passthrough');\n}\n\nexport async function getActiveReplicas(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n): Promise<ReplicaState[]> {\n const schema = sql(upstreamSchema(shard));\n const results = await sql`\n SELECT\n replicas.\"id\",\n replicas.\"rank\",\n replicas.\"slot\",\n replicas.\"epoch\",\n replicas.\"generation\",\n replicas.\"stage\",\n replicas.\"backupPath\",\n replicas.\"backupV5\",\n replicas.\"pgChangeLog\",\n slots.\"active\",\n slots.\"confirmed_flush_lsn\" as \"confirmedFlushLsn\"\n FROM ${schema}.replicas JOIN pg_replication_slots slots ON slot = slot_name\n WHERE \"initialSyncContext\" IS NOT NULL\n -- Exclude replicas whose slot has been invalidated (e.g. for exceeding\n -- max_slot_wal_keep_size). Such a replica can never be resumed, and\n -- restoring it just results in an AutoResetSignal on every attempt.\n AND slots.restart_lsn IS NOT NULL\n AND slots.wal_status IS DISTINCT FROM 'lost'\n ORDER BY generation DESC, confirmed_flush_lsn DESC;\n `;\n const replicas = v.parse(results, v.array(replicaStateSchema), 'passthrough');\n lc.info?.(`current replicas`, {replicas});\n return replicas;\n}\n\n/**\n * Returns candidates for restoring a new replica from. These include\n * all replicas in the newest `generation` of the given `epoch`, in order\n * of earliest available backup:\n * - An active InitialSync replica, which will be the only replica in\n * its generation by definition. Orphaned/inactive initial syncs are skipped.\n * - Replicas in the Replicate stage, active first.\n * - Replicas in the Restore stage, potentially available for resumption\n * as a last resort.\n */\nexport async function getRestoreCandidates(\n sql: PostgresDB,\n shard: ShardID,\n epoch: number,\n): Promise<ReplicaState[]> {\n const schema = sql(upstreamSchema(shard));\n const results = await sql`\n SELECT\n replicas.\"id\",\n replicas.\"rank\",\n replicas.\"slot\",\n replicas.\"epoch\",\n replicas.\"generation\",\n replicas.\"stage\",\n replicas.\"backupPath\",\n replicas.\"backupV5\",\n replicas.\"pgChangeLog\",\n slots.\"active\",\n slots.\"confirmed_flush_lsn\" as \"confirmedFlushLsn\"\n FROM ${schema}.replicas JOIN pg_replication_slots slots ON slot = slot_name\n\n -- coalesce on the newest generation in the epoch\n WHERE generation = (\n SELECT MAX(r.generation)\n FROM ${schema}.replicas r\n JOIN pg_replication_slots s ON r.slot = s.slot_name\n WHERE r.\"epoch\" = ${epoch} AND r.\"backupV5\"\n -- ignore orphaned InitialSyncs, which cannot be restored from\n AND (s.\"active\" OR r.\"stage\" != ${InitialSync})\n AND s.restart_lsn IS NOT NULL\n AND s.wal_status IS DISTINCT FROM 'lost'\n )\n AND \"epoch\" = ${epoch} AND \"backupV5\"\n AND (slots.active OR \"stage\" != ${InitialSync})\n AND slots.restart_lsn IS NOT NULL\n AND slots.wal_status IS DISTINCT FROM 'lost'\n\n ORDER BY \n stage ASC, -- ordered by backup availalibility\n active DESC, -- prefer active slots (fork over resume)\n confirmed_flush_lsn DESC; -- prefer replicas further ahead\n `;\n return v.parse(results, v.array(replicaStateSchema), 'passthrough');\n}\n\nexport async function getReplicaState(\n sql: PostgresDB,\n shard: ShardID,\n id: string,\n): Promise<ReplicaState | null> {\n const schema = sql(upstreamSchema(shard));\n const results = await sql`\n SELECT\n replicas.\"id\",\n replicas.\"rank\",\n replicas.\"slot\",\n replicas.\"epoch\",\n replicas.\"generation\",\n replicas.\"stage\",\n replicas.\"backupPath\",\n replicas.\"backupV5\",\n replicas.\"pgChangeLog\",\n slots.\"active\",\n slots.\"confirmed_flush_lsn\" as \"confirmedFlushLsn\"\n FROM ${schema}.replicas JOIN pg_replication_slots slots ON slot = slot_name\n WHERE \"id\" = ${id};\n `;\n return results.length\n ? v.parse(results[0], replicaStateSchema, 'passthrough')\n : null;\n}\n\nexport async function getInternalShardConfig(\n sql: PostgresDB,\n shard: ShardID,\n): Promise<InternalShardConfig> {\n const result = await sql`\n SELECT \"publications\", \"ddlDetection\"\n FROM ${sql(upstreamSchema(shard))}.\"shardConfig\";\n `;\n assert(\n result.length === 1,\n () => `Expected exactly one shardConfig row, got ${result.length}`,\n );\n return v.parse(result[0], internalShardConfigSchema, 'passthrough');\n}\n\n/**\n * Sets up and returns all publications (including internal ones) for\n * the given shard.\n */\nexport async function setupTablesAndReplication(\n lc: LogContext,\n sql: PostgresTransaction,\n requested: ShardConfig,\n includePartialIndexes = true,\n) {\n const {publications} = requested;\n // Validate requested publications.\n for (const pub of publications) {\n validatePublicationName(pub);\n if (pub.startsWith('_')) {\n throw new Error(\n `Publication names starting with \"_\" are reserved for internal use.\\n` +\n `Please use a different name for publication \"${pub}\".`,\n );\n }\n }\n const allPublications: string[] = [];\n\n // Setup application publications.\n if (publications.length) {\n const results = await sql<{pubname: string}[]>`\n SELECT pubname from pg_publication WHERE pubname IN ${sql(\n publications,\n )}`.values();\n\n if (results.length !== publications.length) {\n throw new Error(\n `Unknown or invalid publications. Specified: [${publications}]. Found: [${results.flat()}]`,\n );\n }\n allPublications.push(...publications);\n } else {\n const defaultPublication = defaultPublicationName(\n requested.appID,\n requested.shardNum,\n );\n await sql`\n DROP PUBLICATION IF EXISTS ${sql(defaultPublication)}`;\n await sql`\n CREATE PUBLICATION ${sql(defaultPublication)} \n FOR TABLES IN SCHEMA public\n WITH (publish_via_partition_root = true)`;\n allPublications.push(defaultPublication);\n }\n\n const metadataPublication = metadataPublicationName(\n requested.appID,\n requested.shardNum,\n );\n allPublications.push(metadataPublication);\n\n const shard = {...requested, publications: allPublications};\n\n // Setup the global tables and shard tables / publications.\n await sql.unsafe(globalSetup(shard) + shardSetup(shard, metadataPublication));\n\n const pubs = await getPublicationInfo(sql, allPublications);\n await replicaIdentitiesForTablesWithoutPrimaryKeys(pubs)?.apply(lc, sql);\n\n await setupTriggers(lc, sql, shard, includePartialIndexes);\n}\n\nexport async function setupTriggers(\n lc: LogContext,\n tx: PostgresTransaction,\n shard: ShardConfig,\n includePartialIndexes = true,\n) {\n const schema = upstreamSchema(shard);\n const [{ddlDetection}] = await tx<InternalShardConfig[]> /*sql*/ `\n SELECT \"ddlDetection\" FROM ${tx(schema)}.\"shardConfig\"`;\n\n // The functions invoked by event triggers are installed even if\n // event triggers are not supported/allowed by the db provider.\n // This allows users to manually invoke the update_schemas() function\n // as a workaround.\n await tx.unsafe(createEventFunctionStatements(shard, includePartialIndexes));\n try {\n await tx.savepoint(sub => sub.unsafe(triggerSetup(shard)));\n } catch (e) {\n if (ddlDetection) {\n // If ddlDetection has already been enabled, subsequent failures to\n // upgrade the trigger should be propagated rather than swallowed.\n throw e;\n }\n if (\n !(\n e instanceof postgres.PostgresError &&\n e.code === PG_INSUFFICIENT_PRIVILEGE\n )\n ) {\n throw e;\n }\n // If triggerSetup() fails, replication continues in ddlDetection=false mode.\n lc.warn?.(\n `Unable to create event triggers for schema change detection:\\n\\n` +\n `\"${e.hint ?? e.message}\"\\n\\n` +\n `Proceeding in degraded mode: schema changes will halt replication,\\n` +\n `requiring the replica to be reset (manually or with --auto-reset).`,\n );\n }\n}\n\nexport function validatePublications(\n lc: LogContext,\n published: PublicationInfo,\n) {\n // Verify that all publications export the proper events.\n published.publications.forEach(pub => {\n if (\n !pub.pubinsert ||\n !pub.pubupdate ||\n !pub.pubdelete ||\n !pub.pubtruncate\n ) {\n // TODO: Make APIError?\n throw new Error(\n `PUBLICATION ${pub.pubname} must publish insert, update, delete, and truncate`,\n );\n }\n });\n\n published.tables.forEach(table => validate(lc, table));\n}\n\ntype ReplicaIdentities = {\n apply(lc: LogContext, db: PostgresDB): Promise<void>;\n};\n\nexport function replicaIdentitiesForTablesWithoutPrimaryKeys(\n pubs: PublishedSchema,\n): ReplicaIdentities | undefined {\n const replicaIdentities: {\n schema: string;\n tableName: string;\n indexName: string;\n }[] = [];\n for (const table of pubs.tables) {\n if (!table.primaryKey?.length && table.replicaIdentity === Default) {\n // Look for an index that can serve as the REPLICA IDENTITY USING INDEX. It must be:\n // - UNIQUE\n // - NOT NULL columns\n // - not deferrable (i.e. isImmediate)\n // - not partial\n //\n // https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-REPLICA-IDENTITY\n const {schema, name: tableName} = table;\n for (const {columns, name: indexName} of pubs.indexes.filter(\n idx =>\n idx.schema === schema &&\n idx.tableName === tableName &&\n idx.unique &&\n idx.isImmediate &&\n idx.predicate === undefined,\n )) {\n if (Object.keys(columns).some(col => !table.columns[col].notNull)) {\n continue; // Only indexes with all NOT NULL columns are suitable.\n }\n replicaIdentities.push({schema, tableName, indexName});\n break;\n }\n }\n }\n\n if (replicaIdentities.length === 0) {\n return undefined;\n }\n return {\n apply: async (lc: LogContext, sql: PostgresDB) => {\n for (const {schema, tableName, indexName} of replicaIdentities) {\n lc.info?.(\n `setting \"${indexName}\" as the REPLICA IDENTITY for \"${tableName}\"`,\n );\n await sql`\n ALTER TABLE ${sql(schema)}.${sql(tableName)} \n REPLICA IDENTITY USING INDEX ${sql(indexName)}`;\n }\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -14,7 +14,7 @@ import { discoverChangeStreamerAddress } from "./schema/tables.js";
|
|
|
14
14
|
import "./change-streamer.js";
|
|
15
15
|
import "./snapshot-message.js";
|
|
16
16
|
import { subscribeDownstreamSchema, subscribeUpstreamSchema } from "./subscribe.js";
|
|
17
|
-
import WebSocket
|
|
17
|
+
import WebSocket from "ws";
|
|
18
18
|
import websocket from "@fastify/websocket";
|
|
19
19
|
//#region ../zero-cache/src/services/change-streamer/change-streamer-http.ts
|
|
20
20
|
var MIN_SUPPORTED_PROTOCOL_VERSION = 4;
|
|
@@ -226,7 +226,7 @@ var ChangeStreamerHttpClient = class {
|
|
|
226
226
|
* replication-manager is the one that serves the subscription.
|
|
227
227
|
*/
|
|
228
228
|
async connect(taskID) {
|
|
229
|
-
const ws = new WebSocket
|
|
229
|
+
const ws = new WebSocket(await this.#resolveChangeStreamer(SUBSCRIBE_PATH) + `?${new URLSearchParams({ taskID }).toString()}`, { handshakeTimeout: CONNECT_TIMEOUT_MS });
|
|
230
230
|
const outbound = Subscription.create();
|
|
231
231
|
return {
|
|
232
232
|
instream: await streamInternalWithSize(this.#lc, ws, subscribeDownstreamSchema, outbound),
|