@rocicorp/zero 0.25.13-canary.1 → 0.25.13-canary.10

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.
Files changed (76) hide show
  1. package/out/zero/package.json.js +1 -1
  2. package/out/zero/src/zero.js +2 -0
  3. package/out/zero/src/zero.js.map +1 -1
  4. package/out/zero-cache/src/server/main.js +1 -1
  5. package/out/zero-cache/src/server/main.js.map +1 -1
  6. package/out/zero-cache/src/server/runner/run-worker.d.ts.map +1 -1
  7. package/out/zero-cache/src/server/runner/run-worker.js +7 -3
  8. package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
  9. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  10. package/out/zero-cache/src/services/change-source/pg/change-source.js +29 -2
  11. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  12. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +1 -1
  13. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  14. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +5 -0
  15. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  16. package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts.map +1 -1
  17. package/out/zero-cache/src/services/change-streamer/schema/tables.js +9 -1
  18. package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
  19. package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
  20. package/out/zero-cache/src/services/change-streamer/storer.js +12 -1
  21. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  22. package/out/zero-cache/src/services/litestream/commands.d.ts +1 -1
  23. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  24. package/out/zero-cache/src/services/litestream/commands.js +2 -1
  25. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  26. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
  27. package/out/zero-cache/src/services/view-syncer/cvr-store.js +300 -135
  28. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  29. package/out/zero-cache/src/services/view-syncer/row-record-cache.js +3 -3
  30. package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
  31. package/out/zero-client/src/client/context.js +1 -0
  32. package/out/zero-client/src/client/context.js.map +1 -1
  33. package/out/zero-client/src/client/crud-impl.d.ts +4 -4
  34. package/out/zero-client/src/client/crud-impl.d.ts.map +1 -1
  35. package/out/zero-client/src/client/crud-impl.js +32 -23
  36. package/out/zero-client/src/client/crud-impl.js.map +1 -1
  37. package/out/zero-client/src/client/crud.d.ts +1 -1
  38. package/out/zero-client/src/client/crud.d.ts.map +1 -1
  39. package/out/zero-client/src/client/crud.js +9 -3
  40. package/out/zero-client/src/client/crud.js.map +1 -1
  41. package/out/zero-client/src/client/custom.d.ts +2 -2
  42. package/out/zero-client/src/client/custom.d.ts.map +1 -1
  43. package/out/zero-client/src/client/custom.js +4 -4
  44. package/out/zero-client/src/client/custom.js.map +1 -1
  45. package/out/zero-client/src/client/ivm-branch.d.ts +5 -2
  46. package/out/zero-client/src/client/ivm-branch.d.ts.map +1 -1
  47. package/out/zero-client/src/client/ivm-branch.js +43 -21
  48. package/out/zero-client/src/client/ivm-branch.js.map +1 -1
  49. package/out/zero-client/src/client/make-replicache-mutators.d.ts.map +1 -1
  50. package/out/zero-client/src/client/make-replicache-mutators.js +7 -5
  51. package/out/zero-client/src/client/make-replicache-mutators.js.map +1 -1
  52. package/out/zero-client/src/client/version.js +1 -1
  53. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  54. package/out/zero-client/src/client/zero.js +1 -0
  55. package/out/zero-client/src/client/zero.js.map +1 -1
  56. package/out/zero-client/src/mod.d.ts +1 -0
  57. package/out/zero-client/src/mod.d.ts.map +1 -1
  58. package/out/zero-react/src/zero-provider.js +3 -0
  59. package/out/zero-react/src/zero-provider.js.map +1 -1
  60. package/out/zero-solid/src/solid-view.js +3 -2
  61. package/out/zero-solid/src/solid-view.js.map +1 -1
  62. package/out/zero-solid/src/use-query.js +2 -1
  63. package/out/zero-solid/src/use-query.js.map +1 -1
  64. package/out/zero-solid/src/use-zero.js +3 -0
  65. package/out/zero-solid/src/use-zero.js.map +1 -1
  66. package/out/zql/src/ivm/memory-source.d.ts +27 -4
  67. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  68. package/out/zql/src/ivm/memory-source.js +141 -16
  69. package/out/zql/src/ivm/memory-source.js.map +1 -1
  70. package/out/zql/src/ivm/source.d.ts +10 -2
  71. package/out/zql/src/ivm/source.d.ts.map +1 -1
  72. package/out/zqlite/src/table-source.d.ts +3 -3
  73. package/out/zqlite/src/table-source.d.ts.map +1 -1
  74. package/out/zqlite/src/table-source.js +2 -2
  75. package/out/zqlite/src/table-source.js.map +1 -1
  76. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- const version = "0.25.13-canary.1";
1
+ const version = "0.25.13-canary.10";
2
2
  const packageJson = {
3
3
  version
4
4
  };
@@ -9,6 +9,7 @@ import { relationships } from "../../zero-schema/src/builder/relationship-builde
9
9
  import { createSchema } from "../../zero-schema/src/builder/schema-builder.js";
10
10
  import { boolean, enumeration, json, number, string, table } from "../../zero-schema/src/builder/table-builder.js";
11
11
  import { ANYONE_CAN, ANYONE_CAN_DO_ANYTHING, NOBODY_CAN, definePermissions } from "../../zero-schema/src/permissions.js";
12
+ import { debugMemorySource } from "../../zql/src/ivm/memory-source.js";
12
13
  import { defineMutators, defineMutatorsWithType, getMutator, isMutatorRegistry, mustGetMutator } from "../../zql/src/mutate/mutator-registry.js";
13
14
  import { defineMutator, defineMutatorWithType, isMutator, isMutatorDefinition } from "../../zql/src/mutate/mutator.js";
14
15
  import { createBuilder } from "../../zql/src/query/create-builder.js";
@@ -32,6 +33,7 @@ export {
32
33
  boolean,
33
34
  createBuilder,
34
35
  createSchema,
36
+ debugMemorySource,
35
37
  defineMutator,
36
38
  defineMutatorWithType,
37
39
  defineMutators,
@@ -1 +1 @@
1
- {"version":3,"file":"zero.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"zero.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;"}
@@ -82,7 +82,7 @@ async function runWorker(parent, env) {
82
82
  "message",
83
83
  () => {
84
84
  processes.addSubprocess(
85
- startReplicaBackupProcess(config),
85
+ startReplicaBackupProcess(lc, config),
86
86
  "supporting",
87
87
  "litestream"
88
88
  );
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","sources":["../../../../../zero-cache/src/server/main.ts"],"sourcesContent":["import {resolver} from '@rocicorp/resolver';\nimport path from 'node:path';\nimport {must} from '../../../shared/src/must.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {initEventSink} from '../observability/events.ts';\nimport {\n exitAfter,\n ProcessManager,\n runUntilKilled,\n type WorkerType,\n} from '../services/life-cycle.ts';\nimport {\n restoreReplica,\n startReplicaBackupProcess,\n} from '../services/litestream/commands.ts';\nimport {\n childWorker,\n parentWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {\n createNotifierFrom,\n handleSubscriptionsFrom,\n type ReplicaFileMode,\n subscribeTo,\n} from '../workers/replicator.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\nimport {WorkerDispatcher} from './worker-dispatcher.ts';\nimport {\n CHANGE_STREAMER_URL,\n MUTATOR_URL,\n REAPER_URL,\n REPLICATOR_URL,\n SYNCER_URL,\n} from './worker-urls.ts';\n\nconst clientConnectionBifurcated = false;\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n): Promise<void> {\n const startMs = Date.now();\n const config = getNormalizedZeroConfig({env});\n\n startOtelAuto(createLogContext(config, {worker: 'dispatcher'}, false));\n const lc = createLogContext(config, {worker: 'dispatcher'}, true);\n initEventSink(lc, config);\n\n const processes = new ProcessManager(lc, parent);\n\n const {numSyncWorkers: numSyncers} = config;\n if (config.upstream.maxConns < numSyncers) {\n throw new Error(\n `Insufficient upstream connections (${config.upstream.maxConns}) for ${numSyncers} syncers.` +\n `Increase ZERO_UPSTREAM_MAX_CONNS or decrease ZERO_NUM_SYNC_WORKERS (which defaults to available cores).`,\n );\n }\n if (config.cvr.maxConns < numSyncers) {\n throw new Error(\n `Insufficient cvr connections (${config.cvr.maxConns}) for ${numSyncers} syncers.` +\n `Increase ZERO_CVR_MAX_CONNS or decrease ZERO_NUM_SYNC_WORKERS (which defaults to available cores).`,\n );\n }\n\n const internalFlags: string[] =\n numSyncers === 0\n ? []\n : [\n '--upstream-max-conns-per-worker',\n String(Math.floor(config.upstream.maxConns / numSyncers)),\n '--cvr-max-conns-per-worker',\n String(Math.floor(config.cvr.maxConns / numSyncers)),\n ];\n\n function loadWorker(\n moduleUrl: URL,\n type: WorkerType,\n id?: string | number,\n ...args: string[]\n ): Worker {\n const worker = childWorker(moduleUrl, env, ...args, ...internalFlags);\n const name = path.basename(moduleUrl.pathname) + (id ? ` (${id})` : '');\n return processes.addWorker(worker, type, name);\n }\n\n const {\n taskID,\n changeStreamer: {mode: changeStreamerMode, uri: changeStreamerURI},\n litestream,\n } = config;\n const runChangeStreamer =\n changeStreamerMode === 'dedicated' && changeStreamerURI === undefined;\n\n let restoreStart = new Date();\n if (litestream.backupURL || (litestream.executable && !runChangeStreamer)) {\n try {\n restoreStart = await restoreReplica(lc, config);\n } catch (e) {\n if (runChangeStreamer) {\n // If the restore failed, e.g. due to a corrupt backup, the\n // replication-manager recovers by re-syncing.\n lc.error?.('error restoring backup. resyncing the replica.', e);\n } else {\n // View-syncers, on the other hand, have no option other than to retry\n // until a valid backup has been published. This is achieved by\n // shutting down and letting the container runner retry with its\n // configured policy.\n throw e;\n }\n }\n }\n\n const {promise: changeStreamerReady, resolve: changeStreamerStarted} =\n resolver();\n const changeStreamer = runChangeStreamer\n ? loadWorker(\n CHANGE_STREAMER_URL,\n 'supporting',\n undefined,\n String(restoreStart.getTime()),\n ).once('message', changeStreamerStarted)\n : (changeStreamerStarted() ?? undefined);\n\n const {promise: reaperReady, resolve: reaperStarted} = resolver();\n if (numSyncers > 0) {\n loadWorker(REAPER_URL, 'supporting').once('message', reaperStarted);\n } else {\n reaperStarted();\n }\n\n // Wait for the change-streamer to be ready to guarantee that a replica\n // file is present.\n await changeStreamerReady;\n\n if (runChangeStreamer && litestream.backupURL) {\n // Start a backup replicator and corresponding litestream backup process.\n const {promise: backupReady, resolve} = resolver();\n const mode: ReplicaFileMode = 'backup';\n loadWorker(REPLICATOR_URL, 'supporting', mode, mode).once(\n // Wait for the Replicator's first message (i.e. \"ready\") before starting\n // litestream backup in order to avoid contending on the lock when the\n // replicator first prepares the db file.\n 'message',\n () => {\n processes.addSubprocess(\n startReplicaBackupProcess(config),\n 'supporting',\n 'litestream',\n );\n resolve();\n },\n );\n await backupReady;\n }\n\n // Before starting the view-syncers, ensure that the reaper has started\n // up, indicating that any CVR db migrations have been performed.\n await reaperReady;\n\n const syncers: Worker[] = [];\n if (numSyncers) {\n const mode: ReplicaFileMode =\n runChangeStreamer && litestream.backupURL ? 'serving-copy' : 'serving';\n const {promise: replicaReady, resolve} = resolver();\n const replicator = loadWorker(\n REPLICATOR_URL,\n 'supporting',\n mode,\n mode,\n ).once('message', () => {\n subscribeTo(lc, replicator);\n resolve();\n });\n await replicaReady;\n\n const notifier = createNotifierFrom(lc, replicator);\n for (let i = 0; i < numSyncers; i++) {\n syncers.push(loadWorker(SYNCER_URL, 'user-facing', i + 1, mode));\n }\n syncers.forEach(syncer => handleSubscriptionsFrom(lc, syncer, notifier));\n }\n let mutator: Worker | undefined;\n if (clientConnectionBifurcated) {\n mutator = loadWorker(MUTATOR_URL, 'supporting', 'mutator');\n }\n\n lc.info?.('waiting for workers to be ready ...');\n const logWaiting = setInterval(\n () => lc.info?.(`still waiting for ${processes.initializing().join(', ')}`),\n 10_000,\n );\n await processes.allWorkersReady();\n clearInterval(logWaiting);\n lc.info?.(`all workers ready (${Date.now() - startMs} ms)`);\n\n parent.send(['ready', {ready: true}]);\n\n try {\n await runUntilKilled(\n lc,\n parent,\n new WorkerDispatcher(\n lc,\n taskID,\n parent,\n syncers,\n mutator,\n changeStreamer,\n ),\n );\n } catch (err) {\n processes.logErrorAndExit(err, 'dispatcher');\n }\n\n await processes.done();\n}\n\nif (!singleProcessMode()) {\n void exitAfter(() => runWorker(must(parentWorker), process.env));\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"main.js","sources":["../../../../../zero-cache/src/server/main.ts"],"sourcesContent":["import {resolver} from '@rocicorp/resolver';\nimport path from 'node:path';\nimport {must} from '../../../shared/src/must.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {initEventSink} from '../observability/events.ts';\nimport {\n exitAfter,\n ProcessManager,\n runUntilKilled,\n type WorkerType,\n} from '../services/life-cycle.ts';\nimport {\n restoreReplica,\n startReplicaBackupProcess,\n} from '../services/litestream/commands.ts';\nimport {\n childWorker,\n parentWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {\n createNotifierFrom,\n handleSubscriptionsFrom,\n type ReplicaFileMode,\n subscribeTo,\n} from '../workers/replicator.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\nimport {WorkerDispatcher} from './worker-dispatcher.ts';\nimport {\n CHANGE_STREAMER_URL,\n MUTATOR_URL,\n REAPER_URL,\n REPLICATOR_URL,\n SYNCER_URL,\n} from './worker-urls.ts';\n\nconst clientConnectionBifurcated = false;\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n): Promise<void> {\n const startMs = Date.now();\n const config = getNormalizedZeroConfig({env});\n\n startOtelAuto(createLogContext(config, {worker: 'dispatcher'}, false));\n const lc = createLogContext(config, {worker: 'dispatcher'}, true);\n initEventSink(lc, config);\n\n const processes = new ProcessManager(lc, parent);\n\n const {numSyncWorkers: numSyncers} = config;\n if (config.upstream.maxConns < numSyncers) {\n throw new Error(\n `Insufficient upstream connections (${config.upstream.maxConns}) for ${numSyncers} syncers.` +\n `Increase ZERO_UPSTREAM_MAX_CONNS or decrease ZERO_NUM_SYNC_WORKERS (which defaults to available cores).`,\n );\n }\n if (config.cvr.maxConns < numSyncers) {\n throw new Error(\n `Insufficient cvr connections (${config.cvr.maxConns}) for ${numSyncers} syncers.` +\n `Increase ZERO_CVR_MAX_CONNS or decrease ZERO_NUM_SYNC_WORKERS (which defaults to available cores).`,\n );\n }\n\n const internalFlags: string[] =\n numSyncers === 0\n ? []\n : [\n '--upstream-max-conns-per-worker',\n String(Math.floor(config.upstream.maxConns / numSyncers)),\n '--cvr-max-conns-per-worker',\n String(Math.floor(config.cvr.maxConns / numSyncers)),\n ];\n\n function loadWorker(\n moduleUrl: URL,\n type: WorkerType,\n id?: string | number,\n ...args: string[]\n ): Worker {\n const worker = childWorker(moduleUrl, env, ...args, ...internalFlags);\n const name = path.basename(moduleUrl.pathname) + (id ? ` (${id})` : '');\n return processes.addWorker(worker, type, name);\n }\n\n const {\n taskID,\n changeStreamer: {mode: changeStreamerMode, uri: changeStreamerURI},\n litestream,\n } = config;\n const runChangeStreamer =\n changeStreamerMode === 'dedicated' && changeStreamerURI === undefined;\n\n let restoreStart = new Date();\n if (litestream.backupURL || (litestream.executable && !runChangeStreamer)) {\n try {\n restoreStart = await restoreReplica(lc, config);\n } catch (e) {\n if (runChangeStreamer) {\n // If the restore failed, e.g. due to a corrupt backup, the\n // replication-manager recovers by re-syncing.\n lc.error?.('error restoring backup. resyncing the replica.', e);\n } else {\n // View-syncers, on the other hand, have no option other than to retry\n // until a valid backup has been published. This is achieved by\n // shutting down and letting the container runner retry with its\n // configured policy.\n throw e;\n }\n }\n }\n\n const {promise: changeStreamerReady, resolve: changeStreamerStarted} =\n resolver();\n const changeStreamer = runChangeStreamer\n ? loadWorker(\n CHANGE_STREAMER_URL,\n 'supporting',\n undefined,\n String(restoreStart.getTime()),\n ).once('message', changeStreamerStarted)\n : (changeStreamerStarted() ?? undefined);\n\n const {promise: reaperReady, resolve: reaperStarted} = resolver();\n if (numSyncers > 0) {\n loadWorker(REAPER_URL, 'supporting').once('message', reaperStarted);\n } else {\n reaperStarted();\n }\n\n // Wait for the change-streamer to be ready to guarantee that a replica\n // file is present.\n await changeStreamerReady;\n\n if (runChangeStreamer && litestream.backupURL) {\n // Start a backup replicator and corresponding litestream backup process.\n const {promise: backupReady, resolve} = resolver();\n const mode: ReplicaFileMode = 'backup';\n loadWorker(REPLICATOR_URL, 'supporting', mode, mode).once(\n // Wait for the Replicator's first message (i.e. \"ready\") before starting\n // litestream backup in order to avoid contending on the lock when the\n // replicator first prepares the db file.\n 'message',\n () => {\n processes.addSubprocess(\n startReplicaBackupProcess(lc, config),\n 'supporting',\n 'litestream',\n );\n resolve();\n },\n );\n await backupReady;\n }\n\n // Before starting the view-syncers, ensure that the reaper has started\n // up, indicating that any CVR db migrations have been performed.\n await reaperReady;\n\n const syncers: Worker[] = [];\n if (numSyncers) {\n const mode: ReplicaFileMode =\n runChangeStreamer && litestream.backupURL ? 'serving-copy' : 'serving';\n const {promise: replicaReady, resolve} = resolver();\n const replicator = loadWorker(\n REPLICATOR_URL,\n 'supporting',\n mode,\n mode,\n ).once('message', () => {\n subscribeTo(lc, replicator);\n resolve();\n });\n await replicaReady;\n\n const notifier = createNotifierFrom(lc, replicator);\n for (let i = 0; i < numSyncers; i++) {\n syncers.push(loadWorker(SYNCER_URL, 'user-facing', i + 1, mode));\n }\n syncers.forEach(syncer => handleSubscriptionsFrom(lc, syncer, notifier));\n }\n let mutator: Worker | undefined;\n if (clientConnectionBifurcated) {\n mutator = loadWorker(MUTATOR_URL, 'supporting', 'mutator');\n }\n\n lc.info?.('waiting for workers to be ready ...');\n const logWaiting = setInterval(\n () => lc.info?.(`still waiting for ${processes.initializing().join(', ')}`),\n 10_000,\n );\n await processes.allWorkersReady();\n clearInterval(logWaiting);\n lc.info?.(`all workers ready (${Date.now() - startMs} ms)`);\n\n parent.send(['ready', {ready: true}]);\n\n try {\n await runUntilKilled(\n lc,\n parent,\n new WorkerDispatcher(\n lc,\n taskID,\n parent,\n syncers,\n mutator,\n changeStreamer,\n ),\n );\n } catch (err) {\n processes.logErrorAndExit(err, 'dispatcher');\n }\n\n await processes.done();\n}\n\nif (!singleProcessMode()) {\n void exitAfter(() => runWorker(must(parentWorker), process.env));\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;AAwCA,eAA8B,UAC5B,QACA,KACe;AACf,QAAM,UAAU,KAAK,IAAA;AACrB,QAAM,SAAS,wBAAwB,EAAC,KAAI;AAE5C,gBAAc,iBAAiB,QAAQ,EAAC,QAAQ,aAAA,GAAe,KAAK,CAAC;AACrE,QAAM,KAAK,iBAAiB,QAAQ,EAAC,QAAQ,aAAA,GAAe,IAAI;AAChE,gBAAc,IAAI,MAAM;AAExB,QAAM,YAAY,IAAI,eAAe,IAAI,MAAM;AAE/C,QAAM,EAAC,gBAAgB,WAAA,IAAc;AACrC,MAAI,OAAO,SAAS,WAAW,YAAY;AACzC,UAAM,IAAI;AAAA,MACR,sCAAsC,OAAO,SAAS,QAAQ,SAAS,UAAU;AAAA,IAAA;AAAA,EAGrF;AACA,MAAI,OAAO,IAAI,WAAW,YAAY;AACpC,UAAM,IAAI;AAAA,MACR,iCAAiC,OAAO,IAAI,QAAQ,SAAS,UAAU;AAAA,IAAA;AAAA,EAG3E;AAEA,QAAM,gBACJ,eAAe,IACX,KACA;AAAA,IACE;AAAA,IACA,OAAO,KAAK,MAAM,OAAO,SAAS,WAAW,UAAU,CAAC;AAAA,IACxD;AAAA,IACA,OAAO,KAAK,MAAM,OAAO,IAAI,WAAW,UAAU,CAAC;AAAA,EAAA;AAG3D,WAAS,WACP,WACA,MACA,OACG,MACK;AACR,UAAM,SAAS,YAAY,WAAW,KAAK,GAAG,MAAM,GAAG,aAAa;AACpE,UAAM,OAAO,KAAK,SAAS,UAAU,QAAQ,KAAK,KAAK,KAAK,EAAE,MAAM;AACpE,WAAO,UAAU,UAAU,QAAQ,MAAM,IAAI;AAAA,EAC/C;AAEA,QAAM;AAAA,IACJ;AAAA,IACA,gBAAgB,EAAC,MAAM,oBAAoB,KAAK,kBAAA;AAAA,IAChD;AAAA,EAAA,IACE;AACJ,QAAM,oBACJ,uBAAuB,eAAe,sBAAsB;AAE9D,MAAI,mCAAmB,KAAA;AACvB,MAAI,WAAW,aAAc,WAAW,cAAc,CAAC,mBAAoB;AACzE,QAAI;AACF,qBAAe,MAAM,eAAe,IAAI,MAAM;AAAA,IAChD,SAAS,GAAG;AACV,UAAI,mBAAmB;AAGrB,WAAG,QAAQ,kDAAkD,CAAC;AAAA,MAChE,OAAO;AAKL,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,EAAC,SAAS,qBAAqB,SAAS,sBAAA,IAC5C,SAAA;AACF,QAAM,iBAAiB,oBACnB;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,aAAa,QAAA,CAAS;AAAA,EAAA,EAC7B,KAAK,WAAW,qBAAqB,IACtC,2BAA2B;AAEhC,QAAM,EAAC,SAAS,aAAa,SAAS,cAAA,IAAiB,SAAA;AACvD,MAAI,aAAa,GAAG;AAClB,eAAW,YAAY,YAAY,EAAE,KAAK,WAAW,aAAa;AAAA,EACpE,OAAO;AACL,kBAAA;AAAA,EACF;AAIA,QAAM;AAEN,MAAI,qBAAqB,WAAW,WAAW;AAE7C,UAAM,EAAC,SAAS,aAAa,QAAA,IAAW,SAAA;AACxC,UAAM,OAAwB;AAC9B,eAAW,gBAAgB,cAAc,MAAM,IAAI,EAAE;AAAA;AAAA;AAAA;AAAA,MAInD;AAAA,MACA,MAAM;AACJ,kBAAU;AAAA,UACR,0BAA0B,IAAI,MAAM;AAAA,UACpC;AAAA,UACA;AAAA,QAAA;AAEF,gBAAA;AAAA,MACF;AAAA,IAAA;AAEF,UAAM;AAAA,EACR;AAIA,QAAM;AAEN,QAAM,UAAoB,CAAA;AAC1B,MAAI,YAAY;AACd,UAAM,OACJ,qBAAqB,WAAW,YAAY,iBAAiB;AAC/D,UAAM,EAAC,SAAS,cAAc,QAAA,IAAW,SAAA;AACzC,UAAM,aAAa;AAAA,MACjB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA,EACA,KAAK,WAAW,MAAM;AACtB,kBAAY,IAAI,UAAU;AAC1B,cAAA;AAAA,IACF,CAAC;AACD,UAAM;AAEN,UAAM,WAAW,mBAAmB,IAAI,UAAU;AAClD,aAAS,IAAI,GAAG,IAAI,YAAY,KAAK;AACnC,cAAQ,KAAK,WAAW,YAAY,eAAe,IAAI,GAAG,IAAI,CAAC;AAAA,IACjE;AACA,YAAQ,QAAQ,CAAA,WAAU,wBAAwB,IAAI,QAAQ,QAAQ,CAAC;AAAA,EACzE;AACA,MAAI;AAKJ,KAAG,OAAO,qCAAqC;AAC/C,QAAM,aAAa;AAAA,IACjB,MAAM,GAAG,OAAO,qBAAqB,UAAU,eAAe,KAAK,IAAI,CAAC,EAAE;AAAA,IAC1E;AAAA,EAAA;AAEF,QAAM,UAAU,gBAAA;AAChB,gBAAc,UAAU;AACxB,KAAG,OAAO,sBAAsB,KAAK,QAAQ,OAAO,MAAM;AAE1D,SAAO,KAAK,CAAC,SAAS,EAAC,OAAO,KAAA,CAAK,CAAC;AAEpC,MAAI;AACF,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA,IAAI;AAAA,QACF;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,IACF;AAAA,EAEJ,SAAS,KAAK;AACZ,cAAU,gBAAgB,KAAK,YAAY;AAAA,EAC7C;AAEA,QAAM,UAAU,KAAA;AAClB;AAEA,IAAI,CAAC,qBAAqB;AACxB,OAAK,UAAU,MAAM,UAAU,KAAK,YAAY,GAAG,QAAQ,GAAG,CAAC;AACjE;"}
@@ -1 +1 @@
1
- {"version":3,"file":"run-worker.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/server/runner/run-worker.ts"],"names":[],"mappings":"AAAA,OAAO,kCAAkC,CAAC;AAO1C,OAAO,EAAc,KAAK,MAAM,EAAC,MAAM,0BAA0B,CAAC;AAMlE;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,OAAO,CAAC,IAAI,CAAC,CAuDf"}
1
+ {"version":3,"file":"run-worker.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/server/runner/run-worker.ts"],"names":[],"mappings":"AAAA,OAAO,kCAAkC,CAAC;AAO1C,OAAO,EAAc,KAAK,MAAM,EAAC,MAAM,0BAA0B,CAAC;AAMlE;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,OAAO,CAAC,IAAI,CAAC,CA0Df"}
@@ -17,9 +17,13 @@ async function runWorker(parent, env) {
17
17
  const processes = new ProcessManager(lc, parent ?? process);
18
18
  const { port, lazyStartup } = config;
19
19
  const serverVersion = getServerVersion(config);
20
- lc.info?.(
21
- `starting server${!serverVersion ? "" : `@${serverVersion}`} protocolVersion=${PROTOCOL_VERSION}`
22
- );
20
+ lc.info?.(`starting server${!serverVersion ? "" : `@${serverVersion}`} `, {
21
+ protocolVersion: PROTOCOL_VERSION,
22
+ taskID: config.taskID,
23
+ app: config.app,
24
+ shard: config.shard,
25
+ port: config.port
26
+ });
23
27
  let zeroCache;
24
28
  function startZeroCache() {
25
29
  if (zeroCache === void 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"run-worker.js","sources":["../../../../../../zero-cache/src/server/runner/run-worker.ts"],"sourcesContent":["import '../../../../shared/src/dotenv.ts';\n\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport {PROTOCOL_VERSION} from '../../../../zero-protocol/src/protocol-version.ts';\nimport {normalizeZeroConfig} from '../../config/normalize.ts';\nimport {getServerVersion, getZeroConfig} from '../../config/zero-config.ts';\nimport {ProcessManager, runUntilKilled} from '../../services/life-cycle.ts';\nimport {childWorker, type Worker} from '../../types/processes.ts';\nimport {createLogContext} from '../logging.ts';\nimport {MAIN_URL} from '../worker-urls.ts';\nimport {getTaskID} from './runtime.ts';\nimport {ZeroDispatcher} from './zero-dispatcher.ts';\n\n/**\n * Top-level `runner` entry point to the zero-cache. This layer is responsible for:\n * * runtime-based config normalization\n * * lazy startup\n * * serving /statsz\n * * auto-reset restarts (TODO)\n */\nexport async function runWorker(\n parent: Worker | null,\n env: NodeJS.ProcessEnv,\n): Promise<void> {\n // Note: Deprecation warnings are only emitted at this top-level parse;\n // they are suppressed when parsed in subprocesses.\n const cfg = getZeroConfig({env, emitDeprecationWarnings: true});\n const lc = createLogContext(cfg, {worker: 'runner'});\n\n const defaultTaskID = await getTaskID(lc);\n const config = normalizeZeroConfig(lc, cfg, env, defaultTaskID);\n const processes = new ProcessManager(lc, parent ?? process);\n\n const {port, lazyStartup} = config;\n const serverVersion = getServerVersion(config);\n lc.info?.(\n `starting server${!serverVersion ? '' : `@${serverVersion}`} ` +\n `protocolVersion=${PROTOCOL_VERSION}`,\n );\n\n let zeroCache: Resolver<Worker> | undefined;\n function startZeroCache(): Promise<Worker> {\n if (zeroCache === undefined) {\n const startMs = performance.now();\n lc.info?.('starting zero-cache');\n\n const r = (zeroCache = resolver<Worker>());\n const w = childWorker(MAIN_URL, env)\n .once('message', () => {\n r.resolve(w);\n lc.info?.(`zero-cache ready (${performance.now() - startMs} ms)`);\n })\n .once('error', r.reject);\n\n processes.addWorker(w, 'user-facing', 'zero-cache');\n }\n return zeroCache.promise;\n }\n\n // Eagerly start the zero-cache if it was not configured with --lazy-startup.\n if (!lazyStartup) {\n void startZeroCache();\n }\n\n await processes.allWorkersReady();\n parent?.send(['ready', {ready: true}]);\n\n try {\n await runUntilKilled(\n lc,\n parent ?? process,\n new ZeroDispatcher(config, lc, {port}, startZeroCache),\n );\n } catch (err) {\n processes.logErrorAndExit(err, 'main');\n }\n\n await processes.done();\n}\n"],"names":[],"mappings":";;;;;;;;;;;AAoBA,eAAsB,UACpB,QACA,KACe;AAGf,QAAM,MAAM,cAAc,EAAC,KAAK,yBAAyB,MAAK;AAC9D,QAAM,KAAK,iBAAiB,KAAK,EAAC,QAAQ,UAAS;AAEnD,QAAM,gBAAgB,MAAM,UAAU,EAAE;AACxC,QAAM,SAAS,oBAAoB,IAAI,KAAK,KAAK,aAAa;AAC9D,QAAM,YAAY,IAAI,eAAe,IAAI,UAAU,OAAO;AAE1D,QAAM,EAAC,MAAM,YAAA,IAAe;AAC5B,QAAM,gBAAgB,iBAAiB,MAAM;AAC7C,KAAG;AAAA,IACD,kBAAkB,CAAC,gBAAgB,KAAK,IAAI,aAAa,EAAE,oBACtC,gBAAgB;AAAA,EAAA;AAGvC,MAAI;AACJ,WAAS,iBAAkC;AACzC,QAAI,cAAc,QAAW;AAC3B,YAAM,UAAU,YAAY,IAAA;AAC5B,SAAG,OAAO,qBAAqB;AAE/B,YAAM,IAAK,YAAY,SAAA;AACvB,YAAM,IAAI,YAAY,UAAU,GAAG,EAChC,KAAK,WAAW,MAAM;AACrB,UAAE,QAAQ,CAAC;AACX,WAAG,OAAO,qBAAqB,YAAY,QAAQ,OAAO,MAAM;AAAA,MAClE,CAAC,EACA,KAAK,SAAS,EAAE,MAAM;AAEzB,gBAAU,UAAU,GAAG,eAAe,YAAY;AAAA,IACpD;AACA,WAAO,UAAU;AAAA,EACnB;AAGA,MAAI,CAAC,aAAa;AAChB,SAAK,eAAA;AAAA,EACP;AAEA,QAAM,UAAU,gBAAA;AAChB,UAAQ,KAAK,CAAC,SAAS,EAAC,OAAO,KAAA,CAAK,CAAC;AAErC,MAAI;AACF,UAAM;AAAA,MACJ;AAAA,MACA,UAAU;AAAA,MACV,IAAI,eAAe,QAAQ,IAAI,EAAC,KAAA,GAAO,cAAc;AAAA,IAAA;AAAA,EAEzD,SAAS,KAAK;AACZ,cAAU,gBAAgB,KAAK,MAAM;AAAA,EACvC;AAEA,QAAM,UAAU,KAAA;AAClB;"}
1
+ {"version":3,"file":"run-worker.js","sources":["../../../../../../zero-cache/src/server/runner/run-worker.ts"],"sourcesContent":["import '../../../../shared/src/dotenv.ts';\n\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport {PROTOCOL_VERSION} from '../../../../zero-protocol/src/protocol-version.ts';\nimport {normalizeZeroConfig} from '../../config/normalize.ts';\nimport {getServerVersion, getZeroConfig} from '../../config/zero-config.ts';\nimport {ProcessManager, runUntilKilled} from '../../services/life-cycle.ts';\nimport {childWorker, type Worker} from '../../types/processes.ts';\nimport {createLogContext} from '../logging.ts';\nimport {MAIN_URL} from '../worker-urls.ts';\nimport {getTaskID} from './runtime.ts';\nimport {ZeroDispatcher} from './zero-dispatcher.ts';\n\n/**\n * Top-level `runner` entry point to the zero-cache. This layer is responsible for:\n * * runtime-based config normalization\n * * lazy startup\n * * serving /statsz\n * * auto-reset restarts (TODO)\n */\nexport async function runWorker(\n parent: Worker | null,\n env: NodeJS.ProcessEnv,\n): Promise<void> {\n // Note: Deprecation warnings are only emitted at this top-level parse;\n // they are suppressed when parsed in subprocesses.\n const cfg = getZeroConfig({env, emitDeprecationWarnings: true});\n const lc = createLogContext(cfg, {worker: 'runner'});\n\n const defaultTaskID = await getTaskID(lc);\n const config = normalizeZeroConfig(lc, cfg, env, defaultTaskID);\n const processes = new ProcessManager(lc, parent ?? process);\n\n const {port, lazyStartup} = config;\n const serverVersion = getServerVersion(config);\n lc.info?.(`starting server${!serverVersion ? '' : `@${serverVersion}`} `, {\n protocolVersion: PROTOCOL_VERSION,\n taskID: config.taskID,\n app: config.app,\n shard: config.shard,\n port: config.port,\n });\n\n let zeroCache: Resolver<Worker> | undefined;\n function startZeroCache(): Promise<Worker> {\n if (zeroCache === undefined) {\n const startMs = performance.now();\n lc.info?.('starting zero-cache');\n\n const r = (zeroCache = resolver<Worker>());\n const w = childWorker(MAIN_URL, env)\n .once('message', () => {\n r.resolve(w);\n lc.info?.(`zero-cache ready (${performance.now() - startMs} ms)`);\n })\n .once('error', r.reject);\n\n processes.addWorker(w, 'user-facing', 'zero-cache');\n }\n return zeroCache.promise;\n }\n\n // Eagerly start the zero-cache if it was not configured with --lazy-startup.\n if (!lazyStartup) {\n void startZeroCache();\n }\n\n await processes.allWorkersReady();\n parent?.send(['ready', {ready: true}]);\n\n try {\n await runUntilKilled(\n lc,\n parent ?? process,\n new ZeroDispatcher(config, lc, {port}, startZeroCache),\n );\n } catch (err) {\n processes.logErrorAndExit(err, 'main');\n }\n\n await processes.done();\n}\n"],"names":[],"mappings":";;;;;;;;;;;AAoBA,eAAsB,UACpB,QACA,KACe;AAGf,QAAM,MAAM,cAAc,EAAC,KAAK,yBAAyB,MAAK;AAC9D,QAAM,KAAK,iBAAiB,KAAK,EAAC,QAAQ,UAAS;AAEnD,QAAM,gBAAgB,MAAM,UAAU,EAAE;AACxC,QAAM,SAAS,oBAAoB,IAAI,KAAK,KAAK,aAAa;AAC9D,QAAM,YAAY,IAAI,eAAe,IAAI,UAAU,OAAO;AAE1D,QAAM,EAAC,MAAM,YAAA,IAAe;AAC5B,QAAM,gBAAgB,iBAAiB,MAAM;AAC7C,KAAG,OAAO,kBAAkB,CAAC,gBAAgB,KAAK,IAAI,aAAa,EAAE,KAAK;AAAA,IACxE,iBAAiB;AAAA,IACjB,QAAQ,OAAO;AAAA,IACf,KAAK,OAAO;AAAA,IACZ,OAAO,OAAO;AAAA,IACd,MAAM,OAAO;AAAA,EAAA,CACd;AAED,MAAI;AACJ,WAAS,iBAAkC;AACzC,QAAI,cAAc,QAAW;AAC3B,YAAM,UAAU,YAAY,IAAA;AAC5B,SAAG,OAAO,qBAAqB;AAE/B,YAAM,IAAK,YAAY,SAAA;AACvB,YAAM,IAAI,YAAY,UAAU,GAAG,EAChC,KAAK,WAAW,MAAM;AACrB,UAAE,QAAQ,CAAC;AACX,WAAG,OAAO,qBAAqB,YAAY,QAAQ,OAAO,MAAM;AAAA,MAClE,CAAC,EACA,KAAK,SAAS,EAAE,MAAM;AAEzB,gBAAU,UAAU,GAAG,eAAe,YAAY;AAAA,IACpD;AACA,WAAO,UAAU;AAAA,EACnB;AAGA,MAAI,CAAC,aAAa;AAChB,SAAK,eAAA;AAAA,EACP;AAEA,QAAM,UAAU,gBAAA;AAChB,UAAQ,KAAK,CAAC,SAAS,EAAC,OAAO,KAAA,CAAK,CAAC;AAErC,MAAI;AACF,UAAM;AAAA,MACJ;AAAA,MACA,UAAU;AAAA,MACV,IAAI,eAAe,QAAQ,IAAI,EAAC,KAAA,GAAO,cAAc;AAAA,IAAA;AAAA,EAEzD,SAAS,KAAK;AACZ,cAAU,gBAAgB,KAAK,MAAM;AAAA,EACvC;AAEA,QAAM,UAAU,KAAA;AAClB;"}
@@ -1 +1 @@
1
- {"version":3,"file":"change-source.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/change-source/pg/change-source.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAgBjD,OAAO,KAAK,EAEV,kBAAkB,EAEnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAEL,KAAK,WAAW,EAEjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,2BAA2B,CAAC;AAEpD,OAAO,KAAK,EACV,YAAY,EAEb,MAAM,kDAAkD,CAAC;AAE1D,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,8CAA8C,CAAC;AAYtD,OAAO,EAAC,KAAK,kBAAkB,EAAC,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAGV,eAAe,IAAI,gBAAgB,EACpC,MAAM,yCAAyC,CAAC;AAoBjD;;;;GAIG;AACH,wBAAsB,8BAA8B,CAClD,EAAE,EAAE,UAAU,EACd,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,kBAAkB,GAC9B,OAAO,CAAC;IAAC,iBAAiB,EAAE,iBAAiB,CAAC;IAAC,YAAY,EAAE,YAAY,CAAA;CAAC,CAAC,CAoC7E;AA6PD,qBAAa,KAAK;;gBAIJ,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;IAI9B,SAAS;IAiBT,GAAG,CAAC,SAAS,EAAE,WAAW;CAa3B;AAycD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,kBAAkB,EAAE,CAAC,EAAE,gBAAgB,WAwB3E"}
1
+ {"version":3,"file":"change-source.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/change-source/pg/change-source.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAgBjD,OAAO,KAAK,EAEV,kBAAkB,EAEnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAEL,KAAK,WAAW,EAEjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,2BAA2B,CAAC;AAEpD,OAAO,KAAK,EACV,YAAY,EAEb,MAAM,kDAAkD,CAAC;AAE1D,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,8CAA8C,CAAC;AAYtD,OAAO,EAAC,KAAK,kBAAkB,EAAC,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAGV,eAAe,IAAI,gBAAgB,EACpC,MAAM,yCAAyC,CAAC;AAoBjD;;;;GAIG;AACH,wBAAsB,8BAA8B,CAClD,EAAE,EAAE,UAAU,EACd,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,kBAAkB,GAC9B,OAAO,CAAC;IAAC,iBAAiB,EAAE,iBAAiB,CAAC;IAAC,YAAY,EAAE,YAAY,CAAA;CAAC,CAAC,CAoC7E;AAgSD,qBAAa,KAAK;;gBAIJ,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;IAI9B,SAAS;IAiBT,GAAG,CAAC,SAAS,EAAE,WAAW;CAa3B;AAycD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,kBAAkB,EAAE,CAAC,EAAE,gBAAgB,WAwB3E"}
@@ -197,17 +197,44 @@ class PostgresChangeSource {
197
197
  const slotExpression = replicationSlotExpression(this.#shard);
198
198
  const legacySlotName = legacyReplicationSlot(this.#shard);
199
199
  const result = await sql`
200
- SELECT slot_name as slot, pg_terminate_backend(active_pid), active_pid as pid
200
+ SELECT slot_name as slot, pg_terminate_backend(active_pid) as terminated, active_pid as pid
201
201
  FROM pg_replication_slots
202
202
  WHERE (slot_name LIKE ${slotExpression} OR slot_name = ${legacySlotName})
203
203
  AND slot_name <= ${slotToKeep}`;
204
+ this.#lc.info?.(`terminated replication slots: ${JSON.stringify(result)}`);
204
205
  if (result.length === 0) {
206
+ const shardSlots = await sql`
207
+ SELECT slot_name as slot, active, active_pid as pid
208
+ FROM pg_replication_slots
209
+ WHERE slot_name LIKE ${slotExpression} OR slot_name = ${legacySlotName}
210
+ ORDER BY slot_name`;
211
+ this.#lc.warn?.(
212
+ `slot ${slotToKeep} not found while cleaning subscribers; shard slots at time of failure: ${JSON.stringify(
213
+ shardSlots
214
+ )}`
215
+ );
205
216
  throw new AbortError(
206
217
  `replication slot ${slotToKeep} is missing. A different replication-manager should now be running on a new replication slot.`
207
218
  );
208
219
  }
209
220
  const replicasTable = `${upstreamSchema(this.#shard)}.replicas`;
210
- await sql`DELETE FROM ${sql(replicasTable)} WHERE slot != ${slotToKeep}`;
221
+ const replicasBefore = await sql`
222
+ SELECT slot, version FROM ${sql(replicasTable)} ORDER BY slot`;
223
+ this.#lc.info?.(
224
+ `replicas before cleanup (slotToKeep=${slotToKeep}): ${JSON.stringify(
225
+ replicasBefore
226
+ )}`
227
+ );
228
+ await sql`
229
+ DELETE FROM ${sql(replicasTable)}
230
+ WHERE slot < ${slotToKeep}`;
231
+ const replicasAfter = await sql`
232
+ SELECT slot, version FROM ${sql(replicasTable)} ORDER BY slot`;
233
+ this.#lc.info?.(
234
+ `replicas after cleanup (slotToKeep=${slotToKeep}): ${JSON.stringify(
235
+ replicasAfter
236
+ )}`
237
+ );
211
238
  const pids = result.filter(({ pid }) => pid !== null).map(({ pid }) => pid);
212
239
  if (pids.length) {
213
240
  this.#lc.info?.(`signaled subscriber ${pids} to shut down`);
@@ -1 +1 @@
1
- {"version":3,"file":"change-source.js","sources":["../../../../../../../zero-cache/src/services/change-source/pg/change-source.ts"],"sourcesContent":["import {\n PG_ADMIN_SHUTDOWN,\n PG_OBJECT_IN_USE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport postgres from 'postgres';\nimport {AbortError} from '../../../../../shared/src/abort-error.ts';\nimport {stringify} from '../../../../../shared/src/bigint-json.ts';\nimport {deepEqual} from '../../../../../shared/src/json.ts';\nimport {must} from '../../../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../../../shared/src/resolved-promises.ts';\nimport {\n equals,\n intersection,\n symmetricDifferences,\n} from '../../../../../shared/src/set-utils.ts';\nimport {sleep} from '../../../../../shared/src/sleep.ts';\nimport * as v from '../../../../../shared/src/valita.ts';\nimport {Database} from '../../../../../zqlite/src/db.ts';\nimport {mapPostgresToLiteColumn} from '../../../db/pg-to-lite.ts';\nimport type {\n ColumnSpec,\n PublishedTableSpec,\n TableSpec,\n} from '../../../db/specs.ts';\nimport {StatementRunner} from '../../../db/statements.ts';\nimport {\n oneAfter,\n versionFromLexi,\n versionToLexi,\n type LexiVersion,\n} from '../../../types/lexi-version.ts';\nimport {pgClient, type PostgresDB} from '../../../types/pg.ts';\nimport {\n upstreamSchema,\n type ShardConfig,\n type ShardID,\n} from '../../../types/shards.ts';\nimport type {Sink} from '../../../types/streams.ts';\nimport {Subscription, type PendingResult} from '../../../types/subscription.ts';\nimport type {\n ChangeSource,\n ChangeStream,\n} from '../../change-streamer/change-streamer-service.ts';\nimport {AutoResetSignal} from '../../change-streamer/schema/tables.ts';\nimport {\n getSubscriptionState,\n type SubscriptionState,\n} from '../../replicator/schema/replication-state.ts';\nimport type {JSONObject} from '../protocol/current.ts';\nimport type {\n DataChange,\n Identifier,\n MessageRelation,\n} from '../protocol/current/data.ts';\nimport type {\n ChangeStreamData,\n ChangeStreamMessage,\n Data,\n} from '../protocol/current/downstream.ts';\nimport {type InitialSyncOptions} from './initial-sync.ts';\nimport type {\n Message,\n MessageMessage,\n MessageRelation as PostgresRelation,\n} from './logical-replication/pgoutput.types.ts';\nimport {subscribe} from './logical-replication/stream.ts';\nimport {fromBigInt, toLexiVersion, type LSN} from './lsn.ts';\nimport {replicationEventSchema, type DdlUpdateEvent} from './schema/ddl.ts';\nimport {updateShardSchema} from './schema/init.ts';\nimport {getPublicationInfo, type PublishedSchema} from './schema/published.ts';\nimport {\n dropShard,\n getInternalShardConfig,\n getReplicaAtVersion,\n internalPublicationPrefix,\n legacyReplicationSlot,\n replicaIdentitiesForTablesWithoutPrimaryKeys,\n replicationSlotExpression,\n type InternalShardConfig,\n type Replica,\n} from './schema/shard.ts';\nimport {validate} from './schema/validation.ts';\nimport {initSyncSchema} from './sync-schema.ts';\n\n/**\n * Initializes a Postgres change source, including the initial sync of the\n * replica, before streaming changes from the corresponding logical replication\n * stream.\n */\nexport async function initializePostgresChangeSource(\n lc: LogContext,\n upstreamURI: string,\n shard: ShardConfig,\n replicaDbFile: string,\n syncOptions: InitialSyncOptions,\n): Promise<{subscriptionState: SubscriptionState; changeSource: ChangeSource}> {\n await initSyncSchema(\n lc,\n `replica-${shard.appID}-${shard.shardNum}`,\n shard,\n replicaDbFile,\n upstreamURI,\n syncOptions,\n );\n\n const replica = new Database(lc, replicaDbFile);\n const subscriptionState = getSubscriptionState(new StatementRunner(replica));\n replica.close();\n\n // Check that upstream is properly setup, and throw an AutoReset to re-run\n // initial sync if not.\n const db = pgClient(lc, upstreamURI);\n try {\n const upstreamReplica = await checkAndUpdateUpstream(\n lc,\n db,\n shard,\n subscriptionState,\n );\n\n const changeSource = new PostgresChangeSource(\n lc,\n upstreamURI,\n shard,\n upstreamReplica,\n );\n\n return {subscriptionState, changeSource};\n } finally {\n await db.end();\n }\n}\n\nasync function checkAndUpdateUpstream(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardConfig,\n {replicaVersion, publications: subscribed}: SubscriptionState,\n) {\n // Perform any shard schema updates\n await updateShardSchema(lc, sql, shard, replicaVersion);\n\n const upstreamReplica = await getReplicaAtVersion(\n lc,\n sql,\n shard,\n replicaVersion,\n );\n if (!upstreamReplica) {\n throw new AutoResetSignal(\n `No replication slot for replica at version ${replicaVersion}`,\n );\n }\n\n // Verify that the publications match what is being replicated.\n const requested = [...shard.publications].sort();\n const replicated = upstreamReplica.publications\n .filter(p => !p.startsWith(internalPublicationPrefix(shard)))\n .sort();\n if (!deepEqual(requested, replicated)) {\n lc.warn?.(`Dropping shard to change publications to: [${requested}]`);\n await sql.unsafe(dropShard(shard.appID, shard.shardNum));\n throw new AutoResetSignal(\n `Requested publications [${requested}] do not match configured ` +\n `publications: [${replicated}]`,\n );\n }\n\n // Sanity check: The subscription state on the replica should have the\n // same publications. This should be guaranteed by the equivalence of the\n // replicaVersion, but it doesn't hurt to verify.\n if (!deepEqual(upstreamReplica.publications, subscribed)) {\n throw new AutoResetSignal(\n `Upstream publications [${upstreamReplica.publications}] do not ` +\n `match subscribed publications [${subscribed}]`,\n );\n }\n\n // Verify that the publications exist.\n const exists = await sql`\n SELECT pubname FROM pg_publication WHERE pubname IN ${sql(subscribed)};\n `.values();\n if (exists.length !== subscribed.length) {\n throw new AutoResetSignal(\n `Upstream publications [${exists.flat()}] do not contain ` +\n `all subscribed publications [${subscribed}]`,\n );\n }\n\n const {slot} = upstreamReplica;\n const result = await sql<\n {restartLSN: LSN | null; walStatus: string | null}[]\n > /*sql*/ `\n SELECT restart_lsn as \"restartLSN\", wal_status as \"walStatus\" FROM pg_replication_slots\n WHERE slot_name = ${slot}`;\n if (result.length === 0) {\n throw new AutoResetSignal(`replication slot ${slot} is missing`);\n }\n const [{restartLSN, walStatus}] = result;\n if (restartLSN === null || walStatus === 'lost') {\n throw new AutoResetSignal(\n `replication slot ${slot} has been invalidated for exceeding the max_slot_wal_keep_size`,\n );\n }\n return upstreamReplica;\n}\n\n/**\n * Postgres implementation of a {@link ChangeSource} backed by a logical\n * replication stream.\n */\nclass PostgresChangeSource implements ChangeSource {\n readonly #lc: LogContext;\n readonly #upstreamUri: string;\n readonly #shard: ShardID;\n readonly #replica: Replica;\n\n constructor(\n lc: LogContext,\n upstreamUri: string,\n shard: ShardID,\n replica: Replica,\n ) {\n this.#lc = lc.withContext('component', 'change-source');\n this.#upstreamUri = upstreamUri;\n this.#shard = shard;\n this.#replica = replica;\n }\n\n async startStream(clientWatermark: string): Promise<ChangeStream> {\n const db = pgClient(this.#lc, this.#upstreamUri, {}, 'json-as-string');\n const {slot} = this.#replica;\n\n let cleanup = promiseVoid;\n try {\n ({cleanup} = await this.#stopExistingReplicationSlotSubscribers(\n db,\n slot,\n ));\n const config = await getInternalShardConfig(db, this.#shard);\n this.#lc.info?.(`starting replication stream@${slot}`);\n return await this.#startStream(db, slot, clientWatermark, config);\n } finally {\n void cleanup.then(() => db.end());\n }\n }\n\n async #startStream(\n db: PostgresDB,\n slot: string,\n clientWatermark: string,\n shardConfig: InternalShardConfig,\n ): Promise<ChangeStream> {\n const clientStart = oneAfter(clientWatermark);\n const {messages, acks} = await subscribe(\n this.#lc,\n db,\n slot,\n [...shardConfig.publications],\n versionFromLexi(clientStart),\n );\n\n const changes = Subscription.create<ChangeStreamMessage>({\n cleanup: () => messages.cancel(),\n });\n const acker = new Acker(acks);\n\n const changeMaker = new ChangeMaker(\n this.#lc,\n this.#shard,\n shardConfig,\n this.#replica.initialSchema,\n this.#upstreamUri,\n );\n\n void (async function () {\n try {\n for await (const [lsn, msg] of messages) {\n if (msg.tag === 'keepalive') {\n changes.push(['status', msg, {watermark: versionToLexi(lsn)}]);\n continue;\n }\n let last: PendingResult | undefined;\n for (const change of await changeMaker.makeChanges(lsn, msg)) {\n last = changes.push(change);\n }\n await last?.result; // Allow the change-streamer to push back.\n }\n } catch (e) {\n changes.fail(translateError(e));\n }\n })();\n\n this.#lc.info?.(\n `started replication stream@${slot} from ${clientWatermark} (replicaVersion: ${\n this.#replica.version\n })`,\n );\n\n return {\n changes,\n acks: {push: status => acker.ack(status[2].watermark)},\n };\n }\n\n /**\n * Stops replication slots associated with this shard, and returns\n * a `cleanup` task that drops any slot other than the specified\n * `slotToKeep`.\n *\n * Note that replication slots created after `slotToKeep` (as indicated by\n * the timestamp suffix) are preserved, as those are newly syncing replicas\n * that will soon take over the slot.\n */\n async #stopExistingReplicationSlotSubscribers(\n sql: PostgresDB,\n slotToKeep: string,\n ): Promise<{cleanup: Promise<void>}> {\n const slotExpression = replicationSlotExpression(this.#shard);\n const legacySlotName = legacyReplicationSlot(this.#shard);\n\n // Note: `slot_name <= slotToKeep` uses a string compare of the millisecond\n // timestamp, which works until it exceeds 13 digits (sometime in 2286).\n const result = await sql<{slot: string; pid: string | null}[]>`\n SELECT slot_name as slot, pg_terminate_backend(active_pid), active_pid as pid\n FROM pg_replication_slots \n WHERE (slot_name LIKE ${slotExpression} OR slot_name = ${legacySlotName})\n AND slot_name <= ${slotToKeep}`;\n if (result.length === 0) {\n throw new AbortError(\n `replication slot ${slotToKeep} is missing. A different ` +\n `replication-manager should now be running on a new ` +\n `replication slot.`,\n );\n }\n // Clean up the replicas table.\n const replicasTable = `${upstreamSchema(this.#shard)}.replicas`;\n await sql`DELETE FROM ${sql(replicasTable)} WHERE slot != ${slotToKeep}`;\n\n const pids = result.filter(({pid}) => pid !== null).map(({pid}) => pid);\n if (pids.length) {\n this.#lc.info?.(`signaled subscriber ${pids} to shut down`);\n }\n const otherSlots = result\n .filter(({slot}) => slot !== slotToKeep)\n .map(({slot}) => slot);\n return {\n cleanup: otherSlots.length\n ? this.#dropReplicationSlots(sql, otherSlots)\n : promiseVoid,\n };\n }\n\n async #dropReplicationSlots(sql: PostgresDB, slots: string[]) {\n this.#lc.info?.(`dropping other replication slot(s) ${slots}`);\n for (let i = 0; i < 5; i++) {\n try {\n await sql`\n SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots\n WHERE slot_name IN ${sql(slots)}\n `;\n this.#lc.info?.(`successfully dropped ${slots}`);\n return;\n } catch (e) {\n // error: replication slot \"zero_slot_change_source_test_id\" is active for PID 268\n if (\n e instanceof postgres.PostgresError &&\n e.code === PG_OBJECT_IN_USE\n ) {\n // The freeing up of the replication slot is not transactional;\n // sometimes it takes time for Postgres to consider the slot\n // inactive.\n this.#lc.debug?.(`attempt ${i + 1}: ${String(e)}`, e);\n } else {\n this.#lc.warn?.(`error dropping ${slots}`, e);\n }\n await sleep(1000);\n }\n }\n this.#lc.warn?.(`maximum attempts exceeded dropping ${slots}`);\n }\n}\n\n// Exported for testing.\nexport class Acker {\n #acks: Sink<bigint>;\n #keepaliveTimer: NodeJS.Timeout | undefined;\n\n constructor(acks: Sink<bigint>) {\n this.#acks = acks;\n }\n\n keepalive() {\n // Sets a timeout to send a standby status update in response to\n // a primary keepalive message.\n //\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-PRIMARY-KEEPALIVE-MESSAGE\n //\n // A primary keepalive message is streamed to the change-streamer as a\n // 'status' message, which in turn responds with an ack. However, in the\n // event that the change-streamer is backed up processing preceding\n // changes, this timeout will fire to send a status update that does not\n // change the confirmed flush position. This timeout must be shorter than\n // the `wal_sender_timeout`, which defaults to 60 seconds.\n //\n // https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT\n this.#keepaliveTimer ??= setTimeout(() => this.#sendAck(), 1000);\n }\n\n ack(watermark: LexiVersion) {\n this.#sendAck(watermark);\n }\n\n #sendAck(watermark?: LexiVersion) {\n clearTimeout(this.#keepaliveTimer);\n this.#keepaliveTimer = undefined;\n\n // Note: Sending '0/0' means \"keep alive but do not update confirmed_flush_lsn\"\n // https://github.com/postgres/postgres/blob/3edc67d337c2e498dad1cd200e460f7c63e512e6/src/backend/replication/walsender.c#L2457\n const lsn = watermark ? versionFromLexi(watermark) : 0n;\n this.#acks.push(lsn);\n }\n}\n\ntype ReplicationError = {\n lsn: bigint;\n msg: Message;\n err: unknown;\n lastLogTime: number;\n};\n\nconst SET_REPLICA_IDENTITY_DELAY_MS = 500;\n\nclass ChangeMaker {\n readonly #lc: LogContext;\n readonly #shardPrefix: string;\n readonly #shardConfig: InternalShardConfig;\n readonly #initialSchema: PublishedSchema;\n readonly #upstreamDB: PostgresDB;\n\n #replicaIdentityTimer: NodeJS.Timeout | undefined;\n #error: ReplicationError | undefined;\n\n constructor(\n lc: LogContext,\n {appID, shardNum}: ShardID,\n shardConfig: InternalShardConfig,\n initialSchema: PublishedSchema,\n upstreamURI: string,\n ) {\n this.#lc = lc;\n // Note: This matches the prefix used in pg_logical_emit_message() in pg/schema/ddl.ts.\n this.#shardPrefix = `${appID}/${shardNum}`;\n this.#shardConfig = shardConfig;\n this.#initialSchema = initialSchema;\n this.#upstreamDB = pgClient(lc, upstreamURI, {\n ['idle_timeout']: 10, // only used occasionally\n connection: {['application_name']: 'zero-schema-change-detector'},\n });\n }\n\n async makeChanges(lsn: bigint, msg: Message): Promise<ChangeStreamMessage[]> {\n if (this.#error) {\n this.#logError(this.#error);\n return [];\n }\n try {\n return await this.#makeChanges(msg);\n } catch (err) {\n this.#error = {lsn, msg, err, lastLogTime: 0};\n this.#logError(this.#error);\n\n const message = `Unable to continue replication from LSN ${fromBigInt(lsn)}`;\n const errorDetails: JSONObject = {error: message};\n if (err instanceof UnsupportedSchemaChangeError) {\n errorDetails.reason = err.description;\n errorDetails.context = err.ddlUpdate.context;\n } else {\n errorDetails.reason = String(err);\n }\n\n // Rollback the current transaction to avoid dangling transactions in\n // downstream processors (i.e. changeLog, replicator).\n return [\n ['rollback', {tag: 'rollback'}],\n ['control', {tag: 'reset-required', message, errorDetails}],\n ];\n }\n }\n\n #logError(error: ReplicationError) {\n const {lsn, msg, err, lastLogTime} = error;\n const now = Date.now();\n\n // Output an error to logs as replication messages continue to be dropped,\n // at most once a minute.\n if (now - lastLogTime > 60_000) {\n this.#lc.error?.(\n `Unable to continue replication from LSN ${fromBigInt(lsn)}: ${String(\n err,\n )}`,\n err instanceof UnsupportedSchemaChangeError\n ? err.ddlUpdate.context\n : // 'content' can be a large byte Buffer. Exclude it from logging output.\n {...msg, content: undefined},\n );\n error.lastLogTime = now;\n }\n }\n\n // oxlint-disable-next-line require-await\n async #makeChanges(msg: Message): Promise<ChangeStreamData[]> {\n switch (msg.tag) {\n case 'begin':\n return [\n [\n 'begin',\n {...msg, json: 's'},\n {commitWatermark: toLexiVersion(must(msg.commitLsn))},\n ],\n ];\n\n case 'delete': {\n if (!(msg.key ?? msg.old)) {\n throw new Error(\n `Invalid DELETE msg (missing key): ${stringify(msg)}`,\n );\n }\n return [\n [\n 'data',\n {\n ...msg,\n relation: withoutColumns(msg.relation),\n // https://www.postgresql.org/docs/current/protocol-logicalrep-message-formats.html#PROTOCOL-LOGICALREP-MESSAGE-FORMATS-DELETE\n key: must(msg.old ?? msg.key),\n },\n ],\n ];\n }\n\n case 'update': {\n return [\n [\n 'data',\n {\n ...msg,\n relation: withoutColumns(msg.relation),\n // https://www.postgresql.org/docs/current/protocol-logicalrep-message-formats.html#PROTOCOL-LOGICALREP-MESSAGE-FORMATS-UPDATE\n key: msg.old ?? msg.key,\n },\n ],\n ];\n }\n\n case 'insert':\n return [['data', {...msg, relation: withoutColumns(msg.relation)}]];\n case 'truncate':\n return [\n ['data', {...msg, relations: msg.relations.map(withoutColumns)}],\n ];\n\n case 'message':\n if (msg.prefix !== this.#shardPrefix) {\n this.#lc.debug?.('ignoring message for different shard', msg.prefix);\n return [];\n }\n return this.#handleCustomMessage(msg);\n\n case 'commit':\n return [\n ['commit', msg, {watermark: toLexiVersion(must(msg.commitLsn))}],\n ];\n\n case 'relation':\n return this.#handleRelation(msg);\n case 'type':\n return []; // Nothing need be done for custom types.\n case 'origin':\n // No need to detect replication loops since we are not a\n // PG replication source.\n return [];\n default:\n msg satisfies never;\n throw new Error(`Unexpected message type ${stringify(msg)}`);\n }\n }\n\n #preSchema: PublishedSchema | undefined;\n\n #handleCustomMessage(msg: MessageMessage) {\n const event = this.#parseReplicationEvent(msg.content);\n // Cancel manual schema adjustment timeouts when an upstream schema change\n // is about to happen, so as to avoid interfering / redundant work.\n clearTimeout(this.#replicaIdentityTimer);\n\n if (event.type === 'ddlStart') {\n // Store the schema in order to diff it with a potential ddlUpdate.\n this.#preSchema = event.schema;\n return [];\n }\n // ddlUpdate\n const changes = this.#makeSchemaChanges(\n must(this.#preSchema, `ddlUpdate received without a ddlStart`),\n event,\n ).map(change => ['data', change] satisfies Data);\n\n this.#lc\n .withContext('query', event.context.query)\n .info?.(`${changes.length} schema change(s)`, changes);\n\n const replicaIdentities = replicaIdentitiesForTablesWithoutPrimaryKeys(\n event.schema,\n );\n if (replicaIdentities) {\n this.#replicaIdentityTimer = setTimeout(async () => {\n try {\n await replicaIdentities.apply(this.#lc, this.#upstreamDB);\n } catch (err) {\n this.#lc.warn?.(`error setting replica identities`, err);\n }\n }, SET_REPLICA_IDENTITY_DELAY_MS);\n }\n\n return changes;\n }\n\n /**\n * A note on operation order:\n *\n * Postgres will drop related indexes when columns are dropped,\n * but SQLite will error instead (https://sqlite.org/forum/forumpost/2e62dba69f?t=c&hist).\n * The current workaround is to drop indexes first.\n *\n * Also note that although it should not be possible to both rename and\n * add/drop tables/columns in a single statement, the operations are\n * ordered to handle that possibility, by always dropping old entities,\n * then modifying kept entities, and then adding new entities.\n *\n * Thus, the order of replicating DDL updates is:\n * - drop indexes\n * - drop tables\n * - alter tables\n * - drop columns\n * - alter columns\n * - add columns\n * - create tables\n * - create indexes\n *\n * In the future the replication logic should be improved to handle this\n * behavior in SQLite by dropping dependent indexes manually before dropping\n * columns. This, for example, would be needed to properly support changing\n * the type of a column that's indexed.\n */\n #makeSchemaChanges(\n preSchema: PublishedSchema,\n update: DdlUpdateEvent,\n ): DataChange[] {\n try {\n const [prevTbl, prevIdx] = specsByID(preSchema);\n const [nextTbl, nextIdx] = specsByID(update.schema);\n const changes: DataChange[] = [];\n\n // Validate the new table schemas\n for (const table of nextTbl.values()) {\n validate(this.#lc, table, update.schema.indexes);\n }\n\n const [droppedIdx, createdIdx] = symmetricDifferences(prevIdx, nextIdx);\n for (const id of droppedIdx) {\n const {schema, name} = must(prevIdx.get(id));\n changes.push({tag: 'drop-index', id: {schema, name}});\n }\n\n // DROP\n const [droppedTbl, createdTbl] = symmetricDifferences(prevTbl, nextTbl);\n for (const id of droppedTbl) {\n const {schema, name} = must(prevTbl.get(id));\n changes.push({tag: 'drop-table', id: {schema, name}});\n }\n // ALTER\n const tables = intersection(prevTbl, nextTbl);\n for (const id of tables) {\n changes.push(\n ...this.#getTableChanges(\n must(prevTbl.get(id)),\n must(nextTbl.get(id)),\n ),\n );\n }\n // CREATE\n for (const id of createdTbl) {\n const spec = must(nextTbl.get(id));\n changes.push({tag: 'create-table', spec});\n }\n\n // Add indexes last since they may reference tables / columns that need\n // to be created first.\n for (const id of createdIdx) {\n const spec = must(nextIdx.get(id));\n changes.push({tag: 'create-index', spec});\n }\n return changes;\n } catch (e) {\n throw new UnsupportedSchemaChangeError(String(e), update, {cause: e});\n }\n }\n\n #getTableChanges(oldTable: TableSpec, newTable: TableSpec): DataChange[] {\n const changes: DataChange[] = [];\n if (\n oldTable.schema !== newTable.schema ||\n oldTable.name !== newTable.name\n ) {\n changes.push({\n tag: 'rename-table',\n old: {schema: oldTable.schema, name: oldTable.name},\n new: {schema: newTable.schema, name: newTable.name},\n });\n }\n const table = {schema: newTable.schema, name: newTable.name};\n const oldColumns = columnsByID(oldTable.columns);\n const newColumns = columnsByID(newTable.columns);\n\n // DROP\n const [dropped, added] = symmetricDifferences(oldColumns, newColumns);\n for (const id of dropped) {\n const {name: column} = must(oldColumns.get(id));\n changes.push({tag: 'drop-column', table, column});\n }\n\n // ALTER\n const both = intersection(oldColumns, newColumns);\n for (const id of both) {\n const {name: oldName, ...oldSpec} = must(oldColumns.get(id));\n const {name: newName, ...newSpec} = must(newColumns.get(id));\n // The three things that we care about are:\n // 1. name\n // 2. type\n // 3. not-null\n if (\n oldName !== newName ||\n oldSpec.dataType !== newSpec.dataType ||\n oldSpec.notNull !== newSpec.notNull\n ) {\n changes.push({\n tag: 'update-column',\n table,\n old: {name: oldName, spec: oldSpec},\n new: {name: newName, spec: newSpec},\n });\n }\n }\n\n // ADD\n for (const id of added) {\n const {name, ...spec} = must(newColumns.get(id));\n const column = {name, spec};\n // Validate that the ChangeProcessor will accept the column change.\n mapPostgresToLiteColumn(table.name, column);\n changes.push({tag: 'add-column', table, column});\n }\n return changes;\n }\n\n #parseReplicationEvent(content: Uint8Array) {\n const str =\n content instanceof Buffer\n ? content.toString('utf-8')\n : new TextDecoder().decode(content);\n const json = JSON.parse(str);\n return v.parse(json, replicationEventSchema, 'passthrough');\n }\n\n /**\n * If `ddlDetection === true`, relation messages are irrelevant,\n * as schema changes are detected by event triggers that\n * emit custom messages.\n *\n * For degraded-mode replication (`ddlDetection === false`):\n * 1. query the current published schemas on upstream\n * 2. compare that with the InternalShardConfig.initialSchema\n * 3. compare that with the incoming MessageRelation\n * 4. On any discrepancy, throw an UnsupportedSchemaChangeError\n * to halt replication.\n *\n * Note that schemas queried in step [1] will be *post-transaction*\n * schemas, which are not necessarily suitable for actually processing\n * the statements in the transaction being replicated. In other words,\n * this mechanism cannot be used to reliably *replicate* schema changes.\n * However, they serve the purpose determining if schemas have changed.\n */\n async #handleRelation(rel: PostgresRelation): Promise<ChangeStreamData[]> {\n const {publications, ddlDetection} = this.#shardConfig;\n if (ddlDetection) {\n return [];\n }\n const currentSchema = await getPublicationInfo(\n this.#upstreamDB,\n publications,\n );\n const difference = getSchemaDifference(this.#initialSchema, currentSchema);\n if (difference !== null) {\n throw new MissingEventTriggerSupport(difference);\n }\n // Even if the currentSchema is equal to the initialSchema, the\n // MessageRelation itself must be checked to detect transient\n // schema changes within the transaction (e.g. adding and dropping\n // a table, or renaming a column and then renaming it back).\n const orel = this.#initialSchema.tables.find(\n t => t.oid === rel.relationOid,\n );\n if (!orel) {\n // Can happen if a table is created and then dropped in the same transaction.\n throw new MissingEventTriggerSupport(\n `relation not in initialSchema: ${stringify(rel)}`,\n );\n }\n if (relationDifferent(orel, rel)) {\n throw new MissingEventTriggerSupport(\n `relation has changed within the transaction: ${stringify(orel)} vs ${stringify(rel)}`,\n );\n }\n return [];\n }\n}\n\nfunction getSchemaDifference(\n a: PublishedSchema,\n b: PublishedSchema,\n): string | null {\n // Note: ignore indexes since changes need not to halt replication\n if (a.tables.length !== b.tables.length) {\n return `tables created or dropped`;\n }\n for (let i = 0; i < a.tables.length; i++) {\n const at = a.tables[i];\n const bt = b.tables[i];\n const difference = getTableDifference(at, bt);\n if (difference) {\n return difference;\n }\n }\n return null;\n}\n\n// ColumnSpec comparator\nconst byColumnPos = (a: [string, ColumnSpec], b: [string, ColumnSpec]) =>\n a[1].pos < b[1].pos ? -1 : a[1].pos > b[1].pos ? 1 : 0;\n\nfunction getTableDifference(\n a: PublishedTableSpec,\n b: PublishedTableSpec,\n): string | null {\n if (a.oid !== b.oid || a.schema !== b.schema || a.name !== b.name) {\n return `Table \"${a.name}\" differs from table \"${b.name}\"`;\n }\n if (!deepEqual(a.primaryKey, b.primaryKey)) {\n return `Primary key of table \"${a.name}\" has changed`;\n }\n const acols = Object.entries(a.columns).sort(byColumnPos);\n const bcols = Object.entries(b.columns).sort(byColumnPos);\n if (\n acols.length !== bcols.length ||\n acols.some(([aname, acol], i) => {\n const [bname, bcol] = bcols[i];\n return (\n aname !== bname ||\n acol.pos !== bcol.pos ||\n acol.typeOID !== bcol.typeOID ||\n acol.notNull !== bcol.notNull\n );\n })\n ) {\n return `Columns of table \"${a.name}\" have changed`;\n }\n return null;\n}\n\nexport function relationDifferent(a: PublishedTableSpec, b: PostgresRelation) {\n if (a.oid !== b.relationOid || a.schema !== b.schema || a.name !== b.name) {\n return true;\n }\n if (\n // The MessageRelation's `keyColumns` field contains the columns in column\n // declaration order, whereas the PublishedTableSpec's `primaryKey`\n // contains the columns in primary key (i.e. index) order. Do an\n // order-agnostic compare here since it is not possible to detect\n // key-order changes from the MessageRelation message alone.\n b.replicaIdentity === 'default' &&\n !equals(new Set(a.primaryKey), new Set(b.keyColumns))\n ) {\n return true;\n }\n const acols = Object.entries(a.columns).sort(byColumnPos);\n const bcols = b.columns;\n return (\n acols.length !== bcols.length ||\n acols.some(([aname, acol], i) => {\n const bcol = bcols[i];\n return aname !== bcol.name || acol.typeOID !== bcol.typeOid;\n })\n );\n}\n\nfunction translateError(e: unknown): Error {\n if (!(e instanceof Error)) {\n return new Error(String(e));\n }\n if (e instanceof postgres.PostgresError && e.code === PG_ADMIN_SHUTDOWN) {\n return new ShutdownSignal(e);\n }\n return e;\n}\nconst idString = (id: Identifier) => `${id.schema}.${id.name}`;\n\nfunction specsByID(published: PublishedSchema) {\n return [\n // It would have been nice to use a CustomKeyMap here, but we rely on set-utils\n // operations which use plain Sets.\n new Map(published.tables.map(t => [t.oid, t])),\n new Map(published.indexes.map(i => [idString(i), i])),\n ] as const;\n}\n\nfunction columnsByID(\n columns: Record<string, ColumnSpec>,\n): Map<number, ColumnSpec & {name: string}> {\n const colsByID = new Map<number, ColumnSpec & {name: string}>();\n for (const [name, spec] of Object.entries(columns)) {\n // The `pos` field is the `attnum` in `pg_attribute`, which is a stable\n // identifier for the column in this table (i.e. never reused).\n colsByID.set(spec.pos, {...spec, name});\n }\n return colsByID;\n}\n\n// Avoid sending the `columns` from the Postgres MessageRelation message.\n// They are not used downstream and the message can be large.\nfunction withoutColumns(relation: PostgresRelation): MessageRelation {\n const {columns: _, ...rest} = relation;\n return rest;\n}\n\nclass UnsupportedSchemaChangeError extends Error {\n readonly name = 'UnsupportedSchemaChangeError';\n readonly description: string;\n readonly ddlUpdate: DdlUpdateEvent;\n\n constructor(\n description: string,\n ddlUpdate: DdlUpdateEvent,\n options?: ErrorOptions,\n ) {\n super(\n `Replication halted. Resync the replica to recover: ${description}`,\n options,\n );\n this.description = description;\n this.ddlUpdate = ddlUpdate;\n }\n}\n\nclass MissingEventTriggerSupport extends Error {\n readonly name = 'MissingEventTriggerSupport';\n\n constructor(msg: string) {\n super(\n `${msg}. Schema changes cannot be reliably replicated without event trigger support.`,\n );\n }\n}\n\n// TODO(0xcadams): should this be a ProtocolError?\nclass ShutdownSignal extends AbortError {\n readonly name = 'ShutdownSignal';\n\n constructor(cause: unknown) {\n super(\n 'shutdown signal received (e.g. another zero-cache taking over the replication stream)',\n {\n cause,\n },\n );\n }\n}\n"],"names":["v.parse"],"mappings":"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"}
1
+ {"version":3,"file":"change-source.js","sources":["../../../../../../../zero-cache/src/services/change-source/pg/change-source.ts"],"sourcesContent":["import {\n PG_ADMIN_SHUTDOWN,\n PG_OBJECT_IN_USE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport postgres from 'postgres';\nimport {AbortError} from '../../../../../shared/src/abort-error.ts';\nimport {stringify} from '../../../../../shared/src/bigint-json.ts';\nimport {deepEqual} from '../../../../../shared/src/json.ts';\nimport {must} from '../../../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../../../shared/src/resolved-promises.ts';\nimport {\n equals,\n intersection,\n symmetricDifferences,\n} from '../../../../../shared/src/set-utils.ts';\nimport {sleep} from '../../../../../shared/src/sleep.ts';\nimport * as v from '../../../../../shared/src/valita.ts';\nimport {Database} from '../../../../../zqlite/src/db.ts';\nimport {mapPostgresToLiteColumn} from '../../../db/pg-to-lite.ts';\nimport type {\n ColumnSpec,\n PublishedTableSpec,\n TableSpec,\n} from '../../../db/specs.ts';\nimport {StatementRunner} from '../../../db/statements.ts';\nimport {\n oneAfter,\n versionFromLexi,\n versionToLexi,\n type LexiVersion,\n} from '../../../types/lexi-version.ts';\nimport {pgClient, type PostgresDB} from '../../../types/pg.ts';\nimport {\n upstreamSchema,\n type ShardConfig,\n type ShardID,\n} from '../../../types/shards.ts';\nimport type {Sink} from '../../../types/streams.ts';\nimport {Subscription, type PendingResult} from '../../../types/subscription.ts';\nimport type {\n ChangeSource,\n ChangeStream,\n} from '../../change-streamer/change-streamer-service.ts';\nimport {AutoResetSignal} from '../../change-streamer/schema/tables.ts';\nimport {\n getSubscriptionState,\n type SubscriptionState,\n} from '../../replicator/schema/replication-state.ts';\nimport type {JSONObject} from '../protocol/current.ts';\nimport type {\n DataChange,\n Identifier,\n MessageRelation,\n} from '../protocol/current/data.ts';\nimport type {\n ChangeStreamData,\n ChangeStreamMessage,\n Data,\n} from '../protocol/current/downstream.ts';\nimport {type InitialSyncOptions} from './initial-sync.ts';\nimport type {\n Message,\n MessageMessage,\n MessageRelation as PostgresRelation,\n} from './logical-replication/pgoutput.types.ts';\nimport {subscribe} from './logical-replication/stream.ts';\nimport {fromBigInt, toLexiVersion, type LSN} from './lsn.ts';\nimport {replicationEventSchema, type DdlUpdateEvent} from './schema/ddl.ts';\nimport {updateShardSchema} from './schema/init.ts';\nimport {getPublicationInfo, type PublishedSchema} from './schema/published.ts';\nimport {\n dropShard,\n getInternalShardConfig,\n getReplicaAtVersion,\n internalPublicationPrefix,\n legacyReplicationSlot,\n replicaIdentitiesForTablesWithoutPrimaryKeys,\n replicationSlotExpression,\n type InternalShardConfig,\n type Replica,\n} from './schema/shard.ts';\nimport {validate} from './schema/validation.ts';\nimport {initSyncSchema} from './sync-schema.ts';\n\n/**\n * Initializes a Postgres change source, including the initial sync of the\n * replica, before streaming changes from the corresponding logical replication\n * stream.\n */\nexport async function initializePostgresChangeSource(\n lc: LogContext,\n upstreamURI: string,\n shard: ShardConfig,\n replicaDbFile: string,\n syncOptions: InitialSyncOptions,\n): Promise<{subscriptionState: SubscriptionState; changeSource: ChangeSource}> {\n await initSyncSchema(\n lc,\n `replica-${shard.appID}-${shard.shardNum}`,\n shard,\n replicaDbFile,\n upstreamURI,\n syncOptions,\n );\n\n const replica = new Database(lc, replicaDbFile);\n const subscriptionState = getSubscriptionState(new StatementRunner(replica));\n replica.close();\n\n // Check that upstream is properly setup, and throw an AutoReset to re-run\n // initial sync if not.\n const db = pgClient(lc, upstreamURI);\n try {\n const upstreamReplica = await checkAndUpdateUpstream(\n lc,\n db,\n shard,\n subscriptionState,\n );\n\n const changeSource = new PostgresChangeSource(\n lc,\n upstreamURI,\n shard,\n upstreamReplica,\n );\n\n return {subscriptionState, changeSource};\n } finally {\n await db.end();\n }\n}\n\nasync function checkAndUpdateUpstream(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardConfig,\n {replicaVersion, publications: subscribed}: SubscriptionState,\n) {\n // Perform any shard schema updates\n await updateShardSchema(lc, sql, shard, replicaVersion);\n\n const upstreamReplica = await getReplicaAtVersion(\n lc,\n sql,\n shard,\n replicaVersion,\n );\n if (!upstreamReplica) {\n throw new AutoResetSignal(\n `No replication slot for replica at version ${replicaVersion}`,\n );\n }\n\n // Verify that the publications match what is being replicated.\n const requested = [...shard.publications].sort();\n const replicated = upstreamReplica.publications\n .filter(p => !p.startsWith(internalPublicationPrefix(shard)))\n .sort();\n if (!deepEqual(requested, replicated)) {\n lc.warn?.(`Dropping shard to change publications to: [${requested}]`);\n await sql.unsafe(dropShard(shard.appID, shard.shardNum));\n throw new AutoResetSignal(\n `Requested publications [${requested}] do not match configured ` +\n `publications: [${replicated}]`,\n );\n }\n\n // Sanity check: The subscription state on the replica should have the\n // same publications. This should be guaranteed by the equivalence of the\n // replicaVersion, but it doesn't hurt to verify.\n if (!deepEqual(upstreamReplica.publications, subscribed)) {\n throw new AutoResetSignal(\n `Upstream publications [${upstreamReplica.publications}] do not ` +\n `match subscribed publications [${subscribed}]`,\n );\n }\n\n // Verify that the publications exist.\n const exists = await sql`\n SELECT pubname FROM pg_publication WHERE pubname IN ${sql(subscribed)};\n `.values();\n if (exists.length !== subscribed.length) {\n throw new AutoResetSignal(\n `Upstream publications [${exists.flat()}] do not contain ` +\n `all subscribed publications [${subscribed}]`,\n );\n }\n\n const {slot} = upstreamReplica;\n const result = await sql<\n {restartLSN: LSN | null; walStatus: string | null}[]\n > /*sql*/ `\n SELECT restart_lsn as \"restartLSN\", wal_status as \"walStatus\" FROM pg_replication_slots\n WHERE slot_name = ${slot}`;\n if (result.length === 0) {\n throw new AutoResetSignal(`replication slot ${slot} is missing`);\n }\n const [{restartLSN, walStatus}] = result;\n if (restartLSN === null || walStatus === 'lost') {\n throw new AutoResetSignal(\n `replication slot ${slot} has been invalidated for exceeding the max_slot_wal_keep_size`,\n );\n }\n return upstreamReplica;\n}\n\n/**\n * Postgres implementation of a {@link ChangeSource} backed by a logical\n * replication stream.\n */\nclass PostgresChangeSource implements ChangeSource {\n readonly #lc: LogContext;\n readonly #upstreamUri: string;\n readonly #shard: ShardID;\n readonly #replica: Replica;\n\n constructor(\n lc: LogContext,\n upstreamUri: string,\n shard: ShardID,\n replica: Replica,\n ) {\n this.#lc = lc.withContext('component', 'change-source');\n this.#upstreamUri = upstreamUri;\n this.#shard = shard;\n this.#replica = replica;\n }\n\n async startStream(clientWatermark: string): Promise<ChangeStream> {\n const db = pgClient(this.#lc, this.#upstreamUri, {}, 'json-as-string');\n const {slot} = this.#replica;\n\n let cleanup = promiseVoid;\n try {\n ({cleanup} = await this.#stopExistingReplicationSlotSubscribers(\n db,\n slot,\n ));\n const config = await getInternalShardConfig(db, this.#shard);\n this.#lc.info?.(`starting replication stream@${slot}`);\n return await this.#startStream(db, slot, clientWatermark, config);\n } finally {\n void cleanup.then(() => db.end());\n }\n }\n\n async #startStream(\n db: PostgresDB,\n slot: string,\n clientWatermark: string,\n shardConfig: InternalShardConfig,\n ): Promise<ChangeStream> {\n const clientStart = oneAfter(clientWatermark);\n const {messages, acks} = await subscribe(\n this.#lc,\n db,\n slot,\n [...shardConfig.publications],\n versionFromLexi(clientStart),\n );\n\n const changes = Subscription.create<ChangeStreamMessage>({\n cleanup: () => messages.cancel(),\n });\n const acker = new Acker(acks);\n\n const changeMaker = new ChangeMaker(\n this.#lc,\n this.#shard,\n shardConfig,\n this.#replica.initialSchema,\n this.#upstreamUri,\n );\n\n void (async function () {\n try {\n for await (const [lsn, msg] of messages) {\n if (msg.tag === 'keepalive') {\n changes.push(['status', msg, {watermark: versionToLexi(lsn)}]);\n continue;\n }\n let last: PendingResult | undefined;\n for (const change of await changeMaker.makeChanges(lsn, msg)) {\n last = changes.push(change);\n }\n await last?.result; // Allow the change-streamer to push back.\n }\n } catch (e) {\n changes.fail(translateError(e));\n }\n })();\n\n this.#lc.info?.(\n `started replication stream@${slot} from ${clientWatermark} (replicaVersion: ${\n this.#replica.version\n })`,\n );\n\n return {\n changes,\n acks: {push: status => acker.ack(status[2].watermark)},\n };\n }\n\n /**\n * Stops replication slots associated with this shard, and returns\n * a `cleanup` task that drops any slot other than the specified\n * `slotToKeep`.\n *\n * Note that replication slots created after `slotToKeep` (as indicated by\n * the timestamp suffix) are preserved, as those are newly syncing replicas\n * that will soon take over the slot.\n */\n async #stopExistingReplicationSlotSubscribers(\n sql: PostgresDB,\n slotToKeep: string,\n ): Promise<{cleanup: Promise<void>}> {\n const slotExpression = replicationSlotExpression(this.#shard);\n const legacySlotName = legacyReplicationSlot(this.#shard);\n\n // Note: `slot_name <= slotToKeep` uses a string compare of the millisecond\n // timestamp, which works until it exceeds 13 digits (sometime in 2286).\n const result = await sql<\n {slot: string; pid: string | null; terminated: boolean | null}[]\n >`\n SELECT slot_name as slot, pg_terminate_backend(active_pid) as terminated, active_pid as pid\n FROM pg_replication_slots \n WHERE (slot_name LIKE ${slotExpression} OR slot_name = ${legacySlotName})\n AND slot_name <= ${slotToKeep}`;\n this.#lc.info?.(`terminated replication slots: ${JSON.stringify(result)}`);\n if (result.length === 0) {\n const shardSlots = await sql<\n {\n slot: string;\n active: boolean;\n pid: string | null;\n }[]\n >`\n SELECT slot_name as slot, active, active_pid as pid\n FROM pg_replication_slots\n WHERE slot_name LIKE ${slotExpression} OR slot_name = ${legacySlotName}\n ORDER BY slot_name`;\n this.#lc.warn?.(\n `slot ${slotToKeep} not found while cleaning subscribers; shard slots at time of failure: ${JSON.stringify(\n shardSlots,\n )}`,\n );\n throw new AbortError(\n `replication slot ${slotToKeep} is missing. A different ` +\n `replication-manager should now be running on a new ` +\n `replication slot.`,\n );\n }\n // Clear the state of the older replicas.\n const replicasTable = `${upstreamSchema(this.#shard)}.replicas`;\n const replicasBefore = await sql<{slot: string; version: string}[]>`\n SELECT slot, version FROM ${sql(replicasTable)} ORDER BY slot`;\n this.#lc.info?.(\n `replicas before cleanup (slotToKeep=${slotToKeep}): ${JSON.stringify(\n replicasBefore,\n )}`,\n );\n await sql<{slot: string; version: string}[]>`\n DELETE FROM ${sql(replicasTable)}\n WHERE slot < ${slotToKeep}`;\n const replicasAfter = await sql<{slot: string; version: string}[]>`\n SELECT slot, version FROM ${sql(replicasTable)} ORDER BY slot`;\n this.#lc.info?.(\n `replicas after cleanup (slotToKeep=${slotToKeep}): ${JSON.stringify(\n replicasAfter,\n )}`,\n );\n\n const pids = result.filter(({pid}) => pid !== null).map(({pid}) => pid);\n if (pids.length) {\n this.#lc.info?.(`signaled subscriber ${pids} to shut down`);\n }\n const otherSlots = result\n .filter(({slot}) => slot !== slotToKeep)\n .map(({slot}) => slot);\n return {\n cleanup: otherSlots.length\n ? this.#dropReplicationSlots(sql, otherSlots)\n : promiseVoid,\n };\n }\n\n async #dropReplicationSlots(sql: PostgresDB, slots: string[]) {\n this.#lc.info?.(`dropping other replication slot(s) ${slots}`);\n for (let i = 0; i < 5; i++) {\n try {\n await sql`\n SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots\n WHERE slot_name IN ${sql(slots)}\n `;\n this.#lc.info?.(`successfully dropped ${slots}`);\n return;\n } catch (e) {\n // error: replication slot \"zero_slot_change_source_test_id\" is active for PID 268\n if (\n e instanceof postgres.PostgresError &&\n e.code === PG_OBJECT_IN_USE\n ) {\n // The freeing up of the replication slot is not transactional;\n // sometimes it takes time for Postgres to consider the slot\n // inactive.\n this.#lc.debug?.(`attempt ${i + 1}: ${String(e)}`, e);\n } else {\n this.#lc.warn?.(`error dropping ${slots}`, e);\n }\n await sleep(1000);\n }\n }\n this.#lc.warn?.(`maximum attempts exceeded dropping ${slots}`);\n }\n}\n\n// Exported for testing.\nexport class Acker {\n #acks: Sink<bigint>;\n #keepaliveTimer: NodeJS.Timeout | undefined;\n\n constructor(acks: Sink<bigint>) {\n this.#acks = acks;\n }\n\n keepalive() {\n // Sets a timeout to send a standby status update in response to\n // a primary keepalive message.\n //\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-PRIMARY-KEEPALIVE-MESSAGE\n //\n // A primary keepalive message is streamed to the change-streamer as a\n // 'status' message, which in turn responds with an ack. However, in the\n // event that the change-streamer is backed up processing preceding\n // changes, this timeout will fire to send a status update that does not\n // change the confirmed flush position. This timeout must be shorter than\n // the `wal_sender_timeout`, which defaults to 60 seconds.\n //\n // https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT\n this.#keepaliveTimer ??= setTimeout(() => this.#sendAck(), 1000);\n }\n\n ack(watermark: LexiVersion) {\n this.#sendAck(watermark);\n }\n\n #sendAck(watermark?: LexiVersion) {\n clearTimeout(this.#keepaliveTimer);\n this.#keepaliveTimer = undefined;\n\n // Note: Sending '0/0' means \"keep alive but do not update confirmed_flush_lsn\"\n // https://github.com/postgres/postgres/blob/3edc67d337c2e498dad1cd200e460f7c63e512e6/src/backend/replication/walsender.c#L2457\n const lsn = watermark ? versionFromLexi(watermark) : 0n;\n this.#acks.push(lsn);\n }\n}\n\ntype ReplicationError = {\n lsn: bigint;\n msg: Message;\n err: unknown;\n lastLogTime: number;\n};\n\nconst SET_REPLICA_IDENTITY_DELAY_MS = 500;\n\nclass ChangeMaker {\n readonly #lc: LogContext;\n readonly #shardPrefix: string;\n readonly #shardConfig: InternalShardConfig;\n readonly #initialSchema: PublishedSchema;\n readonly #upstreamDB: PostgresDB;\n\n #replicaIdentityTimer: NodeJS.Timeout | undefined;\n #error: ReplicationError | undefined;\n\n constructor(\n lc: LogContext,\n {appID, shardNum}: ShardID,\n shardConfig: InternalShardConfig,\n initialSchema: PublishedSchema,\n upstreamURI: string,\n ) {\n this.#lc = lc;\n // Note: This matches the prefix used in pg_logical_emit_message() in pg/schema/ddl.ts.\n this.#shardPrefix = `${appID}/${shardNum}`;\n this.#shardConfig = shardConfig;\n this.#initialSchema = initialSchema;\n this.#upstreamDB = pgClient(lc, upstreamURI, {\n ['idle_timeout']: 10, // only used occasionally\n connection: {['application_name']: 'zero-schema-change-detector'},\n });\n }\n\n async makeChanges(lsn: bigint, msg: Message): Promise<ChangeStreamMessage[]> {\n if (this.#error) {\n this.#logError(this.#error);\n return [];\n }\n try {\n return await this.#makeChanges(msg);\n } catch (err) {\n this.#error = {lsn, msg, err, lastLogTime: 0};\n this.#logError(this.#error);\n\n const message = `Unable to continue replication from LSN ${fromBigInt(lsn)}`;\n const errorDetails: JSONObject = {error: message};\n if (err instanceof UnsupportedSchemaChangeError) {\n errorDetails.reason = err.description;\n errorDetails.context = err.ddlUpdate.context;\n } else {\n errorDetails.reason = String(err);\n }\n\n // Rollback the current transaction to avoid dangling transactions in\n // downstream processors (i.e. changeLog, replicator).\n return [\n ['rollback', {tag: 'rollback'}],\n ['control', {tag: 'reset-required', message, errorDetails}],\n ];\n }\n }\n\n #logError(error: ReplicationError) {\n const {lsn, msg, err, lastLogTime} = error;\n const now = Date.now();\n\n // Output an error to logs as replication messages continue to be dropped,\n // at most once a minute.\n if (now - lastLogTime > 60_000) {\n this.#lc.error?.(\n `Unable to continue replication from LSN ${fromBigInt(lsn)}: ${String(\n err,\n )}`,\n err instanceof UnsupportedSchemaChangeError\n ? err.ddlUpdate.context\n : // 'content' can be a large byte Buffer. Exclude it from logging output.\n {...msg, content: undefined},\n );\n error.lastLogTime = now;\n }\n }\n\n // oxlint-disable-next-line require-await\n async #makeChanges(msg: Message): Promise<ChangeStreamData[]> {\n switch (msg.tag) {\n case 'begin':\n return [\n [\n 'begin',\n {...msg, json: 's'},\n {commitWatermark: toLexiVersion(must(msg.commitLsn))},\n ],\n ];\n\n case 'delete': {\n if (!(msg.key ?? msg.old)) {\n throw new Error(\n `Invalid DELETE msg (missing key): ${stringify(msg)}`,\n );\n }\n return [\n [\n 'data',\n {\n ...msg,\n relation: withoutColumns(msg.relation),\n // https://www.postgresql.org/docs/current/protocol-logicalrep-message-formats.html#PROTOCOL-LOGICALREP-MESSAGE-FORMATS-DELETE\n key: must(msg.old ?? msg.key),\n },\n ],\n ];\n }\n\n case 'update': {\n return [\n [\n 'data',\n {\n ...msg,\n relation: withoutColumns(msg.relation),\n // https://www.postgresql.org/docs/current/protocol-logicalrep-message-formats.html#PROTOCOL-LOGICALREP-MESSAGE-FORMATS-UPDATE\n key: msg.old ?? msg.key,\n },\n ],\n ];\n }\n\n case 'insert':\n return [['data', {...msg, relation: withoutColumns(msg.relation)}]];\n case 'truncate':\n return [\n ['data', {...msg, relations: msg.relations.map(withoutColumns)}],\n ];\n\n case 'message':\n if (msg.prefix !== this.#shardPrefix) {\n this.#lc.debug?.('ignoring message for different shard', msg.prefix);\n return [];\n }\n return this.#handleCustomMessage(msg);\n\n case 'commit':\n return [\n ['commit', msg, {watermark: toLexiVersion(must(msg.commitLsn))}],\n ];\n\n case 'relation':\n return this.#handleRelation(msg);\n case 'type':\n return []; // Nothing need be done for custom types.\n case 'origin':\n // No need to detect replication loops since we are not a\n // PG replication source.\n return [];\n default:\n msg satisfies never;\n throw new Error(`Unexpected message type ${stringify(msg)}`);\n }\n }\n\n #preSchema: PublishedSchema | undefined;\n\n #handleCustomMessage(msg: MessageMessage) {\n const event = this.#parseReplicationEvent(msg.content);\n // Cancel manual schema adjustment timeouts when an upstream schema change\n // is about to happen, so as to avoid interfering / redundant work.\n clearTimeout(this.#replicaIdentityTimer);\n\n if (event.type === 'ddlStart') {\n // Store the schema in order to diff it with a potential ddlUpdate.\n this.#preSchema = event.schema;\n return [];\n }\n // ddlUpdate\n const changes = this.#makeSchemaChanges(\n must(this.#preSchema, `ddlUpdate received without a ddlStart`),\n event,\n ).map(change => ['data', change] satisfies Data);\n\n this.#lc\n .withContext('query', event.context.query)\n .info?.(`${changes.length} schema change(s)`, changes);\n\n const replicaIdentities = replicaIdentitiesForTablesWithoutPrimaryKeys(\n event.schema,\n );\n if (replicaIdentities) {\n this.#replicaIdentityTimer = setTimeout(async () => {\n try {\n await replicaIdentities.apply(this.#lc, this.#upstreamDB);\n } catch (err) {\n this.#lc.warn?.(`error setting replica identities`, err);\n }\n }, SET_REPLICA_IDENTITY_DELAY_MS);\n }\n\n return changes;\n }\n\n /**\n * A note on operation order:\n *\n * Postgres will drop related indexes when columns are dropped,\n * but SQLite will error instead (https://sqlite.org/forum/forumpost/2e62dba69f?t=c&hist).\n * The current workaround is to drop indexes first.\n *\n * Also note that although it should not be possible to both rename and\n * add/drop tables/columns in a single statement, the operations are\n * ordered to handle that possibility, by always dropping old entities,\n * then modifying kept entities, and then adding new entities.\n *\n * Thus, the order of replicating DDL updates is:\n * - drop indexes\n * - drop tables\n * - alter tables\n * - drop columns\n * - alter columns\n * - add columns\n * - create tables\n * - create indexes\n *\n * In the future the replication logic should be improved to handle this\n * behavior in SQLite by dropping dependent indexes manually before dropping\n * columns. This, for example, would be needed to properly support changing\n * the type of a column that's indexed.\n */\n #makeSchemaChanges(\n preSchema: PublishedSchema,\n update: DdlUpdateEvent,\n ): DataChange[] {\n try {\n const [prevTbl, prevIdx] = specsByID(preSchema);\n const [nextTbl, nextIdx] = specsByID(update.schema);\n const changes: DataChange[] = [];\n\n // Validate the new table schemas\n for (const table of nextTbl.values()) {\n validate(this.#lc, table, update.schema.indexes);\n }\n\n const [droppedIdx, createdIdx] = symmetricDifferences(prevIdx, nextIdx);\n for (const id of droppedIdx) {\n const {schema, name} = must(prevIdx.get(id));\n changes.push({tag: 'drop-index', id: {schema, name}});\n }\n\n // DROP\n const [droppedTbl, createdTbl] = symmetricDifferences(prevTbl, nextTbl);\n for (const id of droppedTbl) {\n const {schema, name} = must(prevTbl.get(id));\n changes.push({tag: 'drop-table', id: {schema, name}});\n }\n // ALTER\n const tables = intersection(prevTbl, nextTbl);\n for (const id of tables) {\n changes.push(\n ...this.#getTableChanges(\n must(prevTbl.get(id)),\n must(nextTbl.get(id)),\n ),\n );\n }\n // CREATE\n for (const id of createdTbl) {\n const spec = must(nextTbl.get(id));\n changes.push({tag: 'create-table', spec});\n }\n\n // Add indexes last since they may reference tables / columns that need\n // to be created first.\n for (const id of createdIdx) {\n const spec = must(nextIdx.get(id));\n changes.push({tag: 'create-index', spec});\n }\n return changes;\n } catch (e) {\n throw new UnsupportedSchemaChangeError(String(e), update, {cause: e});\n }\n }\n\n #getTableChanges(oldTable: TableSpec, newTable: TableSpec): DataChange[] {\n const changes: DataChange[] = [];\n if (\n oldTable.schema !== newTable.schema ||\n oldTable.name !== newTable.name\n ) {\n changes.push({\n tag: 'rename-table',\n old: {schema: oldTable.schema, name: oldTable.name},\n new: {schema: newTable.schema, name: newTable.name},\n });\n }\n const table = {schema: newTable.schema, name: newTable.name};\n const oldColumns = columnsByID(oldTable.columns);\n const newColumns = columnsByID(newTable.columns);\n\n // DROP\n const [dropped, added] = symmetricDifferences(oldColumns, newColumns);\n for (const id of dropped) {\n const {name: column} = must(oldColumns.get(id));\n changes.push({tag: 'drop-column', table, column});\n }\n\n // ALTER\n const both = intersection(oldColumns, newColumns);\n for (const id of both) {\n const {name: oldName, ...oldSpec} = must(oldColumns.get(id));\n const {name: newName, ...newSpec} = must(newColumns.get(id));\n // The three things that we care about are:\n // 1. name\n // 2. type\n // 3. not-null\n if (\n oldName !== newName ||\n oldSpec.dataType !== newSpec.dataType ||\n oldSpec.notNull !== newSpec.notNull\n ) {\n changes.push({\n tag: 'update-column',\n table,\n old: {name: oldName, spec: oldSpec},\n new: {name: newName, spec: newSpec},\n });\n }\n }\n\n // ADD\n for (const id of added) {\n const {name, ...spec} = must(newColumns.get(id));\n const column = {name, spec};\n // Validate that the ChangeProcessor will accept the column change.\n mapPostgresToLiteColumn(table.name, column);\n changes.push({tag: 'add-column', table, column});\n }\n return changes;\n }\n\n #parseReplicationEvent(content: Uint8Array) {\n const str =\n content instanceof Buffer\n ? content.toString('utf-8')\n : new TextDecoder().decode(content);\n const json = JSON.parse(str);\n return v.parse(json, replicationEventSchema, 'passthrough');\n }\n\n /**\n * If `ddlDetection === true`, relation messages are irrelevant,\n * as schema changes are detected by event triggers that\n * emit custom messages.\n *\n * For degraded-mode replication (`ddlDetection === false`):\n * 1. query the current published schemas on upstream\n * 2. compare that with the InternalShardConfig.initialSchema\n * 3. compare that with the incoming MessageRelation\n * 4. On any discrepancy, throw an UnsupportedSchemaChangeError\n * to halt replication.\n *\n * Note that schemas queried in step [1] will be *post-transaction*\n * schemas, which are not necessarily suitable for actually processing\n * the statements in the transaction being replicated. In other words,\n * this mechanism cannot be used to reliably *replicate* schema changes.\n * However, they serve the purpose determining if schemas have changed.\n */\n async #handleRelation(rel: PostgresRelation): Promise<ChangeStreamData[]> {\n const {publications, ddlDetection} = this.#shardConfig;\n if (ddlDetection) {\n return [];\n }\n const currentSchema = await getPublicationInfo(\n this.#upstreamDB,\n publications,\n );\n const difference = getSchemaDifference(this.#initialSchema, currentSchema);\n if (difference !== null) {\n throw new MissingEventTriggerSupport(difference);\n }\n // Even if the currentSchema is equal to the initialSchema, the\n // MessageRelation itself must be checked to detect transient\n // schema changes within the transaction (e.g. adding and dropping\n // a table, or renaming a column and then renaming it back).\n const orel = this.#initialSchema.tables.find(\n t => t.oid === rel.relationOid,\n );\n if (!orel) {\n // Can happen if a table is created and then dropped in the same transaction.\n throw new MissingEventTriggerSupport(\n `relation not in initialSchema: ${stringify(rel)}`,\n );\n }\n if (relationDifferent(orel, rel)) {\n throw new MissingEventTriggerSupport(\n `relation has changed within the transaction: ${stringify(orel)} vs ${stringify(rel)}`,\n );\n }\n return [];\n }\n}\n\nfunction getSchemaDifference(\n a: PublishedSchema,\n b: PublishedSchema,\n): string | null {\n // Note: ignore indexes since changes need not to halt replication\n if (a.tables.length !== b.tables.length) {\n return `tables created or dropped`;\n }\n for (let i = 0; i < a.tables.length; i++) {\n const at = a.tables[i];\n const bt = b.tables[i];\n const difference = getTableDifference(at, bt);\n if (difference) {\n return difference;\n }\n }\n return null;\n}\n\n// ColumnSpec comparator\nconst byColumnPos = (a: [string, ColumnSpec], b: [string, ColumnSpec]) =>\n a[1].pos < b[1].pos ? -1 : a[1].pos > b[1].pos ? 1 : 0;\n\nfunction getTableDifference(\n a: PublishedTableSpec,\n b: PublishedTableSpec,\n): string | null {\n if (a.oid !== b.oid || a.schema !== b.schema || a.name !== b.name) {\n return `Table \"${a.name}\" differs from table \"${b.name}\"`;\n }\n if (!deepEqual(a.primaryKey, b.primaryKey)) {\n return `Primary key of table \"${a.name}\" has changed`;\n }\n const acols = Object.entries(a.columns).sort(byColumnPos);\n const bcols = Object.entries(b.columns).sort(byColumnPos);\n if (\n acols.length !== bcols.length ||\n acols.some(([aname, acol], i) => {\n const [bname, bcol] = bcols[i];\n return (\n aname !== bname ||\n acol.pos !== bcol.pos ||\n acol.typeOID !== bcol.typeOID ||\n acol.notNull !== bcol.notNull\n );\n })\n ) {\n return `Columns of table \"${a.name}\" have changed`;\n }\n return null;\n}\n\nexport function relationDifferent(a: PublishedTableSpec, b: PostgresRelation) {\n if (a.oid !== b.relationOid || a.schema !== b.schema || a.name !== b.name) {\n return true;\n }\n if (\n // The MessageRelation's `keyColumns` field contains the columns in column\n // declaration order, whereas the PublishedTableSpec's `primaryKey`\n // contains the columns in primary key (i.e. index) order. Do an\n // order-agnostic compare here since it is not possible to detect\n // key-order changes from the MessageRelation message alone.\n b.replicaIdentity === 'default' &&\n !equals(new Set(a.primaryKey), new Set(b.keyColumns))\n ) {\n return true;\n }\n const acols = Object.entries(a.columns).sort(byColumnPos);\n const bcols = b.columns;\n return (\n acols.length !== bcols.length ||\n acols.some(([aname, acol], i) => {\n const bcol = bcols[i];\n return aname !== bcol.name || acol.typeOID !== bcol.typeOid;\n })\n );\n}\n\nfunction translateError(e: unknown): Error {\n if (!(e instanceof Error)) {\n return new Error(String(e));\n }\n if (e instanceof postgres.PostgresError && e.code === PG_ADMIN_SHUTDOWN) {\n return new ShutdownSignal(e);\n }\n return e;\n}\nconst idString = (id: Identifier) => `${id.schema}.${id.name}`;\n\nfunction specsByID(published: PublishedSchema) {\n return [\n // It would have been nice to use a CustomKeyMap here, but we rely on set-utils\n // operations which use plain Sets.\n new Map(published.tables.map(t => [t.oid, t])),\n new Map(published.indexes.map(i => [idString(i), i])),\n ] as const;\n}\n\nfunction columnsByID(\n columns: Record<string, ColumnSpec>,\n): Map<number, ColumnSpec & {name: string}> {\n const colsByID = new Map<number, ColumnSpec & {name: string}>();\n for (const [name, spec] of Object.entries(columns)) {\n // The `pos` field is the `attnum` in `pg_attribute`, which is a stable\n // identifier for the column in this table (i.e. never reused).\n colsByID.set(spec.pos, {...spec, name});\n }\n return colsByID;\n}\n\n// Avoid sending the `columns` from the Postgres MessageRelation message.\n// They are not used downstream and the message can be large.\nfunction withoutColumns(relation: PostgresRelation): MessageRelation {\n const {columns: _, ...rest} = relation;\n return rest;\n}\n\nclass UnsupportedSchemaChangeError extends Error {\n readonly name = 'UnsupportedSchemaChangeError';\n readonly description: string;\n readonly ddlUpdate: DdlUpdateEvent;\n\n constructor(\n description: string,\n ddlUpdate: DdlUpdateEvent,\n options?: ErrorOptions,\n ) {\n super(\n `Replication halted. Resync the replica to recover: ${description}`,\n options,\n );\n this.description = description;\n this.ddlUpdate = ddlUpdate;\n }\n}\n\nclass MissingEventTriggerSupport extends Error {\n readonly name = 'MissingEventTriggerSupport';\n\n constructor(msg: string) {\n super(\n `${msg}. Schema changes cannot be reliably replicated without event trigger support.`,\n );\n }\n}\n\n// TODO(0xcadams): should this be a ProtocolError?\nclass ShutdownSignal extends AbortError {\n readonly name = 'ShutdownSignal';\n\n constructor(cause: unknown) {\n super(\n 'shutdown signal received (e.g. another zero-cache taking over the replication stream)',\n {\n cause,\n },\n );\n }\n}\n"],"names":["v.parse"],"mappings":"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"}
@@ -193,7 +193,7 @@ async function getReplicaAtVersion(lc, sql, shard, replicaVersion) {
193
193
  if (result.length === 0) {
194
194
  const allReplicas = await sql`
195
195
  SELECT * FROM ${schema}.replicas JOIN ${schema}."shardConfig" ON true`;
196
- lc.debug?.(`Replica not found in: ${JSON.stringify(allReplicas)}`);
196
+ lc.info?.(`Replica not found in: ${JSON.stringify(allReplicas)}`);
197
197
  return null;
198
198
  }
199
199
  return parse(result[0], replicaSchema, "passthrough");
@@ -1 +1 @@
1
- {"version":3,"file":"shard.js","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 * 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 {createEventTriggerStatements} from './ddl.ts';\nimport {\n getPublicationInfo,\n publishedSchema,\n type PublicationInfo,\n type PublishedSchema,\n} from './published.ts';\nimport {validate} from './validation.ts';\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\nexport function newReplicationSlot(shard: ShardID) {\n return replicationSlotPrefix(shard) + Date.now();\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}.\"schemaVersions\" (\n \"minSupportedVersion\" INT4,\n \"maxSupportedVersion\" INT4,\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 INSERT INTO ${app}.\"schemaVersions\" (\"lock\", \"minSupportedVersion\", \"maxSupportedVersion\")\n VALUES (true, 1, 1) ON CONFLICT DO NOTHING;\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].sort();\n assert(pubs.includes(metadataPublication));\n\n return /*sql*/ `\n CREATE SCHEMA IF NOT EXISTS ${shard};\n\n ${getClientsTableDefinition(shard)}\n ${getMutationsTableDefinition(shard)}\n\n DROP PUBLICATION IF EXISTS ${id(metadataPublication)};\n CREATE PUBLICATION ${id(metadataPublication)}\n FOR TABLE ${app}.\"schemaVersions\", ${app}.\"permissions\", TABLE ${shard}.\"clients\", ${shard}.\"mutations\";\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 \"slot\" TEXT PRIMARY KEY,\n \"version\" TEXT NOT NULL,\n \"initialSchema\" JSON NOT NULL\n );\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 replicaSchema = internalShardConfigSchema.extend({\n slot: v.string(),\n version: v.string(),\n initialSchema: publishedSchema,\n});\n\nexport type Replica = v.Infer<typeof replicaSchema>;\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\n// Called in initial-sync to store the exact schema that was initially synced.\nexport async function addReplica(\n sql: PostgresDB,\n shard: ShardID,\n slot: string,\n replicaVersion: string,\n {tables, indexes}: PublishedSchema,\n) {\n const schema = upstreamSchema(shard);\n const synced: PublishedSchema = {tables, indexes};\n await sql`\n INSERT INTO ${sql(schema)}.replicas (\"slot\", \"version\", \"initialSchema\")\n VALUES (${slot}, ${replicaVersion}, ${synced})`;\n}\n\nexport async function getReplicaAtVersion(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n replicaVersion: string,\n): Promise<Replica | null> {\n const schema = sql(upstreamSchema(shard));\n const result = await sql`\n SELECT * FROM ${schema}.replicas JOIN ${schema}.\"shardConfig\" ON true\n WHERE version = ${replicaVersion};\n `;\n if (result.length === 0) {\n // log out all the replicas and the joined shardConfig\n const allReplicas = await sql`\n SELECT * FROM ${schema}.replicas JOIN ${schema}.\"shardConfig\" ON true`;\n lc.debug?.(`Replica not found in: ${JSON.stringify(allReplicas)}`);\n return null;\n }\n return v.parse(result[0], replicaSchema, 'passthrough');\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(result.length === 1);\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 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 try {\n await tx.savepoint(sub => sub.unsafe(triggerSetup(shard)));\n } catch (e) {\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.pubtruncate ||\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, published.indexes));\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 (are already filtered out)\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 )) {\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"],"names":["v.object","v.array","v.string","v.boolean","v.parse"],"mappings":"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"}
1
+ {"version":3,"file":"shard.js","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 * 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 {createEventTriggerStatements} from './ddl.ts';\nimport {\n getPublicationInfo,\n publishedSchema,\n type PublicationInfo,\n type PublishedSchema,\n} from './published.ts';\nimport {validate} from './validation.ts';\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\nexport function newReplicationSlot(shard: ShardID) {\n return replicationSlotPrefix(shard) + Date.now();\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}.\"schemaVersions\" (\n \"minSupportedVersion\" INT4,\n \"maxSupportedVersion\" INT4,\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 INSERT INTO ${app}.\"schemaVersions\" (\"lock\", \"minSupportedVersion\", \"maxSupportedVersion\")\n VALUES (true, 1, 1) ON CONFLICT DO NOTHING;\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].sort();\n assert(pubs.includes(metadataPublication));\n\n return /*sql*/ `\n CREATE SCHEMA IF NOT EXISTS ${shard};\n\n ${getClientsTableDefinition(shard)}\n ${getMutationsTableDefinition(shard)}\n\n DROP PUBLICATION IF EXISTS ${id(metadataPublication)};\n CREATE PUBLICATION ${id(metadataPublication)}\n FOR TABLE ${app}.\"schemaVersions\", ${app}.\"permissions\", TABLE ${shard}.\"clients\", ${shard}.\"mutations\";\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 \"slot\" TEXT PRIMARY KEY,\n \"version\" TEXT NOT NULL,\n \"initialSchema\" JSON NOT NULL\n );\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 replicaSchema = internalShardConfigSchema.extend({\n slot: v.string(),\n version: v.string(),\n initialSchema: publishedSchema,\n});\n\nexport type Replica = v.Infer<typeof replicaSchema>;\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\n// Called in initial-sync to store the exact schema that was initially synced.\nexport async function addReplica(\n sql: PostgresDB,\n shard: ShardID,\n slot: string,\n replicaVersion: string,\n {tables, indexes}: PublishedSchema,\n) {\n const schema = upstreamSchema(shard);\n const synced: PublishedSchema = {tables, indexes};\n await sql`\n INSERT INTO ${sql(schema)}.replicas (\"slot\", \"version\", \"initialSchema\")\n VALUES (${slot}, ${replicaVersion}, ${synced})`;\n}\n\nexport async function getReplicaAtVersion(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n replicaVersion: string,\n): Promise<Replica | null> {\n const schema = sql(upstreamSchema(shard));\n const result = await sql`\n SELECT * FROM ${schema}.replicas JOIN ${schema}.\"shardConfig\" ON true\n WHERE version = ${replicaVersion};\n `;\n if (result.length === 0) {\n // log out all the replicas and the joined shardConfig\n const allReplicas = await sql`\n SELECT * FROM ${schema}.replicas JOIN ${schema}.\"shardConfig\" ON true`;\n lc.info?.(`Replica not found in: ${JSON.stringify(allReplicas)}`);\n return null;\n }\n return v.parse(result[0], replicaSchema, 'passthrough');\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(result.length === 1);\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 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 try {\n await tx.savepoint(sub => sub.unsafe(triggerSetup(shard)));\n } catch (e) {\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.pubtruncate ||\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, published.indexes));\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 (are already filtered out)\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 )) {\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"],"names":["v.object","v.array","v.string","v.boolean","v.parse"],"mappings":"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"}
@@ -214,6 +214,11 @@ class ChangeStreamerImpl {
214
214
  }
215
215
  async getChangeLogState() {
216
216
  const minWatermark = await this.#storer.getMinWatermarkForCatchup();
217
+ if (!minWatermark) {
218
+ this.#lc.warn?.(
219
+ `Unexpected empty changeLog. Resync if "Local replica watermark" errors arise`
220
+ );
221
+ }
217
222
  return {
218
223
  replicaVersion: this.#replicaVersion,
219
224
  minWatermark: minWatermark ?? this.#replicaVersion