@rocicorp/zero 1.12.0-canary.3 → 1.12.0-canary.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/_virtual/{_@oxc-project_runtime@0.130.0/helpers → _@oxc-project_runtime@0.154.0/helpers/esm}/usingCtx.js +2 -2
- package/out/analyze-query/src/analyze-cli.d.ts.map +1 -1
- package/out/analyze-query/src/analyze-cli.js +6 -4
- package/out/analyze-query/src/analyze-cli.js.map +1 -1
- package/out/analyze-query/src/bin-transform.js +4 -3
- package/out/analyze-query/src/bin-transform.js.map +1 -1
- package/out/analyze-query/src/zql-chain.d.ts +23 -0
- package/out/analyze-query/src/zql-chain.d.ts.map +1 -0
- package/out/analyze-query/src/zql-chain.js +162 -0
- package/out/analyze-query/src/zql-chain.js.map +1 -0
- package/out/ast-to-zql/src/ast-to-zql.js.map +1 -1
- package/out/ast-to-zql/src/bin.js +4 -3
- package/out/ast-to-zql/src/bin.js.map +1 -1
- package/out/ast-to-zql/src/format.js.map +1 -1
- package/out/datadog/src/datadog-log-sink.js +5 -4
- package/out/datadog/src/datadog-log-sink.js.map +1 -1
- package/out/otel/src/log-options.js.map +1 -1
- package/out/replicache/src/btree/node.js +13 -11
- package/out/replicache/src/btree/node.js.map +1 -1
- package/out/replicache/src/btree/read.js +4 -2
- package/out/replicache/src/btree/read.js.map +1 -1
- package/out/replicache/src/btree/splice.js.map +1 -1
- package/out/replicache/src/btree/write.js +9 -5
- package/out/replicache/src/btree/write.js.map +1 -1
- package/out/replicache/src/config.js +1 -2
- package/out/replicache/src/connection-loop.js.map +1 -1
- package/out/replicache/src/dag/chunk.js +1 -1
- package/out/replicache/src/dag/gc.js.map +1 -1
- package/out/replicache/src/dag/lazy-store.js +8 -7
- package/out/replicache/src/dag/lazy-store.js.map +1 -1
- package/out/replicache/src/dag/store-impl.js +1 -1
- package/out/replicache/src/dag/store-impl.js.map +1 -1
- package/out/replicache/src/dag/visitor.js.map +1 -1
- package/out/replicache/src/db/commit.js +5 -3
- package/out/replicache/src/db/commit.js.map +1 -1
- package/out/replicache/src/db/read.js +1 -1
- package/out/replicache/src/db/read.js.map +1 -1
- package/out/replicache/src/db/rebase.js.map +1 -1
- package/out/replicache/src/db/write.js +5 -2
- package/out/replicache/src/db/write.js.map +1 -1
- package/out/replicache/src/deleted-clients.js +3 -2
- package/out/replicache/src/deleted-clients.js.map +1 -1
- package/out/replicache/src/expo-sqlite.js +1 -2
- package/out/replicache/src/frozen-json.js +1 -1
- package/out/replicache/src/frozen-json.js.map +1 -1
- package/out/replicache/src/get-default-puller.js +1 -1
- package/out/replicache/src/hash.js +1 -1
- package/out/replicache/src/hash.js.map +1 -1
- package/out/replicache/src/impl.js +1 -2
- package/out/replicache/src/index-defs.js +5 -3
- package/out/replicache/src/index-defs.js.map +1 -1
- package/out/replicache/src/kv/expo-sqlite/store.js +1 -1
- package/out/replicache/src/kv/expo-sqlite/store.js.map +1 -1
- package/out/replicache/src/kv/idb-store.js +1 -1
- package/out/replicache/src/kv/idb-store.js.map +1 -1
- package/out/replicache/src/kv/mem-fallback-store.js +1 -1
- package/out/replicache/src/kv/mem-fallback-store.js.map +1 -1
- package/out/replicache/src/kv/mem-store.js.map +1 -1
- package/out/replicache/src/kv/op-sqlite/store.js.map +1 -1
- package/out/replicache/src/kv/op-sqlite/types.js.map +1 -1
- package/out/replicache/src/kv/read-impl.js.map +1 -1
- package/out/replicache/src/kv/sqlite-store.js +1 -1
- package/out/replicache/src/kv/sqlite-store.js.map +1 -1
- package/out/replicache/src/kv/write-impl-base.js +4 -1
- package/out/replicache/src/kv/write-impl-base.js.map +1 -1
- package/out/replicache/src/kv/write-impl.js.map +1 -1
- package/out/replicache/src/log-options.js +2 -1
- package/out/replicache/src/log-options.js.map +1 -1
- package/out/replicache/src/make-idb-name.js +1 -1
- package/out/replicache/src/new-client-channel.js +9 -7
- package/out/replicache/src/new-client-channel.js.map +1 -1
- package/out/replicache/src/on-persist-channel.js +1 -1
- package/out/replicache/src/on-persist-channel.js.map +1 -1
- package/out/replicache/src/op-sqlite.js +1 -2
- package/out/replicache/src/pending-mutations.js +2 -1
- package/out/replicache/src/pending-mutations.js.map +1 -1
- package/out/replicache/src/persist/client-gc.js +2 -2
- package/out/replicache/src/persist/client-gc.js.map +1 -1
- package/out/replicache/src/persist/client-group-gc.js +2 -2
- package/out/replicache/src/persist/client-group-gc.js.map +1 -1
- package/out/replicache/src/persist/client-groups.js +1 -1
- package/out/replicache/src/persist/client-groups.js.map +1 -1
- package/out/replicache/src/persist/clients.js +1 -1
- package/out/replicache/src/persist/clients.js.map +1 -1
- package/out/replicache/src/persist/collect-idb-databases.js +5 -4
- package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
- package/out/replicache/src/persist/gather-mem-only-visitor.js.map +1 -1
- package/out/replicache/src/persist/gather-not-cached-visitor.js.map +1 -1
- package/out/replicache/src/persist/heartbeat.js +2 -2
- package/out/replicache/src/persist/heartbeat.js.map +1 -1
- package/out/replicache/src/persist/idb-databases-store.js +1 -1
- package/out/replicache/src/persist/idb-databases-store.js.map +1 -1
- package/out/replicache/src/persist/make-client-id.js +1 -1
- package/out/replicache/src/persist/make-client-id.js.map +1 -1
- package/out/replicache/src/persist/persist.js +3 -2
- package/out/replicache/src/persist/persist.js.map +1 -1
- package/out/replicache/src/persist/refresh.js +8 -5
- package/out/replicache/src/persist/refresh.js.map +1 -1
- package/out/replicache/src/process-scheduler.js.map +1 -1
- package/out/replicache/src/replicache-impl.js +3 -3
- package/out/replicache/src/replicache-impl.js.map +1 -1
- package/out/replicache/src/scan-iterator.js.map +1 -1
- package/out/replicache/src/scan-options.js +7 -6
- package/out/replicache/src/scan-options.js.map +1 -1
- package/out/replicache/src/sqlite.js +1 -2
- package/out/replicache/src/subscriptions.js +1 -1
- package/out/replicache/src/subscriptions.js.map +1 -1
- package/out/replicache/src/sync/diff.js +3 -1
- package/out/replicache/src/sync/diff.js.map +1 -1
- package/out/replicache/src/sync/ids.js.map +1 -1
- package/out/replicache/src/sync/patch.js +2 -4
- package/out/replicache/src/sync/patch.js.map +1 -1
- package/out/replicache/src/sync/pull.js +3 -1
- package/out/replicache/src/sync/pull.js.map +1 -1
- package/out/replicache/src/sync/push.js.map +1 -1
- package/out/replicache/src/sync/request-id.js +1 -1
- package/out/replicache/src/sync/request-id.js.map +1 -1
- package/out/replicache/src/transactions.js +1 -1
- package/out/replicache/src/transactions.js.map +1 -1
- package/out/shared/src/arrays.js +4 -1
- package/out/shared/src/arrays.js.map +1 -1
- package/out/shared/src/asserts.js +1 -1
- package/out/shared/src/bigint-json.d.ts +9 -0
- package/out/shared/src/bigint-json.d.ts.map +1 -1
- package/out/shared/src/bigint-json.js +27 -2
- package/out/shared/src/bigint-json.js.map +1 -1
- package/out/shared/src/broadcast-channel.js +1 -1
- package/out/shared/src/btree-set.js +11 -8
- package/out/shared/src/btree-set.js.map +1 -1
- package/out/shared/src/cache.js.map +1 -1
- package/out/shared/src/config.js +1 -1
- package/out/shared/src/custom-key-map.js.map +1 -1
- package/out/shared/src/custom-key-set.js.map +1 -1
- package/out/shared/src/deep-clone.js +1 -1
- package/out/shared/src/document-visible.js.map +1 -1
- package/out/shared/src/dotenv.js.map +1 -1
- package/out/shared/src/error.js +1 -1
- package/out/shared/src/error.js.map +1 -1
- package/out/shared/src/hash.js +2 -2
- package/out/shared/src/hash.js.map +1 -1
- package/out/shared/src/iterables.js.map +1 -1
- package/out/shared/src/json-schema.js.map +1 -1
- package/out/shared/src/json.js.map +1 -1
- package/out/shared/src/logging-test-utils.js +1 -1
- package/out/shared/src/logging.js +1 -1
- package/out/shared/src/logging.js.map +1 -1
- package/out/shared/src/navigator.js +1 -1
- package/out/shared/src/options.js +9 -12
- package/out/shared/src/options.js.map +1 -1
- package/out/shared/src/promise-race.js.map +1 -1
- package/out/shared/src/queue.js.map +1 -1
- package/out/shared/src/record-proxy.js.map +1 -1
- package/out/shared/src/ring-buffer.js.map +1 -1
- package/out/shared/src/size-of-value.js +7 -10
- package/out/shared/src/size-of-value.js.map +1 -1
- package/out/shared/src/sorted-entries.js.map +1 -1
- package/out/shared/src/tdigest-schema.js.map +1 -1
- package/out/shared/src/tdigest.js.map +1 -1
- package/out/shared/src/valita.js +1 -1
- package/out/shared/src/valita.js.map +1 -1
- package/out/z2s/src/compiler.js +2 -4
- package/out/z2s/src/compiler.js.map +1 -1
- package/out/z2s/src/sql.js +1 -1
- package/out/z2s/src/sql.js.map +1 -1
- package/out/zero/package.js +211 -189
- package/out/zero/package.js.map +1 -1
- package/out/zero/src/adapters/drizzle.js +1 -2
- package/out/zero/src/adapters/kysely.js +1 -2
- package/out/zero/src/adapters/pg.js +1 -2
- package/out/zero/src/adapters/postgresjs.js +1 -2
- package/out/zero/src/adapters/prisma.js +1 -2
- package/out/zero/src/analyze.js +1 -2
- package/out/zero/src/change-protocol/v0.js +1 -2
- package/out/zero/src/expo-sqlite.js +1 -2
- package/out/zero/src/op-sqlite.js +1 -2
- package/out/zero/src/react-native.js.map +1 -1
- package/out/zero/src/sqlite.js +1 -2
- package/out/zero/src/zero-cache-dev.js +10 -9
- package/out/zero/src/zero-cache-dev.js.map +1 -1
- package/out/zero/src/zero-out.js +2 -1
- package/out/zero/src/zero-out.js.map +1 -1
- package/out/zero-cache/src/auth/auth.js +1 -1
- package/out/zero-cache/src/auth/auth.js.map +1 -1
- package/out/zero-cache/src/auth/jwt.js +4 -1
- package/out/zero-cache/src/auth/jwt.js.map +1 -1
- package/out/zero-cache/src/auth/load-permissions.js +1 -1
- package/out/zero-cache/src/auth/load-permissions.js.map +1 -1
- package/out/zero-cache/src/auth/read-authorizer.js +3 -2
- package/out/zero-cache/src/auth/read-authorizer.js.map +1 -1
- package/out/zero-cache/src/auth/write-authorizer.js +6 -17
- package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
- package/out/zero-cache/src/config/network.js +1 -1
- package/out/zero-cache/src/config/network.js.map +1 -1
- package/out/zero-cache/src/config/server-context.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +5 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +13 -2
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/custom/fetch.js +1 -1
- package/out/zero-cache/src/custom/fetch.js.map +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
- package/out/zero-cache/src/db/create.js.map +1 -1
- package/out/zero-cache/src/db/index-predicate.js.map +1 -1
- package/out/zero-cache/src/db/lite-tables.d.ts +2 -2
- package/out/zero-cache/src/db/lite-tables.d.ts.map +1 -1
- package/out/zero-cache/src/db/lite-tables.js.map +1 -1
- package/out/zero-cache/src/db/migration-lite.js +1 -1
- package/out/zero-cache/src/db/migration-lite.js.map +1 -1
- package/out/zero-cache/src/db/migration.js +5 -3
- package/out/zero-cache/src/db/migration.js.map +1 -1
- package/out/zero-cache/src/db/pg-copy-binary.js +4 -3
- package/out/zero-cache/src/db/pg-copy-binary.js.map +1 -1
- package/out/zero-cache/src/db/pg-copy.js +0 -1
- package/out/zero-cache/src/db/pg-copy.js.map +1 -1
- package/out/zero-cache/src/db/pg-to-lite.js +1 -1
- package/out/zero-cache/src/db/pg-to-lite.js.map +1 -1
- package/out/zero-cache/src/db/run-transaction.js.map +1 -1
- package/out/zero-cache/src/db/specs.js +7 -5
- package/out/zero-cache/src/db/specs.js.map +1 -1
- package/out/zero-cache/src/db/sqlite-corruption.js.map +1 -1
- package/out/zero-cache/src/db/statements.d.ts.map +1 -1
- package/out/zero-cache/src/db/statements.js +9 -2
- package/out/zero-cache/src/db/statements.js.map +1 -1
- package/out/zero-cache/src/db/transaction-pool.js +5 -3
- package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
- package/out/zero-cache/src/db/warmup.js.map +1 -1
- package/out/zero-cache/src/observability/events.js.map +1 -1
- package/out/zero-cache/src/observability/log-throttle.js.map +1 -1
- package/out/zero-cache/src/observability/metrics.js.map +1 -1
- package/out/zero-cache/src/scripts/decommission.js +1 -1
- package/out/zero-cache/src/scripts/deploy-permissions.js +5 -3
- package/out/zero-cache/src/scripts/deploy-permissions.js.map +1 -1
- package/out/zero-cache/src/scripts/permissions.js.map +1 -1
- package/out/zero-cache/src/server/anonymous-otel-start.js +2 -1
- package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +1 -1
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
- package/out/zero-cache/src/server/logging.js +1 -1
- package/out/zero-cache/src/server/logging.js.map +1 -1
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/otel-log-sink.js.map +1 -1
- package/out/zero-cache/src/server/otel-start.js.map +1 -1
- package/out/zero-cache/src/server/reaper.js +2 -2
- package/out/zero-cache/src/server/reaper.js.map +1 -1
- package/out/zero-cache/src/server/replicator.js +1 -1
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/runner/run-worker.js +2 -1
- package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
- package/out/zero-cache/src/server/runner/runtime.js +2 -1
- package/out/zero-cache/src/server/runner/runtime.js.map +1 -1
- package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
- package/out/zero-cache/src/server/shadow-syncer.js +1 -1
- package/out/zero-cache/src/server/shadow-syncer.js.map +1 -1
- package/out/zero-cache/src/server/slot-keeper.js.map +1 -1
- package/out/zero-cache/src/server/syncer-assigner.js.map +1 -1
- package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/server/syncer.js +9 -4
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/server/worker-dispatcher.js +1 -1
- package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
- package/out/zero-cache/src/server/worker-urls.js.map +1 -1
- package/out/zero-cache/src/services/analyze.js +2 -2
- package/out/zero-cache/src/services/analyze.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/backfill-manager.js +2 -2
- package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/init-cleanup.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-schema.js +4 -4
- package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +4 -4
- package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/backfill-metadata.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +4 -4
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source-init.js +3 -3
- package/out/zero-cache/src/services/change-source/pg/change-source-init.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js +6 -8
- package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/decommission.js +2 -1
- package/out/zero-cache/src/services/change-source/pg/decommission.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js +9 -7
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/binary-reader.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/pgoutput-parser.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +5 -4
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/lsn.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.js +2 -2
- package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slot-health.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slot-health.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.js +3 -3
- package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +2 -2
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js +13 -12
- package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/published.js +2 -2
- package/out/zero-cache/src/services/change-source/pg/schema/published.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/replica-stage-enum.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js +10 -7
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/validation.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/validation.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js +1 -1
- package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/control.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/downstream.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/status.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/upstream.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current.js +1 -1
- package/out/zero-cache/src/services/change-source/protocol/mod.js +1 -2
- package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backfill-state.js +1 -1
- package/out/zero-cache/src/services/change-streamer/backfill-state.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-monitor.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/broadcast.js +1 -1
- package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts +1 -1
- package/out/zero-cache/src/services/change-streamer/change-log-codec.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-log-codec.js +2 -3
- package/out/zero-cache/src/services/change-streamer/change-log-codec.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-log-initializer.js +3 -3
- package/out/zero-cache/src/services/change-streamer/change-log-initializer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-log-transaction-hash.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +5 -3
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +21 -21
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.js +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/drain-tracker.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/forwarder.js +11 -10
- package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/litestream-sync-requester.js +1 -1
- package/out/zero-cache/src/services/change-streamer/litestream-sync-requester.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js +3 -3
- package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/replica-poller.js +2 -2
- package/out/zero-cache/src/services/change-streamer/replica-poller.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/restore-admission.js +12 -1
- package/out/zero-cache/src/services/change-streamer/restore-admission.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/init.js +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.js +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot-message.js +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot-message.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.js +2 -2
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-purge-scheduler.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-read-router.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js +4 -6
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js +3 -5
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-writer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.js +3 -3
- package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscribe.js +1 -1
- package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.js +2 -2
- package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/upstream-acker.js +0 -1
- package/out/zero-cache/src/services/change-streamer/upstream-acker.js.map +1 -1
- package/out/zero-cache/src/services/heapz.js +2 -1
- package/out/zero-cache/src/services/heapz.js.map +1 -1
- package/out/zero-cache/src/services/http-service.js.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +2 -2
- package/out/zero-cache/src/services/life-cycle.js.map +1 -1
- package/out/zero-cache/src/services/limiter/sliding-window-limiter.js +4 -2
- package/out/zero-cache/src/services/limiter/sliding-window-limiter.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +3 -3
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/litestream/litestream-checkpointer.js +2 -2
- package/out/zero-cache/src/services/litestream/litestream-checkpointer.js.map +1 -1
- package/out/zero-cache/src/services/litestream/litestream-controller.js.map +1 -1
- package/out/zero-cache/src/services/litestream/metrics.js.map +1 -1
- package/out/zero-cache/src/services/litestream/restore-progress.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.js +3 -2
- package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
- package/out/zero-cache/src/services/plannerz.js +6 -5
- package/out/zero-cache/src/services/plannerz.js.map +1 -1
- package/out/zero-cache/src/services/profz.js +5 -3
- package/out/zero-cache/src/services/profz.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-log-cookies.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-log-db.js +4 -3
- package/out/zero-cache/src/services/replicator/change-log-db.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-log-stream-writer.js +1 -1
- package/out/zero-cache/src/services/replicator/change-log-stream-writer.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
- package/out/zero-cache/src/services/replicator/drain-handler.js.map +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.js +4 -4
- package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
- package/out/zero-cache/src/services/replicator/notifier.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replication-status.js +4 -3
- package/out/zero-cache/src/services/replicator/replication-status.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.js +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/report-schema.js +1 -1
- package/out/zero-cache/src/services/replicator/reporter/report-schema.js.map +1 -1
- package/out/zero-cache/src/services/replicator/schema/backfilling.js +2 -2
- package/out/zero-cache/src/services/replicator/schema/backfilling.js.map +1 -1
- package/out/zero-cache/src/services/replicator/schema/change-log.d.ts +19 -7
- package/out/zero-cache/src/services/replicator/schema/change-log.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/schema/change-log.js +31 -15
- package/out/zero-cache/src/services/replicator/schema/change-log.js.map +1 -1
- package/out/zero-cache/src/services/replicator/schema/column-metadata.js +1 -1
- package/out/zero-cache/src/services/replicator/schema/column-metadata.js.map +1 -1
- package/out/zero-cache/src/services/replicator/schema/replication-state.js +10 -7
- package/out/zero-cache/src/services/replicator/schema/replication-state.js.map +1 -1
- package/out/zero-cache/src/services/replicator/schema/table-metadata.js.map +1 -1
- package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js +1 -1
- package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js.map +1 -1
- package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js +1 -1
- package/out/zero-cache/src/services/replicator/sqlite-change-log-purger.js.map +1 -1
- package/out/zero-cache/src/services/replicator/write-worker-client.js +1 -1
- package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
- package/out/zero-cache/src/services/replicator/write-worker.js +3 -3
- package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
- package/out/zero-cache/src/services/run-ast.js +1 -1
- package/out/zero-cache/src/services/run-ast.js.map +1 -1
- package/out/zero-cache/src/services/runner.js.map +1 -1
- package/out/zero-cache/src/services/running-state.js.map +1 -1
- package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js.map +1 -1
- package/out/zero-cache/src/services/statz.js +4 -3
- package/out/zero-cache/src/services/statz.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/active-users-gauge.js +2 -2
- package/out/zero-cache/src/services/view-syncer/active-users-gauge.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/advancement-metrics.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/batch-pacer.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js +24 -3
- package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-schema.d.ts +15 -2
- package/out/zero-cache/src/services/view-syncer/client-schema.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-schema.js +46 -6
- package/out/zero-cache/src/services/view-syncer/client-schema.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-purger.js +2 -2
- package/out/zero-cache/src/services/view-syncer/cvr-purger.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.js +11 -6
- package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +12 -4
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js +1 -1
- package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/drain-coordinator.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/hydration-budget.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js +2 -2
- package/out/zero-cache/src/services/view-syncer/hydration-circuit-breaker.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js +6 -4
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +13 -10
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/plan-warnings.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/push-cost.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/push-cutoff.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/query-covering.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/query-shape.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/query-stats.js +1 -1
- package/out/zero-cache/src/services/view-syncer/query-stats.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js +1 -1
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/cvr.js +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/types.js +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/types.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.js +7 -1
- package/out/zero-cache/src/services/view-syncer/snapshot-row-cache.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +8 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js +7 -6
- package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/ttl-clock.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +19 -15
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/error-with-level.js.map +1 -1
- package/out/zero-cache/src/types/lexi-version.js.map +1 -1
- package/out/zero-cache/src/types/lite.js +1 -1
- package/out/zero-cache/src/types/lite.js.map +1 -1
- package/out/zero-cache/src/types/pg-data-type.js +1 -1
- package/out/zero-cache/src/types/pg-data-type.js.map +1 -1
- package/out/zero-cache/src/types/pg.js +6 -4
- package/out/zero-cache/src/types/pg.js.map +1 -1
- package/out/zero-cache/src/types/poke-chunk.js +2 -2
- package/out/zero-cache/src/types/poke-chunk.js.map +1 -1
- package/out/zero-cache/src/types/processes.js +1 -1
- package/out/zero-cache/src/types/processes.js.map +1 -1
- package/out/zero-cache/src/types/profiler.js +1 -1
- package/out/zero-cache/src/types/profiler.js.map +1 -1
- package/out/zero-cache/src/types/row-key.js.map +1 -1
- package/out/zero-cache/src/types/sql.js.map +1 -1
- package/out/zero-cache/src/types/streams.js +10 -9
- package/out/zero-cache/src/types/streams.js.map +1 -1
- package/out/zero-cache/src/types/strings.js.map +1 -1
- package/out/zero-cache/src/types/subscription.js.map +1 -1
- package/out/zero-cache/src/types/timeout.js +1 -1
- package/out/zero-cache/src/types/websocket-handoff.js +1 -1
- package/out/zero-cache/src/types/ws.js +1 -1
- package/out/zero-cache/src/workers/connection.js +5 -6
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/mutator.js.map +1 -1
- package/out/zero-cache/src/workers/replicator.js +5 -4
- package/out/zero-cache/src/workers/replicator.js.map +1 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js +9 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +4 -4
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/active-clients-manager.js +1 -1
- package/out/zero-client/src/client/active-clients-manager.js.map +1 -1
- package/out/zero-client/src/client/bindings.js +11 -11
- package/out/zero-client/src/client/connection-manager.js.map +1 -1
- package/out/zero-client/src/client/connection.js +1 -1
- package/out/zero-client/src/client/connection.js.map +1 -1
- package/out/zero-client/src/client/context.js.map +1 -1
- package/out/zero-client/src/client/crud-impl.js +1 -1
- package/out/zero-client/src/client/crud-impl.js.map +1 -1
- package/out/zero-client/src/client/crud.js +1 -1
- package/out/zero-client/src/client/crud.js.map +1 -1
- package/out/zero-client/src/client/custom.js +1 -1
- package/out/zero-client/src/client/custom.js.map +1 -1
- package/out/zero-client/src/client/delete-clients-manager.js.map +1 -1
- package/out/zero-client/src/client/enable-analytics.js +1 -1
- package/out/zero-client/src/client/enable-analytics.js.map +1 -1
- package/out/zero-client/src/client/error.js.map +1 -1
- package/out/zero-client/src/client/inspector/client-group.js.map +1 -1
- package/out/zero-client/src/client/inspector/client.js.map +1 -1
- package/out/zero-client/src/client/inspector/html-dialog-prompt.js +1 -1
- package/out/zero-client/src/client/inspector/html-dialog-prompt.js.map +1 -1
- package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
- package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
- package/out/zero-client/src/client/inspector/query.js.map +1 -1
- package/out/zero-client/src/client/ivm-branch.js +1 -3
- package/out/zero-client/src/client/ivm-branch.js.map +1 -1
- package/out/zero-client/src/client/keys.js +2 -1
- package/out/zero-client/src/client/keys.js.map +1 -1
- package/out/zero-client/src/client/log-options.js.map +1 -1
- package/out/zero-client/src/client/make-replicache-mutators.js +5 -3
- package/out/zero-client/src/client/make-replicache-mutators.js.map +1 -1
- package/out/zero-client/src/client/metrics.js +3 -5
- package/out/zero-client/src/client/metrics.js.map +1 -1
- package/out/zero-client/src/client/mutation-tracker.js.map +1 -1
- package/out/zero-client/src/client/mutator-proxy.js.map +1 -1
- package/out/zero-client/src/client/options.js.map +1 -1
- package/out/zero-client/src/client/query-manager.js +2 -2
- package/out/zero-client/src/client/query-manager.js.map +1 -1
- package/out/zero-client/src/client/reload-error-handler.js +4 -3
- package/out/zero-client/src/client/reload-error-handler.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero-poke-handler.js +1 -1
- package/out/zero-client/src/client/zero-poke-handler.js.map +1 -1
- package/out/zero-client/src/client/zero-rep.js.map +1 -1
- package/out/zero-client/src/client/zero.js +19 -11
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-client/src/mod.js +23 -23
- package/out/zero-client/src/util/nanoid.js.map +1 -1
- package/out/zero-permissions/src/permissions.js +3 -2
- package/out/zero-permissions/src/permissions.js.map +1 -1
- package/out/zero-pg/src/mod.js +9 -9
- package/out/zero-protocol/src/analyze-query-result.js +1 -1
- package/out/zero-protocol/src/analyze-query-result.js.map +1 -1
- package/out/zero-protocol/src/application-error.js.map +1 -1
- package/out/zero-protocol/src/ast.js +19 -9
- package/out/zero-protocol/src/ast.js.map +1 -1
- package/out/zero-protocol/src/change-desired-queries.js.map +1 -1
- package/out/zero-protocol/src/client-schema.js +1 -1
- package/out/zero-protocol/src/client-schema.js.map +1 -1
- package/out/zero-protocol/src/close-connection.js +1 -1
- package/out/zero-protocol/src/close-connection.js.map +1 -1
- package/out/zero-protocol/src/connect.js +1 -1
- package/out/zero-protocol/src/connect.js.map +1 -1
- package/out/zero-protocol/src/custom-queries.js +1 -1
- package/out/zero-protocol/src/custom-queries.js.map +1 -1
- package/out/zero-protocol/src/data.js +4 -2
- package/out/zero-protocol/src/data.js.map +1 -1
- package/out/zero-protocol/src/delete-clients.js.map +1 -1
- package/out/zero-protocol/src/down.d.ts +27 -27
- package/out/zero-protocol/src/down.js.map +1 -1
- package/out/zero-protocol/src/error.js.map +1 -1
- package/out/zero-protocol/src/inspect-down.d.ts +81 -81
- package/out/zero-protocol/src/inspect-down.js +1 -1
- package/out/zero-protocol/src/inspect-down.js.map +1 -1
- package/out/zero-protocol/src/inspect-up.js +1 -1
- package/out/zero-protocol/src/inspect-up.js.map +1 -1
- package/out/zero-protocol/src/mutate-server.js +1 -1
- package/out/zero-protocol/src/mutate-server.js.map +1 -1
- package/out/zero-protocol/src/mutation-id.js.map +1 -1
- package/out/zero-protocol/src/mutation.js +1 -1
- package/out/zero-protocol/src/mutation.js.map +1 -1
- package/out/zero-protocol/src/mutations-patch.js +1 -1
- package/out/zero-protocol/src/mutations-patch.js.map +1 -1
- package/out/zero-protocol/src/ping.js +1 -1
- package/out/zero-protocol/src/ping.js.map +1 -1
- package/out/zero-protocol/src/poke.d.ts +6 -6
- package/out/zero-protocol/src/poke.js +1 -1
- package/out/zero-protocol/src/poke.js.map +1 -1
- package/out/zero-protocol/src/pong.js +1 -1
- package/out/zero-protocol/src/pong.js.map +1 -1
- package/out/zero-protocol/src/primary-key.js.map +1 -1
- package/out/zero-protocol/src/protocol-version.js +1 -0
- package/out/zero-protocol/src/protocol-version.js.map +1 -1
- package/out/zero-protocol/src/pull.js +1 -1
- package/out/zero-protocol/src/pull.js.map +1 -1
- package/out/zero-protocol/src/push.js +1 -1
- package/out/zero-protocol/src/push.js.map +1 -1
- package/out/zero-protocol/src/queries-patch.js +1 -1
- package/out/zero-protocol/src/queries-patch.js.map +1 -1
- package/out/zero-protocol/src/query-hash-visitor.js.map +1 -1
- package/out/zero-protocol/src/query-hash.js +2 -2
- package/out/zero-protocol/src/query-server.js +1 -1
- package/out/zero-protocol/src/query-server.js.map +1 -1
- package/out/zero-protocol/src/row-patch.js.map +1 -1
- package/out/zero-protocol/src/up.d.ts +60 -60
- package/out/zero-protocol/src/up.js.map +1 -1
- package/out/zero-protocol/src/update-auth.js.map +1 -1
- package/out/zero-protocol/src/version.js.map +1 -1
- package/out/zero-react/src/bindings.js +11 -11
- package/out/zero-react/src/mod.js +5 -5
- package/out/zero-react/src/use-query.js +1 -1
- package/out/zero-react/src/use-query.js.map +1 -1
- package/out/zero-react/src/zero-provider.d.ts +1 -1
- package/out/zero-react/src/zero-provider.d.ts.map +1 -1
- package/out/zero-react/src/zero-provider.js +2 -1
- package/out/zero-react/src/zero-provider.js.map +1 -1
- package/out/zero-react/src/zero.js +23 -23
- package/out/zero-schema/src/builder/schema-builder.js +3 -2
- package/out/zero-schema/src/builder/schema-builder.js.map +1 -1
- package/out/zero-schema/src/builder/table-builder.js +1 -1
- package/out/zero-schema/src/builder/table-builder.js.map +1 -1
- package/out/zero-schema/src/compiled-permissions.js.map +1 -1
- package/out/zero-schema/src/name-mapper.js +7 -5
- package/out/zero-schema/src/name-mapper.js.map +1 -1
- package/out/zero-schema/src/schema-config.js +1 -1
- package/out/zero-schema/src/schema-config.js.map +1 -1
- package/out/zero-server/src/adapters/drizzle.js.map +1 -1
- package/out/zero-server/src/adapters/kysely.js.map +1 -1
- package/out/zero-server/src/adapters/pg.js.map +1 -1
- package/out/zero-server/src/adapters/postgresjs.js.map +1 -1
- package/out/zero-server/src/adapters/prisma.js.map +1 -1
- package/out/zero-server/src/closable-db-transaction.js.map +1 -1
- package/out/zero-server/src/custom.js +1 -1
- package/out/zero-server/src/custom.js.map +1 -1
- package/out/zero-server/src/mod.js +8 -8
- package/out/zero-server/src/process-mutations.js.map +1 -1
- package/out/zero-server/src/push-processor.js.map +1 -1
- package/out/zero-server/src/queries/process-queries.js +2 -1
- package/out/zero-server/src/queries/process-queries.js.map +1 -1
- package/out/zero-server/src/schema.js +7 -7
- package/out/zero-server/src/schema.js.map +1 -1
- package/out/zero-server/src/zql-database.js.map +1 -1
- package/out/zero-solid/src/bindings.js +11 -11
- package/out/zero-solid/src/mod.js +5 -5
- package/out/zero-solid/src/solid-view.js +1 -1
- package/out/zero-solid/src/solid-view.js.map +1 -1
- package/out/zero-solid/src/use-connection-state.js +2 -1
- package/out/zero-solid/src/use-connection-state.js.map +1 -1
- package/out/zero-solid/src/use-query.js.map +1 -1
- package/out/zero-solid/src/use-zero-online.js +2 -1
- package/out/zero-solid/src/use-zero-online.js.map +1 -1
- package/out/zero-solid/src/use-zero.js +3 -2
- package/out/zero-solid/src/use-zero.js.map +1 -1
- package/out/zero-solid/src/zero.js +23 -23
- package/out/zero-types/src/name-mapper.js.map +1 -1
- package/out/zql/src/builder/builder.js +15 -13
- package/out/zql/src/builder/builder.js.map +1 -1
- package/out/zql/src/builder/correlated-predicate-pushdown.js.map +1 -1
- package/out/zql/src/builder/debug-delegate.js.map +1 -1
- package/out/zql/src/builder/like.js +0 -1
- package/out/zql/src/builder/like.js.map +1 -1
- package/out/zql/src/ivm/array-view.js.map +1 -1
- package/out/zql/src/ivm/cap.d.ts +1 -1
- package/out/zql/src/ivm/cap.d.ts.map +1 -1
- package/out/zql/src/ivm/cap.js +10 -0
- package/out/zql/src/ivm/cap.js.map +1 -1
- package/out/zql/src/ivm/constraint.js +1 -1
- package/out/zql/src/ivm/constraint.js.map +1 -1
- package/out/zql/src/ivm/data.d.ts +15 -1
- package/out/zql/src/ivm/data.d.ts.map +1 -1
- package/out/zql/src/ivm/data.js +15 -1
- package/out/zql/src/ivm/data.js.map +1 -1
- package/out/zql/src/ivm/deferred-input.js.map +1 -1
- package/out/zql/src/ivm/exists.d.ts.map +1 -1
- package/out/zql/src/ivm/exists.js +32 -22
- package/out/zql/src/ivm/exists.js.map +1 -1
- package/out/zql/src/ivm/fan-in.js.map +1 -1
- package/out/zql/src/ivm/fan-out.js.map +1 -1
- package/out/zql/src/ivm/filter-operators.d.ts +5 -0
- package/out/zql/src/ivm/filter-operators.d.ts.map +1 -1
- package/out/zql/src/ivm/filter-operators.js +1 -1
- package/out/zql/src/ivm/filter-operators.js.map +1 -1
- package/out/zql/src/ivm/filter-push.js.map +1 -1
- package/out/zql/src/ivm/filter.js.map +1 -1
- package/out/zql/src/ivm/flipped-join.js +1 -3
- package/out/zql/src/ivm/flipped-join.js.map +1 -1
- package/out/zql/src/ivm/join-index-tap.js.map +1 -1
- package/out/zql/src/ivm/join-index.js.map +1 -1
- package/out/zql/src/ivm/join-utils.js +11 -13
- package/out/zql/src/ivm/join-utils.js.map +1 -1
- package/out/zql/src/ivm/join.d.ts.map +1 -1
- package/out/zql/src/ivm/join.js +6 -3
- package/out/zql/src/ivm/join.js.map +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.js.map +1 -1
- package/out/zql/src/ivm/memory-source.js +27 -23
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/ivm/memory-storage.js.map +1 -1
- package/out/zql/src/ivm/operator.d.ts +6 -0
- package/out/zql/src/ivm/operator.d.ts.map +1 -1
- package/out/zql/src/ivm/operator.js +2 -2
- package/out/zql/src/ivm/operator.js.map +1 -1
- package/out/zql/src/ivm/push-accumulated.js +0 -1
- package/out/zql/src/ivm/push-accumulated.js.map +1 -1
- package/out/zql/src/ivm/skip-yields.js.map +1 -1
- package/out/zql/src/ivm/skip.js.map +1 -1
- package/out/zql/src/ivm/take-gate.js.map +1 -1
- package/out/zql/src/ivm/take.d.ts.map +1 -1
- package/out/zql/src/ivm/take.js +5 -4
- package/out/zql/src/ivm/take.js.map +1 -1
- package/out/zql/src/ivm/union-fan-in.js +1 -1
- package/out/zql/src/ivm/union-fan-in.js.map +1 -1
- package/out/zql/src/ivm/union-fan-out.js.map +1 -1
- package/out/zql/src/ivm/view-apply-change.js +1 -1
- package/out/zql/src/ivm/view-apply-change.js.map +1 -1
- package/out/zql/src/mutate/crud.js +3 -2
- package/out/zql/src/mutate/crud.js.map +1 -1
- package/out/zql/src/planner/planner-builder.js +1 -1
- package/out/zql/src/planner/planner-builder.js.map +1 -1
- package/out/zql/src/planner/planner-connection.js.map +1 -1
- package/out/zql/src/planner/planner-debug.js.map +1 -1
- package/out/zql/src/planner/planner-fan-in.js.map +1 -1
- package/out/zql/src/planner/planner-fan-out.js.map +1 -1
- package/out/zql/src/planner/planner-filter.js.map +1 -1
- package/out/zql/src/planner/planner-graph.js +1 -5
- package/out/zql/src/planner/planner-graph.js.map +1 -1
- package/out/zql/src/planner/planner-join.js +1 -1
- package/out/zql/src/planner/planner-join.js.map +1 -1
- package/out/zql/src/planner/planner-source.js.map +1 -1
- package/out/zql/src/planner/planner-terminus.js.map +1 -1
- package/out/zql/src/query/create-builder.js +2 -1
- package/out/zql/src/query/create-builder.js.map +1 -1
- package/out/zql/src/query/expression.js +1 -1
- package/out/zql/src/query/expression.js.map +1 -1
- package/out/zql/src/query/measure-push-operator.js.map +1 -1
- package/out/zql/src/query/named.js +2 -1
- package/out/zql/src/query/named.js.map +1 -1
- package/out/zql/src/query/query-delegate-base.js +1 -1
- package/out/zql/src/query/query-delegate-base.js.map +1 -1
- package/out/zql/src/query/query-impl.js +3 -2
- package/out/zql/src/query/query-impl.js.map +1 -1
- package/out/zql/src/query/query-registry.js +1 -1
- package/out/zql/src/query/query-registry.js.map +1 -1
- package/out/zql/src/query/query-transitions.js +1 -1
- package/out/zql/src/query/query-transitions.js.map +1 -1
- package/out/zql/src/query/runnable-query-impl.js +3 -2
- package/out/zql/src/query/runnable-query-impl.js.map +1 -1
- package/out/zql/src/query/static-query.js.map +1 -1
- package/out/zql/src/query/ttl.js +8 -8
- package/out/zql/src/query/ttl.js.map +1 -1
- package/out/zql/src/query/validate-input.js.map +1 -1
- package/out/zqlite/src/database-storage.d.ts +27 -8
- package/out/zqlite/src/database-storage.d.ts.map +1 -1
- package/out/zqlite/src/database-storage.js +190 -53
- package/out/zqlite/src/database-storage.js.map +1 -1
- package/out/zqlite/src/db.d.ts +47 -1
- package/out/zqlite/src/db.d.ts.map +1 -1
- package/out/zqlite/src/db.js +121 -21
- package/out/zqlite/src/db.js.map +1 -1
- package/out/zqlite/src/explain-queries.js.map +1 -1
- package/out/zqlite/src/fetch-sql.js.map +1 -1
- package/out/zqlite/src/index-usage.js.map +1 -1
- package/out/zqlite/src/internal/sql-inline.js.map +1 -1
- package/out/zqlite/src/internal/sql.js.map +1 -1
- package/out/zqlite/src/internal/statement-cache.d.ts +7 -0
- package/out/zqlite/src/internal/statement-cache.d.ts.map +1 -1
- package/out/zqlite/src/internal/statement-cache.js +11 -4
- package/out/zqlite/src/internal/statement-cache.js.map +1 -1
- package/out/zqlite/src/mod.js +5 -5
- package/out/zqlite/src/pending-delta.js.map +1 -1
- package/out/zqlite/src/query-builder.js +1 -1
- package/out/zqlite/src/query-builder.js.map +1 -1
- package/out/zqlite/src/query-delegate.js.map +1 -1
- package/out/zqlite/src/resolve-scalar-subqueries.js +2 -5
- package/out/zqlite/src/resolve-scalar-subqueries.js.map +1 -1
- package/out/zqlite/src/sqlite-cost-model.js +3 -2
- package/out/zqlite/src/sqlite-cost-model.js.map +1 -1
- package/out/zqlite/src/sqlite-stat-fanout.js.map +1 -1
- package/out/zqlite/src/table-source.d.ts +11 -0
- package/out/zqlite/src/table-source.d.ts.map +1 -1
- package/out/zqlite/src/table-source.js +63 -26
- package/out/zqlite/src/table-source.js.map +1 -1
- package/package.json +37 -36
- package/out/analyze-query/src/bin-analyze.d.ts +0 -2
- package/out/analyze-query/src/bin-analyze.d.ts.map +0 -1
- package/out/analyze-query/src/bin-analyze.js +0 -283
- package/out/analyze-query/src/bin-analyze.js.map +0 -1
- package/out/otel/src/maybe-time.d.ts +0 -3
- package/out/otel/src/maybe-time.d.ts.map +0 -1
- package/out/otel/src/maybe-time.js +0 -15
- package/out/otel/src/maybe-time.js.map +0 -1
- package/out/zero/src/analyze-query.d.ts +0 -3
- package/out/zero/src/analyze-query.d.ts.map +0 -1
- package/out/zero/src/analyze-query.js +0 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-log-db.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/change-log-db.ts"],"sourcesContent":["/**\n * The change log lives in its own SQLite database, `${replicaFile}-change-log`,\n * rather than in the replica file. It is a catchup cache: everything it holds is\n * either already durable in the replica's litestream backup or re-derivable from\n * the upstream replication slot, so it is deliberately excluded from the backup\n * and is never a source of truth.\n *\n * The change-streamer writes it, from the stream loop that fans changes out to\n * subscribers, and commits it before forwarding each transaction's `commit`\n * message. Its head is therefore always at or above the watermark the stream\n * would resume from, which is what {@link reconcileChangeLog} anchors on.\n *\n * Beside the buffer it holds the log's *cookie jar* — the backfill progress\n * state folded up to the same head. That is `change-log-cookies.ts`; this file\n * owns its tables' lifecycle, because a cookie set is only meaningful paired\n * with the watermark it was folded to, and so must be created, dropped, and —\n * whenever reconciliation moves the head — replaced with the anchor's, all with\n * the buffer rather than beside it.\n *\n * Because it is disposable, it has no migration framework. Any inconsistency —\n * a schema change, a leftover file from a different replica, a gap left by a\n * crash or by running with the writer disabled, a corrupt file — is resolved by\n * {@link reconcileChangeLog} and {@link openChangeLogDBForWriting}, which\n * truncate a bounded suffix or replace and reseed the log at the resume\n * watermark. The worst outcome is a `too-old` for a lagging subscriber; a\n * silently delivered gap is not reachable.\n *\n * Disk sizing: a task that writes the log needs room for the replica *plus* the\n * retained change stream — the log's main file, its wal2 sidecars, and pages a\n * purge has freed but not yet returned to the OS. The change-streamer schedules\n * purges against the same floor as the PG change log, so retention is bounded\n * by the confirmed backup watermark ANDed with the minimum retention window —\n * a function of the backup interval, not a constant: 15–30+ minutes behind an\n * RMv1 sync interval, ~60 seconds once litestream v5 backs up every 15–30 s.\n * A held snapshot reservation pins retention unboundedly for its duration, so\n * peak retained bytes is set by the slowest concurrent restore rather than by\n * the retention setting — size disk and alerts on that. The log is excluded\n * from the litestream backup, so this is local disk only and never S3.\n */\n\nimport {existsSync} from 'node:fs';\nimport type {LogContext} from '@rocicorp/logger';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {deleteLiteDB} from '../../db/delete-lite-db.ts';\nimport {\n isSQLiteCorruption,\n logSQLiteCorruptionDiagnostics,\n} from '../../db/sqlite-corruption.ts';\nimport {\n CHANGE_LOG_BACKFILLING_TABLE,\n CHANGE_LOG_TABLE_METADATA_TABLE,\n CREATE_CHANGE_LOG_COOKIE_SCHEMA,\n DROP_CHANGE_LOG_COOKIE_TABLES,\n replaceCookies,\n type CookieSet,\n} from './change-log-cookies.ts';\n\n/**\n * Bumped whenever the change-log database's schema changes. Since the file is a\n * cache, a mismatch costs one reseed rather than a migration.\n *\n * v2 re-homed the writer to the change-streamer: the log anchors on the\n * watermark its stream connection resumes from rather than on the replica's\n * state version, and its meta row carries the replica's identity triple plus\n * the seed point.\n *\n * v3 added the cookie tables (see `change-log-cookies.ts`), i.e. the log's\n * second job. The reseed it costs on upgrade buys one retention window of\n * catchup reach on a log nothing reads yet.\n *\n * v4 stores each row's tag and estimated retained bytes alongside its JSON.\n * Readers can now filter without parsing arbitrary row payloads, and\n * reconciliation can bound truncation by bytes as well as rows.\n *\n * `auto_vacuum = INCREMENTAL` arrived within v2, deliberately without a bump:\n * it is a file-header property rather than a schema change, and the reseed a\n * version mismatch triggers cannot enable it (see\n * {@link applyChangeLogPragmas}), so {@link openChangeLogDBForWriting} guards\n * on the pragma itself instead of on this number.\n */\nexport const CHANGE_LOG_DB_SCHEMA_VERSION = 4;\n\n/** https://www.sqlite.org/pragma.html#pragma_auto_vacuum */\nconst AUTO_VACUUM_INCREMENTAL = 2;\n\nexport const CHANGE_LOG_STREAM_TABLE = '_zero.changeLogStream';\nexport const CHANGE_LOG_STREAM_WRITE_TIME_INDEX =\n '_zero.changeLogStream_writeTimeMs';\n\nconst INTEGER_BYTES = 8;\n\n/**\n * Estimates the retained payload size of a stream row. This deliberately does\n * not claim to measure SQLite b-tree/page overhead; it is stable across the\n * write path, reconciliation, and the startup scan used by observability.\n */\nexport function estimateChangeLogStreamRowBytes(\n watermark: string,\n tag: string,\n change: string,\n precommit?: string,\n hasWriteTime = false,\n): number {\n return (\n Buffer.byteLength(watermark) +\n INTEGER_BYTES +\n Buffer.byteLength(tag) +\n Buffer.byteLength(change) +\n (precommit === undefined ? 0 : Buffer.byteLength(precommit)) +\n (hasWriteTime ? INTEGER_BYTES : 0)\n );\n}\n\n// The index is partial because only commit rows carry a \"writeTimeMs\", so it\n// holds one entry per transaction rather than one per change. It is what the\n// purger scans to find transactions older than the retention window.\n//\n// Pre-release builds briefly carried this table in the replica; see the test\n// fixture `CREATE_V14_CHANGE_LOG_STREAM` in\n// `change-source/common/replica-schema.ts`. Its old shape must not be kept in\n// sync with this one.\nexport const CREATE_CHANGE_LOG_STREAM_SCHEMA = /*sql*/ `\n CREATE TABLE \"${CHANGE_LOG_STREAM_TABLE}\" (\n \"watermark\" TEXT NOT NULL,\n \"pos\" INTEGER NOT NULL,\n \"tag\" TEXT NOT NULL,\n \"estimatedBytes\" INTEGER NOT NULL,\n \"change\" TEXT NOT NULL,\n \"precommit\" TEXT,\n \"writeTimeMs\" INTEGER,\n PRIMARY KEY (\"watermark\", \"pos\")\n );\n\n CREATE INDEX \"${CHANGE_LOG_STREAM_WRITE_TIME_INDEX}\"\n ON \"${CHANGE_LOG_STREAM_TABLE}\" (\"writeTimeMs\", \"watermark\")\n WHERE \"writeTimeMs\" IS NOT NULL;\n`;\n\n/**\n * Inserts the synthetic begin/commit pair for the seed watermark.\n *\n * Both rows are inserted by one statement so a caller outside a larger\n * transaction cannot leave a partial seed.\n */\nconst SEED_CHANGE_LOG_STREAM_SQL = /*sql*/ `\n INSERT INTO \"${CHANGE_LOG_STREAM_TABLE}\"\n (\"watermark\", \"pos\", \"tag\", \"estimatedBytes\", \"change\", \"precommit\",\n \"writeTimeMs\")\n VALUES\n (@watermark, 0, 'begin', @beginBytes, @beginChange, NULL, NULL),\n (@watermark, 1, 'commit', @commitBytes, @commitChange, @watermark,\n @writeTimeMs)\n ON CONFLICT (\"watermark\", \"pos\") DO NOTHING\n`;\n\nexport const CHANGE_LOG_META_TABLE = '_zero.changeLogMeta';\n\n// The identity triple (see ChangeLogIdentity) detects a file left behind by a\n// different replica -- most concretely, a leftover log on a reused volume.\n//\n// \"seededAtMs\" : when the log was seeded, i.e. how far its retention window\n// can possibly reach back. Slice 11 uses it as the warm-up\n// signal for canary reads.\n// \"seedWatermark\" : where the log was seeded. `min(\"watermark\")` cannot answer\n// this once purge has advanced the minimum, and the\n// difference is what distinguishes a log that purged past the\n// retention floor (an invariant violation) from one that\n// never held that history at all (routine at every fork).\n// \"lock\" : enforces single-row semantics, as in\n// \"_zero.replicationConfig\".\nconst CREATE_CHANGE_LOG_META_SCHEMA = /*sql*/ `\n CREATE TABLE \"${CHANGE_LOG_META_TABLE}\" (\n \"epoch\" TEXT,\n \"generation\" TEXT NOT NULL,\n \"replicaID\" TEXT,\n \"schemaVersion\" INTEGER NOT NULL,\n \"seededAtMs\" INTEGER NOT NULL,\n \"seedWatermark\" TEXT NOT NULL,\n \"lock\" INTEGER PRIMARY KEY DEFAULT 1 CHECK (\"lock\" = 1)\n );\n`;\n\n/**\n * `${replicaFile}-change-log`, matching the `-serving-copy` convention.\n *\n * The name outlives the writer's move into the change-streamer: the\n * change-streamer derives the path from the replica file it owns, so renaming\n * would save no plumbing and would orphan the old file on every existing\n * volume, where nothing would delete it.\n *\n * Its `-wal` / `-wal2` / `-shm` sidecars are suffixes of *this* name, so they\n * never collide with the replica's own sidecars.\n */\nexport function changeLogFileName(replicaFile: string): string {\n return `${replicaFile}-change-log`;\n}\n\n/**\n * Removes the change-log database and its `-wal` / `-wal2` / `-shm` sidecars.\n * Safe to call when the file does not exist.\n */\nexport function deleteChangeLogDB(replicaFile: string): void {\n deleteLiteDB(changeLogFileName(replicaFile));\n}\n\n/**\n * Which replica the log's contents belong to.\n *\n * `generation` is RMv1's `replicaVersion`: the version at which initial sync\n * copied every row. It is shared by every sibling of a forked replica, which is\n * why it cannot be the whole of the identity under RMv2 — `epoch` and\n * `replicaID` are what distinguish two siblings' logs. Both are `null` until\n * RMv2 supplies them, and a `null` matches only another `null`, so a log\n * written before they exist is still recognized as this replica's.\n */\nexport type ChangeLogIdentity = {\n readonly epoch: string | null;\n readonly generation: string;\n readonly replicaID: string | null;\n};\n\n/**\n * Where the stream connection resumes, and everything the log's state at that\n * position consists of.\n *\n * The two fields travel together because a cookie set is only meaningful paired\n * with the watermark it was folded up to (invariant 15). They are read from one\n * store, in one snapshot: `Storer.getStartStreamInitializationParameters()`\n * reads both from Postgres in a single REPEATABLE READ transaction. Pairing one\n * store's cookies with another's watermark would silently drop every backfill\n * that completed in the interval, and those rows are unrecoverable — backfill\n * transactions are synthesized by the change source rather than replayed from\n * the slot.\n */\nexport type ChangeLogResumePoint = {\n /**\n * The watermark this stream connection resumes from. The log's own head is\n * measured against it, and a reseed seeds a synthetic transaction at it.\n *\n * Deliberately *not* \"the replica's state version\": after the writer moved to\n * the change-streamer, the canonical replicator is a subscriber downstream of\n * the write loop, so its state version is a consequence of this log rather\n * than a constraint on it.\n */\n readonly resumeWatermark: string;\n /**\n * The cookie set that belongs to {@link resumeWatermark}. Any reconciliation\n * that moves the head invalidates the set the log was holding — the cookies\n * are unversioned point-in-time state, and truncating the stream above `W`\n * does not roll them back to `W` — so the same transaction installs this one\n * in its place (invariant 17).\n *\n * Postgres supplies it while it remains the store the stream resumes from. At\n * the C5 flip the log's own head *is* the resume point, phantoms cannot\n * exist, and both this field and the truncate-above path become dead code.\n */\n readonly cookies: CookieSet;\n};\n\n/**\n * What the log is reconciled against. Reconciliation runs once per stream\n * connection, since every reconnect can re-read a different resume point.\n */\nexport type ChangeLogAnchor = ChangeLogResumePoint & {\n readonly identity: ChangeLogIdentity;\n /**\n * The change-streamer's clock, at reconciliation time. Supplied rather than\n * read inline so that tests control it. Used for `seededAtMs` and for the\n * seed transaction's `writeTimeMs`, which is what the purger's time floor\n * measures.\n */\n readonly nowMs: number;\n};\n\n/** The meta row, i.e. everything the log records about itself. */\nexport type ChangeLogMeta = ChangeLogIdentity & {\n readonly schemaVersion: number;\n readonly seededAtMs: number;\n readonly seedWatermark: string;\n};\n\n/**\n * Opens the change-log database beside `replicaFile`.\n *\n * A writable handle creates the file if it is absent; a `readonly` handle\n * throws, which is the normal state for a change-streamer whose writer is\n * disabled. Pragmas are the caller's responsibility, since only the writer sets\n * the persistent ones.\n */\nexport function openChangeLogDB(\n lc: LogContext,\n replicaFile: string,\n opts: {readonly: boolean},\n): Database {\n return new Database(\n lc.withContext('component', 'change-log-db'),\n changeLogFileName(replicaFile),\n {readonly: opts.readonly},\n );\n}\n\n/**\n * Configures the change-log database, which the change-streamer's writer owns\n * exclusively. These are the settings the `separate-files` mode of\n * `sqlite-change-log-ceiling.bench.ts` measured.\n *\n * `wal2` because the log is a continuous append that catchup reads\n * continuously, which is the case wal2 exists for: it avoids checkpoint\n * starvation without needing an autocheckpoint. `wal_autocheckpoint` therefore\n * keeps its default, unlike the backup replica's 0, which hands checkpointing\n * to litestream — nothing else owns this file.\n *\n * `synchronous = NORMAL` because the commit sits on the forward path, once per\n * upstream transaction, and NORMAL keeps that from costing an fsync. It is not\n * a durability compromise here: in WAL mode NORMAL still writes every commit\n * through to the OS, so it survives a process crash, an OOM kill, or a SIGKILL.\n * It is lost only to kernel panic or power loss, and those destroy the node —\n * the task restarts elsewhere, restores the replica from S3, and has no\n * change-log file at all. Whatever a power loss does take is detected by\n * {@link reconcileChangeLog} and reseeded.\n *\n * `auto_vacuum = INCREMENTAL` so that purge can return freed pages to the OS\n * rather than leaving the file at its high-water mark. **It must be the first\n * statement here.** SQLite honours a `none` -> `incremental` transition only\n * while the file is still empty, and `journal_mode = wal2` materializes page 1;\n * appending the pragma instead of prepending it silently leaves the file at\n * mode 0 forever, since neither `DROP TABLE` nor a reseed makes a file empty\n * again. {@link openChangeLogDBForWriting} verifies the result.\n */\nexport function applyChangeLogPragmas(db: Database): void {\n db.pragma('auto_vacuum = INCREMENTAL');\n db.pragma('busy_timeout = 30000');\n db.pragma('analysis_limit = 1000');\n db.pragma('journal_mode = wal2');\n db.pragma('synchronous = NORMAL');\n}\n\nfunction readAutoVacuum(db: Database): number {\n const [{auto_vacuum: mode}] = db.pragma<{auto_vacuum: number}>('auto_vacuum');\n return mode;\n}\n\n/**\n * Opens the change-log database for the writer, applies its pragmas, and\n * reconciles it against `anchor`, returning the handle and what reconciliation\n * had to do.\n *\n * Corruption is answered with a rebuild rather than an escalation, which is a\n * deliberate divergence from how the replica's own corruption is handled. A\n * corrupt replica is a lost source of truth, so it ends in a restore or an\n * auto-reset; the change log holds nothing that is not either already in the\n * replica's litestream backup or re-derivable from the upstream slot, so the\n * entire response is to delete the file and seed a new one at the resume\n * watermark. The cost is one retention window of catchup — reconnecting\n * subscribers below the head get `too-old` — which is what every other\n * {@link ReseedReason} costs.\n *\n * Only corruption is rebuilt through. Anything else (a bad path, a permissions\n * error, a full disk) is a problem with the environment rather than with the\n * file's contents, and deleting a database in response would destroy state\n * without fixing it. Those propagate to the caller, which disables the writer\n * and keeps replicating.\n *\n * A file that predates `auto_vacuum = INCREMENTAL` takes the same rebuild, for\n * the same reason and at the same cost: the pragma only takes on an empty file,\n * so a reseed cannot enable it. Unlike corruption, a *second* failure there is\n * tolerated rather than thrown — see {@link ensureIncrementalAutoVacuum}.\n */\n/**\n * Returns an anchor after the database opens.\n *\n * The resolver can inspect the existing file before it selects the anchor. A\n * valid log can use its own head. A new or invalid log uses the replica seed.\n * The rebuild path calls the resolver again for the new file.\n */\nexport type AnchorResolver = (db: Database) => ChangeLogAnchor;\n\nexport function openChangeLogDBForWriting(\n lc: LogContext,\n replicaFile: string,\n anchor: AnchorResolver,\n): {db: Database; result: ReconcileResult; replacedFile: boolean} {\n const existed = existsSync(changeLogFileName(replicaFile));\n const opened = openOrRebuildCorrupt(lc, replicaFile, anchor, existed);\n const {db, result, replacedFile} = ensureIncrementalAutoVacuum(\n lc,\n replicaFile,\n anchor,\n opened,\n );\n return {db, result, replacedFile};\n}\n\nfunction openOrRebuildCorrupt(\n lc: LogContext,\n replicaFile: string,\n anchor: AnchorResolver,\n boundReconciliation: boolean,\n): OpenedChangeLog {\n try {\n return openAndReconcile(lc, replicaFile, anchor, boundReconciliation);\n } catch (e) {\n if (e instanceof ChangeLogRebuildRequired) {\n return rebuildChangeLog(lc, replicaFile, anchor, e);\n }\n if (!isSQLiteCorruption(e)) {\n throw e;\n }\n const file = changeLogFileName(replicaFile);\n logSQLiteCorruptionDiagnostics(lc, 'change-log', file, e);\n lc.error?.('rebuilding the corrupt SQLite change log', {\n sqliteChangeLog: {file},\n });\n return rebuildChangeLog(lc, replicaFile, anchor);\n }\n}\n\n/**\n * Rebuilds a log whose `auto_vacuum` mode is not `INCREMENTAL`, which is every\n * log created before the pragma shipped — including every v2 file the writer\n * wrote in the meantime, which is why the guard is on the pragma rather than\n * on the schema version. Deleting the file is the only way to enable it:\n * `DROP TABLE` leaves the header's mode intact, and the in-place alternative —\n * a full `VACUUM` — is an unbounded blocking statement on a file the writer\n * needs.\n *\n * A mode that is still wrong after the rebuild is logged and tolerated, which\n * is the one place this diverges from the corruption path. The two failures are\n * not alike: a file that stays corrupt after a rebuild is an environment\n * problem, but a pragma that stays wrong on a brand-new file can only mean\n * {@link applyChangeLogPragmas}' statement order is wrong — a code bug, present\n * on every task, on every start. Throwing there turns a lost optimization into\n * a crash loop, and rebuilding again turns it into a reseed per start, which is\n * worse than the freelist growth it was trying to avoid. `Database.compact()`\n * already warns and no-ops when the mode is not `INCREMENTAL`, so degrading is\n * the accepted fallback.\n */\nfunction ensureIncrementalAutoVacuum(\n lc: LogContext,\n replicaFile: string,\n anchor: AnchorResolver,\n opened: OpenedChangeLog,\n): OpenedChangeLog {\n if (opened.autoVacuum === AUTO_VACUUM_INCREMENTAL) {\n return opened;\n }\n const file = changeLogFileName(replicaFile);\n lc.info?.('rebuilding the SQLite change log to enable incremental vacuum', {\n sqliteChangeLog: {file, autoVacuum: opened.autoVacuum},\n });\n opened.db.close();\n const rebuilt = rebuildChangeLog(lc, replicaFile, anchor);\n if (rebuilt.autoVacuum !== AUTO_VACUUM_INCREMENTAL) {\n lc.error?.(\n 'SQLite change log does not support incremental vacuum; ' +\n 'it will plateau at its high-water mark',\n {sqliteChangeLog: {file, autoVacuum: rebuilt.autoVacuum}},\n );\n }\n return rebuilt;\n}\n\n/** The shared \"delete the file and open a new one\" body. */\nfunction rebuildChangeLog(\n lc: LogContext,\n replicaFile: string,\n anchor: AnchorResolver,\n required?: ChangeLogRebuildRequired | undefined,\n): OpenedChangeLog {\n if (required) {\n lc.warn?.(\n 'rebuilding the SQLite change log instead of running an unbounded reconciliation',\n {\n sqliteChangeLogRebuild: {\n reason: required.reason,\n rows: required.rows,\n estimatedBytes: required.estimatedBytes,\n },\n },\n );\n }\n deleteChangeLogDB(replicaFile);\n // The file is absent, so the reseed only drops empty/nonexistent tables. A\n // second failure is the environment's, not the old file's.\n const opened = openAndReconcile(\n lc,\n replicaFile,\n anchor,\n false,\n required?.reason,\n );\n return {...opened, replacedFile: true};\n}\n\n/**\n * Replaces an already-closed change-log file after bounded reconciliation\n * declined its work. The caller closes cached readers before calling this so\n * none can continue serving the unlinked inode.\n */\nexport function rebuildChangeLogDBForWriting(\n lc: LogContext,\n replicaFile: string,\n anchor: ChangeLogAnchor,\n required: ChangeLogRebuildRequired,\n): {db: Database; result: ReconcileResult; replacedFile: true} {\n const {db, result} = rebuildChangeLog(\n lc,\n replicaFile,\n () => anchor,\n required,\n );\n return {db, result, replacedFile: true};\n}\n\ntype OpenedChangeLog = {\n db: Database;\n result: ReconcileResult;\n replacedFile: boolean;\n /** The file's persistent `auto_vacuum` mode, read back after the pragmas. */\n autoVacuum: number;\n};\n\nfunction openAndReconcile(\n lc: LogContext,\n replicaFile: string,\n anchor: AnchorResolver,\n boundReconciliation: boolean,\n reseedReason?: ReseedReason | undefined,\n): OpenedChangeLog {\n const db = openChangeLogDB(lc, replicaFile, {readonly: false});\n try {\n applyChangeLogPragmas(db);\n const autoVacuum = readAutoVacuum(db);\n return {\n db,\n result: reconcileChangeLog(lc, db, anchor(db), {\n rebuildInsteadOfUnboundedWork: boundReconciliation,\n reseedReason,\n }),\n replacedFile: false,\n autoVacuum,\n };\n } catch (e) {\n // The caller never sees this handle, so it closes here or leaks — and it\n // must be closed before the corruption path deletes the file.\n db.close();\n throw e;\n }\n}\n\nexport type ReseedReason =\n | 'created' // file or table absent\n | 'schema-mismatch'\n | 'identity-mismatch'\n | 'gap' // head does not land on the resume watermark after truncation\n | 'oversized-truncate'; // preserving the prefix would require too much work\n\n/**\n * Reconciliation runs synchronously on the change-streamer's event loop. A\n * normal phantom is one transaction, so work above either limit is answered by\n * unlinking and reseeding this disposable cache rather than deleting an\n * arbitrarily large suffix in place.\n */\nexport const MAX_RECONCILE_TRUNCATE_ROWS = 10_000;\nexport const MAX_RECONCILE_TRUNCATE_BYTES = 16 * 1024 * 1024;\n\nexport class ChangeLogRebuildRequired extends Error {\n override readonly name = 'ChangeLogRebuildRequired';\n readonly reason: ReseedReason;\n readonly rows?: number | undefined;\n readonly estimatedBytes?: number | undefined;\n\n constructor(\n reason: ReseedReason,\n rows?: number | undefined,\n estimatedBytes?: number | undefined,\n ) {\n super(`SQLite change log requires a ${reason} rebuild`);\n this.reason = reason;\n this.rows = rows;\n this.estimatedBytes = estimatedBytes;\n }\n}\n\ntype ReconcileOptions = {\n /** Throw before an unbounded delete/drop so the file owner can unlink it. */\n readonly rebuildInsteadOfUnboundedWork?: boolean | undefined;\n /** Preserve the reason that caused an old file to be unlinked. */\n readonly reseedReason?: ReseedReason | undefined;\n};\n\n/**\n * `cookiesStale` records that the cookie set the log held did not survive\n * reconciliation. The cookies are unversioned point-in-time state, so any\n * reconciliation that moves the head invalidates them, and the same transaction\n * replaces them with the anchor's — the set that belongs to the resume\n * watermark (see `change-log-cookies.ts`). It is `action !== 'none'` today; it\n * is a field rather than a derivation because the replacement reads as intent\n * at the call site, and because it is what the `cookie_reseed` counter is keyed\n * on.\n */\nexport type ReconcileResult =\n | {action: 'none'; head: string; cookiesStale: false}\n | {action: 'truncated'; head: string; rows: number; cookiesStale: true}\n | {\n action: 'reseeded';\n head: string;\n reason: ReseedReason;\n cookiesStale: true;\n };\n\n/**\n * Brings the change log into agreement with the watermark its stream connection\n * resumes from, in one transaction on the change-log database:\n *\n * ```\n * head === resumeWatermark → keep appending\n * head > resumeWatermark → truncate above resumeWatermark\n * otherwise → wipe, seed at resumeWatermark, record the reason\n * ```\n *\n * The log commits before anything that can advance the resume watermark, so a\n * *phantom* — a transaction the log holds whose watermark the resumed stream\n * re-delivers — is the normal state, and truncate-above is the whole of\n * reconciliation rather than a special case. A phantom's rows all carry the\n * transaction's commit watermark, which is strictly greater than the resume\n * watermark, so deleting on that predicate removes phantoms whole and retains\n * every transaction at or below the resume point.\n *\n * It must run per stream connection, not once per process: the stream loop\n * re-reads its resume watermark on every reconnect, and the writer inserts with\n * a plain `INSERT` against `PRIMARY KEY (\"watermark\",\"pos\")`, so an\n * un-truncated overlap is a constraint violation on the first re-delivered\n * transaction.\n *\n * Anything truncation cannot resolve — the file was deleted, the writer was\n * disabled for a while, the replica was restored, power was lost with\n * `synchronous=NORMAL` — surfaces as a head that does not land on the resume\n * watermark, and is resolved by wiping and reseeding there.\n *\n * Whichever of the two moves the head also replaces the cookie set with the one\n * the anchor carries, in the same transaction: the head and the cookies are one\n * position, and nothing may observe them at two.\n */\nexport function reconcileChangeLog(\n lc: LogContext,\n db: Database,\n anchor: ChangeLogAnchor,\n opts: ReconcileOptions = {},\n): ReconcileResult {\n db.exec('BEGIN IMMEDIATE');\n try {\n if (opts.rebuildInsteadOfUnboundedWork) {\n const required = rebuildRequired(db, anchor);\n if (required) {\n throw required;\n }\n }\n const result = opts.reseedReason\n ? reseed(lc, db, anchor, opts.reseedReason)\n : reconcile(lc, db, anchor);\n db.exec('COMMIT');\n return result;\n } catch (e) {\n if (db.inTransaction) {\n db.exec('ROLLBACK');\n }\n throw e;\n }\n}\n\n/**\n * Decides whether reconciliation can be performed within its synchronous work\n * budget. Every query is either an index seek or a scan capped at one row past\n * the delete limit. No state changes before the decision is complete.\n */\nfunction rebuildRequired(\n db: Database,\n anchor: ChangeLogAnchor,\n): ChangeLogRebuildRequired | undefined {\n const wipe = changeLogWipeReason(db, anchor.identity);\n if (wipe !== undefined) {\n return new ChangeLogRebuildRequired(wipe);\n }\n\n const head = readChangeLogHead(db);\n const {resumeWatermark} = anchor;\n if (head === null || head < resumeWatermark) {\n return new ChangeLogRebuildRequired('gap');\n }\n if (head === resumeWatermark) {\n return undefined;\n }\n\n const boundary = db\n .prepare(/*sql*/ `\n SELECT 1 FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"watermark\" = ?\n LIMIT 1\n `)\n .get<{1: number} | undefined>(resumeWatermark);\n if (boundary === undefined) {\n return new ChangeLogRebuildRequired('gap');\n }\n\n const {rows, estimatedBytes} = db\n .prepare(/*sql*/ `\n SELECT count(*) AS \"rows\",\n coalesce(sum(\"estimatedBytes\"), 0) AS \"estimatedBytes\"\n FROM (\n SELECT \"estimatedBytes\" FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"watermark\" > ?\n LIMIT ${MAX_RECONCILE_TRUNCATE_ROWS + 1}\n )\n `)\n .get<{rows: number; estimatedBytes: number}>(resumeWatermark);\n return rows > MAX_RECONCILE_TRUNCATE_ROWS ||\n estimatedBytes > MAX_RECONCILE_TRUNCATE_BYTES\n ? new ChangeLogRebuildRequired('oversized-truncate', rows, estimatedBytes)\n : undefined;\n}\n\nfunction reconcile(\n lc: LogContext,\n db: Database,\n anchor: ChangeLogAnchor,\n): ReconcileResult {\n const wipe = changeLogWipeReason(db, anchor.identity);\n if (wipe !== undefined) {\n return reseed(lc, db, anchor, wipe);\n }\n\n const {resumeWatermark} = anchor;\n let head = readChangeLogHead(db);\n let rows = 0;\n if (head !== null && head > resumeWatermark) {\n rows = db\n .prepare(/*sql*/ `\n DELETE FROM \"${CHANGE_LOG_STREAM_TABLE}\" WHERE \"watermark\" > ?\n `)\n .run(resumeWatermark).changes;\n head = readChangeLogHead(db);\n }\n\n if (head !== resumeWatermark) {\n return reseed(lc, db, anchor, 'gap');\n }\n if (rows > 0) {\n // The truncated transactions can have carried `create-table`,\n // `add-column`, and `backfill-completed`, none of which the truncate rolls\n // back, so the cookie set no longer belongs to the head. The anchor's set\n // does, and it lands in this same transaction: invariant 17 holds at every\n // point another connection could observe the log.\n replaceCookies(db, anchor.cookies);\n lc.info?.('truncated phantom transactions from the SQLite change log', {\n sqliteChangeLogReconcile: {head, rows, cookies: cookieCounts(anchor)},\n });\n return {action: 'truncated', head, rows, cookiesStale: true};\n }\n lc.debug?.('SQLite change log is at the resume watermark', {\n sqliteChangeLogReconcile: {head},\n });\n return {action: 'none', head, cookiesStale: false};\n}\n\n/**\n * Returns the reason that the log must be recreated. Returns `undefined` when\n * the writer can continue to use the log.\n *\n * This function takes only the expected identity because callers use the result\n * to select the anchor.\n */\nexport function changeLogWipeReason(\n db: Database,\n identity: ChangeLogIdentity,\n): ReseedReason | undefined {\n if (\n !tableExists(db, CHANGE_LOG_STREAM_TABLE) ||\n !tableExists(db, CHANGE_LOG_META_TABLE)\n ) {\n return 'created';\n }\n // The schema version is read on its own because it is the one column every\n // version of this table has had. Reading the rest of a v1 row would fail on\n // its missing columns instead of reporting the mismatch that explains it.\n const version = db\n .prepare(/*sql*/ `\n SELECT \"schemaVersion\" FROM \"${CHANGE_LOG_META_TABLE}\"\n `)\n .get<{schemaVersion: number} | undefined>();\n if (version === undefined) {\n return 'created';\n }\n if (version.schemaVersion !== CHANGE_LOG_DB_SCHEMA_VERSION) {\n return 'schema-mismatch';\n }\n // Checked *after* the version, so that every pre-v4 file — which is every\n // file in the fleet at the upgrade — reports the `schema-mismatch` that\n // explains it rather than the `created` its absent cookie tables would\n // otherwise look like.\n if (\n !tableExists(db, CHANGE_LOG_TABLE_METADATA_TABLE) ||\n !tableExists(db, CHANGE_LOG_BACKFILLING_TABLE)\n ) {\n return 'created';\n }\n const {epoch, generation, replicaID} = readChangeLogMeta(db);\n if (\n epoch !== identity.epoch ||\n generation !== identity.generation ||\n replicaID !== identity.replicaID\n ) {\n return 'identity-mismatch';\n }\n return undefined;\n}\n\nfunction reseed(\n lc: LogContext,\n db: Database,\n anchor: ChangeLogAnchor,\n reason: ReseedReason,\n): ReconcileResult {\n const {identity, resumeWatermark, nowMs} = anchor;\n // Dropping the table drops its partial index with it. The cookie tables go\n // with the buffer: a cookie set without the transactions it was folded from\n // is not a smaller cookie set, it is a wrong one.\n db.exec(/*sql*/ `\n DROP TABLE IF EXISTS \"${CHANGE_LOG_STREAM_TABLE}\";\n DROP TABLE IF EXISTS \"${CHANGE_LOG_META_TABLE}\";\n ${DROP_CHANGE_LOG_COOKIE_TABLES}\n ${CREATE_CHANGE_LOG_STREAM_SCHEMA}\n ${CREATE_CHANGE_LOG_META_SCHEMA}\n ${CREATE_CHANGE_LOG_COOKIE_SCHEMA}\n `);\n db.prepare(/*sql*/ `\n INSERT INTO \"${CHANGE_LOG_META_TABLE}\"\n (\"epoch\", \"generation\", \"replicaID\", \"schemaVersion\", \"seededAtMs\",\n \"seedWatermark\")\n VALUES (@epoch, @generation, @replicaID, @schemaVersion, @seededAtMs,\n @seedWatermark)\n `).run({\n epoch: identity.epoch,\n generation: identity.generation,\n replicaID: identity.replicaID,\n schemaVersion: CHANGE_LOG_DB_SCHEMA_VERSION,\n seededAtMs: nowMs,\n seedWatermark: resumeWatermark,\n });\n seedChangeLogStream(db, anchor);\n // Into tables this statement just created, so the wholesale replacement is\n // all insert. It is still the same call the truncate path makes: a reseed is\n // the case where the log's cookie set was not merely moved but discarded, and\n // the anchor's is equally what belongs at the head either way.\n replaceCookies(db, anchor.cookies);\n\n lc.info?.('reseeded the SQLite change log', {\n sqliteChangeLogReconcile: {\n reason,\n head: resumeWatermark,\n ...identity,\n schemaVersion: CHANGE_LOG_DB_SCHEMA_VERSION,\n seededAtMs: nowMs,\n cookies: cookieCounts(anchor),\n },\n });\n return {\n action: 'reseeded',\n head: resumeWatermark,\n reason,\n cookiesStale: true,\n };\n}\n\n/**\n * Seeds a valid synthetic transaction at the anchor's resume watermark, making\n * an otherwise empty log serviceable as a catchup boundary for a subscriber\n * that is exactly at that watermark.\n */\nexport function seedChangeLogStream(\n db: Database,\n anchor: ChangeLogAnchor,\n): void {\n const beginChange = '{\"tag\":\"begin\"}';\n const commitChange = '{\"tag\":\"commit\"}';\n db.prepare(SEED_CHANGE_LOG_STREAM_SQL).run({\n watermark: anchor.resumeWatermark,\n beginBytes: estimateChangeLogStreamRowBytes(\n anchor.resumeWatermark,\n 'begin',\n beginChange,\n ),\n beginChange,\n commitBytes: estimateChangeLogStreamRowBytes(\n anchor.resumeWatermark,\n 'commit',\n commitChange,\n anchor.resumeWatermark,\n true,\n ),\n commitChange,\n writeTimeMs: anchor.nowMs,\n });\n}\n\n/** Reads the whole meta row. Throws if the log has not been reconciled. */\nexport function readChangeLogMeta(db: Database): ChangeLogMeta {\n const meta = db\n .prepare(/*sql*/ `\n SELECT \"epoch\", \"generation\", \"replicaID\", \"schemaVersion\",\n \"seededAtMs\", \"seedWatermark\"\n FROM \"${CHANGE_LOG_META_TABLE}\"\n `)\n .get<ChangeLogMeta | undefined>();\n if (meta === undefined) {\n throw new Error('the SQLite change log has no meta row');\n }\n return meta;\n}\n\n/**\n * `max()` on the primary key's leading column, i.e. an index seek. Null when\n * the log is empty.\n */\nexport function readChangeLogHead(db: Database): string | null {\n const {head} = db\n .prepare(/*sql*/ `\n SELECT max(\"watermark\") AS \"head\" FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n `)\n .get<{head: string | null}>();\n return head;\n}\n\n/**\n * The covered watermark range. Both scalar subqueries seek the watermark\n * index, so unlike a `count()` this does not scan the log. Both bounds are\n * null when it is empty.\n */\nexport function readChangeLogBounds(db: Database): ChangeLogBounds {\n return db\n .prepare(/*sql*/ `\n SELECT\n (SELECT min(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\")\n AS \"minWatermark\",\n (SELECT max(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\")\n AS \"headWatermark\"\n `)\n .get<ChangeLogBounds>();\n}\n\nexport type ChangeLogBounds = {\n readonly minWatermark: string | null;\n readonly headWatermark: string | null;\n};\n\n/**\n * What was installed, for the reconcile log line. Normally `{0, 0}`: a nonzero\n * `backfilling` means the resumed stream is being handed backfills to re-request.\n */\nfunction cookieCounts({cookies}: ChangeLogAnchor) {\n return {\n tableMetadata: cookies.tableMetadata.length,\n backfilling: cookies.backfilling.length,\n };\n}\n\nfunction tableExists(db: Database, table: string): boolean {\n return (\n db\n .prepare(/*sql*/ `\n SELECT 1 FROM \"sqlite_master\" WHERE \"type\" = 'table' AND \"name\" = ?\n `)\n .get<{1: number} | undefined>(table) !== undefined\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"change-log-db.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/change-log-db.ts"],"sourcesContent":["/**\n * The change log lives in its own SQLite database, `${replicaFile}-change-log`,\n * rather than in the replica file. It is a catchup cache: everything it holds is\n * either already durable in the replica's litestream backup or re-derivable from\n * the upstream replication slot, so it is deliberately excluded from the backup\n * and is never a source of truth.\n *\n * The change-streamer writes it, from the stream loop that fans changes out to\n * subscribers, and commits it before forwarding each transaction's `commit`\n * message. Its head is therefore always at or above the watermark the stream\n * would resume from, which is what {@link reconcileChangeLog} anchors on.\n *\n * Beside the buffer it holds the log's *cookie jar* — the backfill progress\n * state folded up to the same head. That is `change-log-cookies.ts`; this file\n * owns its tables' lifecycle, because a cookie set is only meaningful paired\n * with the watermark it was folded to, and so must be created, dropped, and —\n * whenever reconciliation moves the head — replaced with the anchor's, all with\n * the buffer rather than beside it.\n *\n * Because it is disposable, it has no migration framework. Any inconsistency —\n * a schema change, a leftover file from a different replica, a gap left by a\n * crash or by running with the writer disabled, a corrupt file — is resolved by\n * {@link reconcileChangeLog} and {@link openChangeLogDBForWriting}, which\n * truncate a bounded suffix or replace and reseed the log at the resume\n * watermark. The worst outcome is a `too-old` for a lagging subscriber; a\n * silently delivered gap is not reachable.\n *\n * Disk sizing: a task that writes the log needs room for the replica *plus* the\n * retained change stream — the log's main file, its wal2 sidecars, and pages a\n * purge has freed but not yet returned to the OS. The change-streamer schedules\n * purges against the same floor as the PG change log, so retention is bounded\n * by the confirmed backup watermark ANDed with the minimum retention window —\n * a function of the backup interval, not a constant: 15–30+ minutes behind an\n * RMv1 sync interval, ~60 seconds once litestream v5 backs up every 15–30 s.\n * A held snapshot reservation pins retention unboundedly for its duration, so\n * peak retained bytes is set by the slowest concurrent restore rather than by\n * the retention setting — size disk and alerts on that. The log is excluded\n * from the litestream backup, so this is local disk only and never S3.\n */\n\nimport {existsSync} from 'node:fs';\nimport type {LogContext} from '@rocicorp/logger';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {deleteLiteDB} from '../../db/delete-lite-db.ts';\nimport {\n isSQLiteCorruption,\n logSQLiteCorruptionDiagnostics,\n} from '../../db/sqlite-corruption.ts';\nimport {\n CHANGE_LOG_BACKFILLING_TABLE,\n CHANGE_LOG_TABLE_METADATA_TABLE,\n CREATE_CHANGE_LOG_COOKIE_SCHEMA,\n DROP_CHANGE_LOG_COOKIE_TABLES,\n replaceCookies,\n type CookieSet,\n} from './change-log-cookies.ts';\n\n/**\n * Bumped whenever the change-log database's schema changes. Since the file is a\n * cache, a mismatch costs one reseed rather than a migration.\n *\n * v2 re-homed the writer to the change-streamer: the log anchors on the\n * watermark its stream connection resumes from rather than on the replica's\n * state version, and its meta row carries the replica's identity triple plus\n * the seed point.\n *\n * v3 added the cookie tables (see `change-log-cookies.ts`), i.e. the log's\n * second job. The reseed it costs on upgrade buys one retention window of\n * catchup reach on a log nothing reads yet.\n *\n * v4 stores each row's tag and estimated retained bytes alongside its JSON.\n * Readers can now filter without parsing arbitrary row payloads, and\n * reconciliation can bound truncation by bytes as well as rows.\n *\n * `auto_vacuum = INCREMENTAL` arrived within v2, deliberately without a bump:\n * it is a file-header property rather than a schema change, and the reseed a\n * version mismatch triggers cannot enable it (see\n * {@link applyChangeLogPragmas}), so {@link openChangeLogDBForWriting} guards\n * on the pragma itself instead of on this number.\n */\nexport const CHANGE_LOG_DB_SCHEMA_VERSION = 4;\n\n/** https://www.sqlite.org/pragma.html#pragma_auto_vacuum */\nconst AUTO_VACUUM_INCREMENTAL = 2;\n\nexport const CHANGE_LOG_STREAM_TABLE = '_zero.changeLogStream';\nexport const CHANGE_LOG_STREAM_WRITE_TIME_INDEX =\n '_zero.changeLogStream_writeTimeMs';\n\nconst INTEGER_BYTES = 8;\n\n/**\n * Estimates the retained payload size of a stream row. This deliberately does\n * not claim to measure SQLite b-tree/page overhead; it is stable across the\n * write path, reconciliation, and the startup scan used by observability.\n */\nexport function estimateChangeLogStreamRowBytes(\n watermark: string,\n tag: string,\n change: string,\n precommit?: string,\n hasWriteTime = false,\n): number {\n return (\n Buffer.byteLength(watermark) +\n INTEGER_BYTES +\n Buffer.byteLength(tag) +\n Buffer.byteLength(change) +\n (precommit === undefined ? 0 : Buffer.byteLength(precommit)) +\n (hasWriteTime ? INTEGER_BYTES : 0)\n );\n}\n\n// The index is partial because only commit rows carry a \"writeTimeMs\", so it\n// holds one entry per transaction rather than one per change. It is what the\n// purger scans to find transactions older than the retention window.\n//\n// Pre-release builds briefly carried this table in the replica; see the test\n// fixture `CREATE_V14_CHANGE_LOG_STREAM` in\n// `change-source/common/replica-schema.ts`. Its old shape must not be kept in\n// sync with this one.\nexport const CREATE_CHANGE_LOG_STREAM_SCHEMA = /*sql*/ `\n CREATE TABLE \"${CHANGE_LOG_STREAM_TABLE}\" (\n \"watermark\" TEXT NOT NULL,\n \"pos\" INTEGER NOT NULL,\n \"tag\" TEXT NOT NULL,\n \"estimatedBytes\" INTEGER NOT NULL,\n \"change\" TEXT NOT NULL,\n \"precommit\" TEXT,\n \"writeTimeMs\" INTEGER,\n PRIMARY KEY (\"watermark\", \"pos\")\n );\n\n CREATE INDEX \"${CHANGE_LOG_STREAM_WRITE_TIME_INDEX}\"\n ON \"${CHANGE_LOG_STREAM_TABLE}\" (\"writeTimeMs\", \"watermark\")\n WHERE \"writeTimeMs\" IS NOT NULL;\n`;\n\n/**\n * Inserts the synthetic begin/commit pair for the seed watermark.\n *\n * Both rows are inserted by one statement so a caller outside a larger\n * transaction cannot leave a partial seed.\n */\nconst SEED_CHANGE_LOG_STREAM_SQL = /*sql*/ `\n INSERT INTO \"${CHANGE_LOG_STREAM_TABLE}\"\n (\"watermark\", \"pos\", \"tag\", \"estimatedBytes\", \"change\", \"precommit\",\n \"writeTimeMs\")\n VALUES\n (@watermark, 0, 'begin', @beginBytes, @beginChange, NULL, NULL),\n (@watermark, 1, 'commit', @commitBytes, @commitChange, @watermark,\n @writeTimeMs)\n ON CONFLICT (\"watermark\", \"pos\") DO NOTHING\n`;\n\nexport const CHANGE_LOG_META_TABLE = '_zero.changeLogMeta';\n\n// The identity triple (see ChangeLogIdentity) detects a file left behind by a\n// different replica -- most concretely, a leftover log on a reused volume.\n//\n// \"seededAtMs\" : when the log was seeded, i.e. how far its retention window\n// can possibly reach back. Slice 11 uses it as the warm-up\n// signal for canary reads.\n// \"seedWatermark\" : where the log was seeded. `min(\"watermark\")` cannot answer\n// this once purge has advanced the minimum, and the\n// difference is what distinguishes a log that purged past the\n// retention floor (an invariant violation) from one that\n// never held that history at all (routine at every fork).\n// \"lock\" : enforces single-row semantics, as in\n// \"_zero.replicationConfig\".\nconst CREATE_CHANGE_LOG_META_SCHEMA = /*sql*/ `\n CREATE TABLE \"${CHANGE_LOG_META_TABLE}\" (\n \"epoch\" TEXT,\n \"generation\" TEXT NOT NULL,\n \"replicaID\" TEXT,\n \"schemaVersion\" INTEGER NOT NULL,\n \"seededAtMs\" INTEGER NOT NULL,\n \"seedWatermark\" TEXT NOT NULL,\n \"lock\" INTEGER PRIMARY KEY DEFAULT 1 CHECK (\"lock\" = 1)\n );\n`;\n\n/**\n * `${replicaFile}-change-log`, matching the `-serving-copy` convention.\n *\n * The name outlives the writer's move into the change-streamer: the\n * change-streamer derives the path from the replica file it owns, so renaming\n * would save no plumbing and would orphan the old file on every existing\n * volume, where nothing would delete it.\n *\n * Its `-wal` / `-wal2` / `-shm` sidecars are suffixes of *this* name, so they\n * never collide with the replica's own sidecars.\n */\nexport function changeLogFileName(replicaFile: string): string {\n return `${replicaFile}-change-log`;\n}\n\n/**\n * Removes the change-log database and its `-wal` / `-wal2` / `-shm` sidecars.\n * Safe to call when the file does not exist.\n */\nexport function deleteChangeLogDB(replicaFile: string): void {\n deleteLiteDB(changeLogFileName(replicaFile));\n}\n\n/**\n * Which replica the log's contents belong to.\n *\n * `generation` is RMv1's `replicaVersion`: the version at which initial sync\n * copied every row. It is shared by every sibling of a forked replica, which is\n * why it cannot be the whole of the identity under RMv2 — `epoch` and\n * `replicaID` are what distinguish two siblings' logs. Both are `null` until\n * RMv2 supplies them, and a `null` matches only another `null`, so a log\n * written before they exist is still recognized as this replica's.\n */\nexport type ChangeLogIdentity = {\n readonly epoch: string | null;\n readonly generation: string;\n readonly replicaID: string | null;\n};\n\n/**\n * Where the stream connection resumes, and everything the log's state at that\n * position consists of.\n *\n * The two fields travel together because a cookie set is only meaningful paired\n * with the watermark it was folded up to (invariant 15). They are read from one\n * store, in one snapshot: `Storer.getStartStreamInitializationParameters()`\n * reads both from Postgres in a single REPEATABLE READ transaction. Pairing one\n * store's cookies with another's watermark would silently drop every backfill\n * that completed in the interval, and those rows are unrecoverable — backfill\n * transactions are synthesized by the change source rather than replayed from\n * the slot.\n */\nexport type ChangeLogResumePoint = {\n /**\n * The watermark this stream connection resumes from. The log's own head is\n * measured against it, and a reseed seeds a synthetic transaction at it.\n *\n * Deliberately *not* \"the replica's state version\": after the writer moved to\n * the change-streamer, the canonical replicator is a subscriber downstream of\n * the write loop, so its state version is a consequence of this log rather\n * than a constraint on it.\n */\n readonly resumeWatermark: string;\n /**\n * The cookie set that belongs to {@link resumeWatermark}. Any reconciliation\n * that moves the head invalidates the set the log was holding — the cookies\n * are unversioned point-in-time state, and truncating the stream above `W`\n * does not roll them back to `W` — so the same transaction installs this one\n * in its place (invariant 17).\n *\n * Postgres supplies it while it remains the store the stream resumes from. At\n * the C5 flip the log's own head *is* the resume point, phantoms cannot\n * exist, and both this field and the truncate-above path become dead code.\n */\n readonly cookies: CookieSet;\n};\n\n/**\n * What the log is reconciled against. Reconciliation runs once per stream\n * connection, since every reconnect can re-read a different resume point.\n */\nexport type ChangeLogAnchor = ChangeLogResumePoint & {\n readonly identity: ChangeLogIdentity;\n /**\n * The change-streamer's clock, at reconciliation time. Supplied rather than\n * read inline so that tests control it. Used for `seededAtMs` and for the\n * seed transaction's `writeTimeMs`, which is what the purger's time floor\n * measures.\n */\n readonly nowMs: number;\n};\n\n/** The meta row, i.e. everything the log records about itself. */\nexport type ChangeLogMeta = ChangeLogIdentity & {\n readonly schemaVersion: number;\n readonly seededAtMs: number;\n readonly seedWatermark: string;\n};\n\n/**\n * Opens the change-log database beside `replicaFile`.\n *\n * A writable handle creates the file if it is absent; a `readonly` handle\n * throws, which is the normal state for a change-streamer whose writer is\n * disabled. Pragmas are the caller's responsibility, since only the writer sets\n * the persistent ones.\n */\nexport function openChangeLogDB(\n lc: LogContext,\n replicaFile: string,\n opts: {readonly: boolean},\n): Database {\n return new Database(\n lc.withContext('component', 'change-log-db'),\n changeLogFileName(replicaFile),\n {readonly: opts.readonly},\n );\n}\n\n/**\n * Configures the change-log database, which the change-streamer's writer owns\n * exclusively. These are the settings the `separate-files` mode of\n * `sqlite-change-log-ceiling.bench.ts` measured.\n *\n * `wal2` because the log is a continuous append that catchup reads\n * continuously, which is the case wal2 exists for: it avoids checkpoint\n * starvation without needing an autocheckpoint. `wal_autocheckpoint` therefore\n * keeps its default, unlike the backup replica's 0, which hands checkpointing\n * to litestream — nothing else owns this file.\n *\n * `synchronous = NORMAL` because the commit sits on the forward path, once per\n * upstream transaction, and NORMAL keeps that from costing an fsync. It is not\n * a durability compromise here: in WAL mode NORMAL still writes every commit\n * through to the OS, so it survives a process crash, an OOM kill, or a SIGKILL.\n * It is lost only to kernel panic or power loss, and those destroy the node —\n * the task restarts elsewhere, restores the replica from S3, and has no\n * change-log file at all. Whatever a power loss does take is detected by\n * {@link reconcileChangeLog} and reseeded.\n *\n * `auto_vacuum = INCREMENTAL` so that purge can return freed pages to the OS\n * rather than leaving the file at its high-water mark. **It must be the first\n * statement here.** SQLite honours a `none` -> `incremental` transition only\n * while the file is still empty, and `journal_mode = wal2` materializes page 1;\n * appending the pragma instead of prepending it silently leaves the file at\n * mode 0 forever, since neither `DROP TABLE` nor a reseed makes a file empty\n * again. {@link openChangeLogDBForWriting} verifies the result.\n */\nexport function applyChangeLogPragmas(db: Database): void {\n db.pragma('auto_vacuum = INCREMENTAL');\n db.pragma('busy_timeout = 30000');\n db.pragma('analysis_limit = 1000');\n db.pragma('journal_mode = wal2');\n db.pragma('synchronous = NORMAL');\n}\n\nfunction readAutoVacuum(db: Database): number {\n const [{auto_vacuum: mode}] = db.pragma<{auto_vacuum: number}>('auto_vacuum');\n return mode;\n}\n\n/**\n * Opens the change-log database for the writer, applies its pragmas, and\n * reconciles it against `anchor`, returning the handle and what reconciliation\n * had to do.\n *\n * Corruption is answered with a rebuild rather than an escalation, which is a\n * deliberate divergence from how the replica's own corruption is handled. A\n * corrupt replica is a lost source of truth, so it ends in a restore or an\n * auto-reset; the change log holds nothing that is not either already in the\n * replica's litestream backup or re-derivable from the upstream slot, so the\n * entire response is to delete the file and seed a new one at the resume\n * watermark. The cost is one retention window of catchup — reconnecting\n * subscribers below the head get `too-old` — which is what every other\n * {@link ReseedReason} costs.\n *\n * Only corruption is rebuilt through. Anything else (a bad path, a permissions\n * error, a full disk) is a problem with the environment rather than with the\n * file's contents, and deleting a database in response would destroy state\n * without fixing it. Those propagate to the caller, which disables the writer\n * and keeps replicating.\n *\n * A file that predates `auto_vacuum = INCREMENTAL` takes the same rebuild, for\n * the same reason and at the same cost: the pragma only takes on an empty file,\n * so a reseed cannot enable it. Unlike corruption, a *second* failure there is\n * tolerated rather than thrown — see {@link ensureIncrementalAutoVacuum}.\n */\n/**\n * Returns an anchor after the database opens.\n *\n * The resolver can inspect the existing file before it selects the anchor. A\n * valid log can use its own head. A new or invalid log uses the replica seed.\n * The rebuild path calls the resolver again for the new file.\n */\nexport type AnchorResolver = (db: Database) => ChangeLogAnchor;\n\nexport function openChangeLogDBForWriting(\n lc: LogContext,\n replicaFile: string,\n anchor: AnchorResolver,\n): {db: Database; result: ReconcileResult; replacedFile: boolean} {\n const existed = existsSync(changeLogFileName(replicaFile));\n const opened = openOrRebuildCorrupt(lc, replicaFile, anchor, existed);\n const {db, result, replacedFile} = ensureIncrementalAutoVacuum(\n lc,\n replicaFile,\n anchor,\n opened,\n );\n return {db, result, replacedFile};\n}\n\nfunction openOrRebuildCorrupt(\n lc: LogContext,\n replicaFile: string,\n anchor: AnchorResolver,\n boundReconciliation: boolean,\n): OpenedChangeLog {\n try {\n return openAndReconcile(lc, replicaFile, anchor, boundReconciliation);\n } catch (e) {\n if (e instanceof ChangeLogRebuildRequired) {\n return rebuildChangeLog(lc, replicaFile, anchor, e);\n }\n if (!isSQLiteCorruption(e)) {\n throw e;\n }\n const file = changeLogFileName(replicaFile);\n logSQLiteCorruptionDiagnostics(lc, 'change-log', file, e);\n lc.error?.('rebuilding the corrupt SQLite change log', {\n sqliteChangeLog: {file},\n });\n return rebuildChangeLog(lc, replicaFile, anchor);\n }\n}\n\n/**\n * Rebuilds a log whose `auto_vacuum` mode is not `INCREMENTAL`, which is every\n * log created before the pragma shipped — including every v2 file the writer\n * wrote in the meantime, which is why the guard is on the pragma rather than\n * on the schema version. Deleting the file is the only way to enable it:\n * `DROP TABLE` leaves the header's mode intact, and the in-place alternative —\n * a full `VACUUM` — is an unbounded blocking statement on a file the writer\n * needs.\n *\n * A mode that is still wrong after the rebuild is logged and tolerated, which\n * is the one place this diverges from the corruption path. The two failures are\n * not alike: a file that stays corrupt after a rebuild is an environment\n * problem, but a pragma that stays wrong on a brand-new file can only mean\n * {@link applyChangeLogPragmas}' statement order is wrong — a code bug, present\n * on every task, on every start. Throwing there turns a lost optimization into\n * a crash loop, and rebuilding again turns it into a reseed per start, which is\n * worse than the freelist growth it was trying to avoid. `Database.compact()`\n * already warns and no-ops when the mode is not `INCREMENTAL`, so degrading is\n * the accepted fallback.\n */\nfunction ensureIncrementalAutoVacuum(\n lc: LogContext,\n replicaFile: string,\n anchor: AnchorResolver,\n opened: OpenedChangeLog,\n): OpenedChangeLog {\n if (opened.autoVacuum === AUTO_VACUUM_INCREMENTAL) {\n return opened;\n }\n const file = changeLogFileName(replicaFile);\n lc.info?.('rebuilding the SQLite change log to enable incremental vacuum', {\n sqliteChangeLog: {file, autoVacuum: opened.autoVacuum},\n });\n opened.db.close();\n const rebuilt = rebuildChangeLog(lc, replicaFile, anchor);\n if (rebuilt.autoVacuum !== AUTO_VACUUM_INCREMENTAL) {\n lc.error?.(\n 'SQLite change log does not support incremental vacuum; ' +\n 'it will plateau at its high-water mark',\n {sqliteChangeLog: {file, autoVacuum: rebuilt.autoVacuum}},\n );\n }\n return rebuilt;\n}\n\n/** The shared \"delete the file and open a new one\" body. */\nfunction rebuildChangeLog(\n lc: LogContext,\n replicaFile: string,\n anchor: AnchorResolver,\n required?: ChangeLogRebuildRequired | undefined,\n): OpenedChangeLog {\n if (required) {\n lc.warn?.(\n 'rebuilding the SQLite change log instead of running an unbounded reconciliation',\n {\n sqliteChangeLogRebuild: {\n reason: required.reason,\n rows: required.rows,\n estimatedBytes: required.estimatedBytes,\n },\n },\n );\n }\n deleteChangeLogDB(replicaFile);\n // The file is absent, so the reseed only drops empty/nonexistent tables. A\n // second failure is the environment's, not the old file's.\n const opened = openAndReconcile(\n lc,\n replicaFile,\n anchor,\n false,\n required?.reason,\n );\n return {...opened, replacedFile: true};\n}\n\n/**\n * Replaces an already-closed change-log file after bounded reconciliation\n * declined its work. The caller closes cached readers before calling this so\n * none can continue serving the unlinked inode.\n */\nexport function rebuildChangeLogDBForWriting(\n lc: LogContext,\n replicaFile: string,\n anchor: ChangeLogAnchor,\n required: ChangeLogRebuildRequired,\n): {db: Database; result: ReconcileResult; replacedFile: true} {\n const {db, result} = rebuildChangeLog(\n lc,\n replicaFile,\n () => anchor,\n required,\n );\n return {db, result, replacedFile: true};\n}\n\ntype OpenedChangeLog = {\n db: Database;\n result: ReconcileResult;\n replacedFile: boolean;\n /** The file's persistent `auto_vacuum` mode, read back after the pragmas. */\n autoVacuum: number;\n};\n\nfunction openAndReconcile(\n lc: LogContext,\n replicaFile: string,\n anchor: AnchorResolver,\n boundReconciliation: boolean,\n reseedReason?: ReseedReason | undefined,\n): OpenedChangeLog {\n const db = openChangeLogDB(lc, replicaFile, {readonly: false});\n try {\n applyChangeLogPragmas(db);\n const autoVacuum = readAutoVacuum(db);\n return {\n db,\n result: reconcileChangeLog(lc, db, anchor(db), {\n rebuildInsteadOfUnboundedWork: boundReconciliation,\n reseedReason,\n }),\n replacedFile: false,\n autoVacuum,\n };\n } catch (e) {\n // The caller never sees this handle, so it closes here or leaks — and it\n // must be closed before the corruption path deletes the file.\n db.close();\n throw e;\n }\n}\n\nexport type ReseedReason =\n | 'created' // file or table absent\n | 'schema-mismatch'\n | 'identity-mismatch'\n | 'gap' // head does not land on the resume watermark after truncation\n | 'oversized-truncate'; // preserving the prefix would require too much work\n\n/**\n * Reconciliation runs synchronously on the change-streamer's event loop. A\n * normal phantom is one transaction, so work above either limit is answered by\n * unlinking and reseeding this disposable cache rather than deleting an\n * arbitrarily large suffix in place.\n */\nexport const MAX_RECONCILE_TRUNCATE_ROWS = 10_000;\nexport const MAX_RECONCILE_TRUNCATE_BYTES = 16 * 1024 * 1024;\n\nexport class ChangeLogRebuildRequired extends Error {\n override readonly name = 'ChangeLogRebuildRequired';\n readonly reason: ReseedReason;\n readonly rows?: number | undefined;\n readonly estimatedBytes?: number | undefined;\n\n constructor(\n reason: ReseedReason,\n rows?: number | undefined,\n estimatedBytes?: number | undefined,\n ) {\n super(`SQLite change log requires a ${reason} rebuild`);\n this.reason = reason;\n this.rows = rows;\n this.estimatedBytes = estimatedBytes;\n }\n}\n\ntype ReconcileOptions = {\n /** Throw before an unbounded delete/drop so the file owner can unlink it. */\n readonly rebuildInsteadOfUnboundedWork?: boolean | undefined;\n /** Preserve the reason that caused an old file to be unlinked. */\n readonly reseedReason?: ReseedReason | undefined;\n};\n\n/**\n * `cookiesStale` records that the cookie set the log held did not survive\n * reconciliation. The cookies are unversioned point-in-time state, so any\n * reconciliation that moves the head invalidates them, and the same transaction\n * replaces them with the anchor's — the set that belongs to the resume\n * watermark (see `change-log-cookies.ts`). It is `action !== 'none'` today; it\n * is a field rather than a derivation because the replacement reads as intent\n * at the call site, and because it is what the `cookie_reseed` counter is keyed\n * on.\n */\nexport type ReconcileResult =\n | {action: 'none'; head: string; cookiesStale: false}\n | {action: 'truncated'; head: string; rows: number; cookiesStale: true}\n | {\n action: 'reseeded';\n head: string;\n reason: ReseedReason;\n cookiesStale: true;\n };\n\n/**\n * Brings the change log into agreement with the watermark its stream connection\n * resumes from, in one transaction on the change-log database:\n *\n * ```\n * head === resumeWatermark → keep appending\n * head > resumeWatermark → truncate above resumeWatermark\n * otherwise → wipe, seed at resumeWatermark, record the reason\n * ```\n *\n * The log commits before anything that can advance the resume watermark, so a\n * *phantom* — a transaction the log holds whose watermark the resumed stream\n * re-delivers — is the normal state, and truncate-above is the whole of\n * reconciliation rather than a special case. A phantom's rows all carry the\n * transaction's commit watermark, which is strictly greater than the resume\n * watermark, so deleting on that predicate removes phantoms whole and retains\n * every transaction at or below the resume point.\n *\n * It must run per stream connection, not once per process: the stream loop\n * re-reads its resume watermark on every reconnect, and the writer inserts with\n * a plain `INSERT` against `PRIMARY KEY (\"watermark\",\"pos\")`, so an\n * un-truncated overlap is a constraint violation on the first re-delivered\n * transaction.\n *\n * Anything truncation cannot resolve — the file was deleted, the writer was\n * disabled for a while, the replica was restored, power was lost with\n * `synchronous=NORMAL` — surfaces as a head that does not land on the resume\n * watermark, and is resolved by wiping and reseeding there.\n *\n * Whichever of the two moves the head also replaces the cookie set with the one\n * the anchor carries, in the same transaction: the head and the cookies are one\n * position, and nothing may observe them at two.\n */\nexport function reconcileChangeLog(\n lc: LogContext,\n db: Database,\n anchor: ChangeLogAnchor,\n opts: ReconcileOptions = {},\n): ReconcileResult {\n db.exec('BEGIN IMMEDIATE');\n try {\n if (opts.rebuildInsteadOfUnboundedWork) {\n const required = rebuildRequired(db, anchor);\n if (required) {\n throw required;\n }\n }\n const result = opts.reseedReason\n ? reseed(lc, db, anchor, opts.reseedReason)\n : reconcile(lc, db, anchor);\n db.exec('COMMIT');\n return result;\n } catch (e) {\n if (db.inTransaction) {\n db.exec('ROLLBACK');\n }\n throw e;\n }\n}\n\n/**\n * Decides whether reconciliation can be performed within its synchronous work\n * budget. Every query is either an index seek or a scan capped at one row past\n * the delete limit. No state changes before the decision is complete.\n */\nfunction rebuildRequired(\n db: Database,\n anchor: ChangeLogAnchor,\n): ChangeLogRebuildRequired | undefined {\n const wipe = changeLogWipeReason(db, anchor.identity);\n if (wipe !== undefined) {\n return new ChangeLogRebuildRequired(wipe);\n }\n\n const head = readChangeLogHead(db);\n const {resumeWatermark} = anchor;\n if (head === null || head < resumeWatermark) {\n return new ChangeLogRebuildRequired('gap');\n }\n if (head === resumeWatermark) {\n return undefined;\n }\n\n const boundary = db\n .prepare(/*sql*/ `\n SELECT 1 FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"watermark\" = ?\n LIMIT 1\n `)\n .get<{1: number} | undefined>(resumeWatermark);\n if (boundary === undefined) {\n return new ChangeLogRebuildRequired('gap');\n }\n\n const {rows, estimatedBytes} = db\n .prepare(/*sql*/ `\n SELECT count(*) AS \"rows\",\n coalesce(sum(\"estimatedBytes\"), 0) AS \"estimatedBytes\"\n FROM (\n SELECT \"estimatedBytes\" FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"watermark\" > ?\n LIMIT ${MAX_RECONCILE_TRUNCATE_ROWS + 1}\n )\n `)\n .get<{rows: number; estimatedBytes: number}>(resumeWatermark);\n return rows > MAX_RECONCILE_TRUNCATE_ROWS ||\n estimatedBytes > MAX_RECONCILE_TRUNCATE_BYTES\n ? new ChangeLogRebuildRequired('oversized-truncate', rows, estimatedBytes)\n : undefined;\n}\n\nfunction reconcile(\n lc: LogContext,\n db: Database,\n anchor: ChangeLogAnchor,\n): ReconcileResult {\n const wipe = changeLogWipeReason(db, anchor.identity);\n if (wipe !== undefined) {\n return reseed(lc, db, anchor, wipe);\n }\n\n const {resumeWatermark} = anchor;\n let head = readChangeLogHead(db);\n let rows = 0;\n if (head !== null && head > resumeWatermark) {\n rows = db\n .prepare(/*sql*/ `\n DELETE FROM \"${CHANGE_LOG_STREAM_TABLE}\" WHERE \"watermark\" > ?\n `)\n .run(resumeWatermark).changes;\n head = readChangeLogHead(db);\n }\n\n if (head !== resumeWatermark) {\n return reseed(lc, db, anchor, 'gap');\n }\n if (rows > 0) {\n // The truncated transactions can have carried `create-table`,\n // `add-column`, and `backfill-completed`, none of which the truncate rolls\n // back, so the cookie set no longer belongs to the head. The anchor's set\n // does, and it lands in this same transaction: invariant 17 holds at every\n // point another connection could observe the log.\n replaceCookies(db, anchor.cookies);\n lc.info?.('truncated phantom transactions from the SQLite change log', {\n sqliteChangeLogReconcile: {head, rows, cookies: cookieCounts(anchor)},\n });\n return {action: 'truncated', head, rows, cookiesStale: true};\n }\n lc.debug?.('SQLite change log is at the resume watermark', {\n sqliteChangeLogReconcile: {head},\n });\n return {action: 'none', head, cookiesStale: false};\n}\n\n/**\n * Returns the reason that the log must be recreated. Returns `undefined` when\n * the writer can continue to use the log.\n *\n * This function takes only the expected identity because callers use the result\n * to select the anchor.\n */\nexport function changeLogWipeReason(\n db: Database,\n identity: ChangeLogIdentity,\n): ReseedReason | undefined {\n if (\n !tableExists(db, CHANGE_LOG_STREAM_TABLE) ||\n !tableExists(db, CHANGE_LOG_META_TABLE)\n ) {\n return 'created';\n }\n // The schema version is read on its own because it is the one column every\n // version of this table has had. Reading the rest of a v1 row would fail on\n // its missing columns instead of reporting the mismatch that explains it.\n const version = db\n .prepare(/*sql*/ `\n SELECT \"schemaVersion\" FROM \"${CHANGE_LOG_META_TABLE}\"\n `)\n .get<{schemaVersion: number} | undefined>();\n if (version === undefined) {\n return 'created';\n }\n if (version.schemaVersion !== CHANGE_LOG_DB_SCHEMA_VERSION) {\n return 'schema-mismatch';\n }\n // Checked *after* the version, so that every pre-v4 file — which is every\n // file in the fleet at the upgrade — reports the `schema-mismatch` that\n // explains it rather than the `created` its absent cookie tables would\n // otherwise look like.\n if (\n !tableExists(db, CHANGE_LOG_TABLE_METADATA_TABLE) ||\n !tableExists(db, CHANGE_LOG_BACKFILLING_TABLE)\n ) {\n return 'created';\n }\n const {epoch, generation, replicaID} = readChangeLogMeta(db);\n if (\n epoch !== identity.epoch ||\n generation !== identity.generation ||\n replicaID !== identity.replicaID\n ) {\n return 'identity-mismatch';\n }\n return undefined;\n}\n\nfunction reseed(\n lc: LogContext,\n db: Database,\n anchor: ChangeLogAnchor,\n reason: ReseedReason,\n): ReconcileResult {\n const {identity, resumeWatermark, nowMs} = anchor;\n // Dropping the table drops its partial index with it. The cookie tables go\n // with the buffer: a cookie set without the transactions it was folded from\n // is not a smaller cookie set, it is a wrong one.\n db.exec(/*sql*/ `\n DROP TABLE IF EXISTS \"${CHANGE_LOG_STREAM_TABLE}\";\n DROP TABLE IF EXISTS \"${CHANGE_LOG_META_TABLE}\";\n ${DROP_CHANGE_LOG_COOKIE_TABLES}\n ${CREATE_CHANGE_LOG_STREAM_SCHEMA}\n ${CREATE_CHANGE_LOG_META_SCHEMA}\n ${CREATE_CHANGE_LOG_COOKIE_SCHEMA}\n `);\n db.prepare(/*sql*/ `\n INSERT INTO \"${CHANGE_LOG_META_TABLE}\"\n (\"epoch\", \"generation\", \"replicaID\", \"schemaVersion\", \"seededAtMs\",\n \"seedWatermark\")\n VALUES (@epoch, @generation, @replicaID, @schemaVersion, @seededAtMs,\n @seedWatermark)\n `).run({\n epoch: identity.epoch,\n generation: identity.generation,\n replicaID: identity.replicaID,\n schemaVersion: CHANGE_LOG_DB_SCHEMA_VERSION,\n seededAtMs: nowMs,\n seedWatermark: resumeWatermark,\n });\n seedChangeLogStream(db, anchor);\n // Into tables this statement just created, so the wholesale replacement is\n // all insert. It is still the same call the truncate path makes: a reseed is\n // the case where the log's cookie set was not merely moved but discarded, and\n // the anchor's is equally what belongs at the head either way.\n replaceCookies(db, anchor.cookies);\n\n lc.info?.('reseeded the SQLite change log', {\n sqliteChangeLogReconcile: {\n reason,\n head: resumeWatermark,\n ...identity,\n schemaVersion: CHANGE_LOG_DB_SCHEMA_VERSION,\n seededAtMs: nowMs,\n cookies: cookieCounts(anchor),\n },\n });\n return {\n action: 'reseeded',\n head: resumeWatermark,\n reason,\n cookiesStale: true,\n };\n}\n\n/**\n * Seeds a valid synthetic transaction at the anchor's resume watermark, making\n * an otherwise empty log serviceable as a catchup boundary for a subscriber\n * that is exactly at that watermark.\n */\nexport function seedChangeLogStream(\n db: Database,\n anchor: ChangeLogAnchor,\n): void {\n const beginChange = '{\"tag\":\"begin\"}';\n const commitChange = '{\"tag\":\"commit\"}';\n db.prepare(SEED_CHANGE_LOG_STREAM_SQL).run({\n watermark: anchor.resumeWatermark,\n beginBytes: estimateChangeLogStreamRowBytes(\n anchor.resumeWatermark,\n 'begin',\n beginChange,\n ),\n beginChange,\n commitBytes: estimateChangeLogStreamRowBytes(\n anchor.resumeWatermark,\n 'commit',\n commitChange,\n anchor.resumeWatermark,\n true,\n ),\n commitChange,\n writeTimeMs: anchor.nowMs,\n });\n}\n\n/** Reads the whole meta row. Throws if the log has not been reconciled. */\nexport function readChangeLogMeta(db: Database): ChangeLogMeta {\n const meta = db\n .prepare(/*sql*/ `\n SELECT \"epoch\", \"generation\", \"replicaID\", \"schemaVersion\",\n \"seededAtMs\", \"seedWatermark\"\n FROM \"${CHANGE_LOG_META_TABLE}\"\n `)\n .get<ChangeLogMeta | undefined>();\n if (meta === undefined) {\n throw new Error('the SQLite change log has no meta row');\n }\n return meta;\n}\n\n/**\n * `max()` on the primary key's leading column, i.e. an index seek. Null when\n * the log is empty.\n */\nexport function readChangeLogHead(db: Database): string | null {\n const {head} = db\n .prepare(/*sql*/ `\n SELECT max(\"watermark\") AS \"head\" FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n `)\n .get<{head: string | null}>();\n return head;\n}\n\n/**\n * The covered watermark range. Both scalar subqueries seek the watermark\n * index, so unlike a `count()` this does not scan the log. Both bounds are\n * null when it is empty.\n */\nexport function readChangeLogBounds(db: Database): ChangeLogBounds {\n return db\n .prepare(/*sql*/ `\n SELECT\n (SELECT min(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\")\n AS \"minWatermark\",\n (SELECT max(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\")\n AS \"headWatermark\"\n `)\n .get<ChangeLogBounds>();\n}\n\nexport type ChangeLogBounds = {\n readonly minWatermark: string | null;\n readonly headWatermark: string | null;\n};\n\n/**\n * What was installed, for the reconcile log line. Normally `{0, 0}`: a nonzero\n * `backfilling` means the resumed stream is being handed backfills to re-request.\n */\nfunction cookieCounts({cookies}: ChangeLogAnchor) {\n return {\n tableMetadata: cookies.tableMetadata.length,\n backfilling: cookies.backfilling.length,\n };\n}\n\nfunction tableExists(db: Database, table: string): boolean {\n return (\n db\n .prepare(/*sql*/ `\n SELECT 1 FROM \"sqlite_master\" WHERE \"type\" = 'table' AND \"name\" = ?\n `)\n .get<{1: number} | undefined>(table) !== undefined\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-log-stream-writer.js","names":["#db","#insertChange","#insertCommit","#cookies","#watermark","#pos","#hasher","#estimatedBytes","#requireWatermark","#cookieMutations","#reset"],"sources":["../../../../../../zero-cache/src/services/replicator/change-log-stream-writer.ts"],"sourcesContent":["import type {Statement} from '../../../../zqlite/src/db.ts';\nimport type {StatementRunner} from '../../db/statements.ts';\nimport {\n isSchemaChange,\n type DataOrSchemaChange,\n} from '../change-source/protocol/current/data.ts';\nimport {extractChangeSubstring} from '../change-streamer/change-log-codec.ts';\nimport {ChangeLogTransactionHasher} from '../change-streamer/change-log-transaction-hash.ts';\nimport {ChangeLogCookieWriter, type CookieOpTag} from './change-log-cookies.ts';\nimport {\n CHANGE_LOG_STREAM_TABLE,\n estimateChangeLogStreamRowBytes,\n} from './change-log-db.ts';\n\nexport type ChangeLogStreamTransactionStats = {\n readonly rows: number;\n readonly estimatedBytes: number;\n /** Present only for transactions selected for shadow-write validation. */\n readonly hash?: string | undefined;\n /**\n * The cookie mutations this transaction's schema changes folded in, in stream\n * order. Empty for the overwhelming majority of transactions.\n */\n readonly cookieMutations: readonly CookieOpTag[];\n};\n\nexport class ChangeLogStreamInvariantError extends Error {\n override readonly name = 'ChangeLogStreamInvariantError';\n}\n\nfunction assertInvariant(\n condition: unknown,\n message: string,\n): asserts condition {\n if (!condition) {\n throw new ChangeLogStreamInvariantError(message);\n }\n}\n\n/**\n * Appends the canonical downstream stream to the change-log database, which is\n * a separate file from the replica, and folds its schema changes into the log's\n * cookie jar (`change-log-cookies.ts`) as it goes.\n *\n * This class owns the change log's transaction, and its transaction alone. It is\n * driven from the change-streamer's stream loop, which commits it before\n * forwarding a transaction's `commit` message; see `SQLiteChangeLogWriter` in\n * `change-streamer/sqlite-change-log-writer.ts` for the ordering that makes a\n * hole unreachable, and for the fail-soft policy that wraps every call here.\n *\n * Because the cookies are written in that same transaction, they are atomic with\n * the head and {@link rollback} discards them with the rows they were folded\n * from. Neither needs any handling of its own.\n */\nexport class ChangeLogStreamWriter {\n readonly #db: StatementRunner;\n readonly #insertChange: Statement;\n readonly #insertCommit: Statement;\n readonly #cookies: ChangeLogCookieWriter;\n\n #watermark: string | undefined;\n #pos = 0;\n #estimatedBytes = 0;\n #hasher: ChangeLogTransactionHasher | undefined;\n #cookieMutations: CookieOpTag[] = [];\n\n constructor(db: StatementRunner) {\n this.#db = db;\n this.#insertChange = db.db.prepare(/*sql*/ `\n INSERT INTO \"${CHANGE_LOG_STREAM_TABLE}\"\n (\"watermark\", \"pos\", \"tag\", \"estimatedBytes\", \"change\")\n VALUES (?, ?, ?, ?, ?)\n `);\n this.#insertCommit = db.db.prepare(/*sql*/ `\n INSERT INTO \"${CHANGE_LOG_STREAM_TABLE}\"\n (\"watermark\", \"pos\", \"tag\", \"estimatedBytes\", \"change\", \"precommit\",\n \"writeTimeMs\")\n VALUES (?, ?, 'commit', ?, ?, ?, ?)\n `);\n this.#cookies = new ChangeLogCookieWriter(db.db);\n }\n\n begin(\n watermark: string,\n json: string,\n storedChange = extractChangeSubstring(json, 'begin'),\n hash = true,\n ): void {\n assertInvariant(\n this.#watermark === undefined,\n `change-log stream transaction already open at ${this.#watermark}`,\n );\n // This process is the sole writer of the change-log database, so the write\n // lock is taken up front rather than risking a failed upgrade from a\n // deferred transaction partway through the append.\n this.#db.beginImmediate();\n this.#watermark = watermark;\n this.#pos = 0;\n const change = storedChange;\n const estimatedBytes = estimateChangeLogStreamRowBytes(\n watermark,\n 'begin',\n change,\n );\n this.#insertChange.run(\n watermark,\n this.#pos,\n 'begin',\n estimatedBytes,\n change,\n );\n if (hash) {\n this.#hasher = new ChangeLogTransactionHasher();\n this.#hasher.add({\n watermark,\n pos: this.#pos,\n tag: 'begin',\n change,\n precommit: null,\n });\n }\n this.#estimatedBytes = estimatedBytes;\n }\n\n /**\n * Appends one data or schema change. The parsed message is taken rather than\n * just its tag because a schema change also mutates the cookie jar, and the\n * caller already holds it — so the fold costs no re-parse.\n *\n * The cookie statements run here, i.e. inside this transaction and in stream\n * order relative to the rows they were folded from. The Postgres change log\n * has to flush its buffered batch to get the same ordering\n * (`Storer.#processQueue()`); SQLite gets it from statement order.\n */\n append(\n json: string,\n change: DataOrSchemaChange,\n storedChange = extractChangeSubstring(json, change.tag),\n ): void {\n const watermark = this.#requireWatermark();\n const {tag} = change;\n const stored = storedChange;\n const estimatedBytes = estimateChangeLogStreamRowBytes(\n watermark,\n tag,\n stored,\n );\n this.#insertChange.run(watermark, ++this.#pos, tag, estimatedBytes, stored);\n this.#hasher?.add({\n watermark,\n pos: this.#pos,\n tag,\n change: stored,\n precommit: null,\n });\n this.#estimatedBytes += estimatedBytes;\n\n if (isSchemaChange(change)) {\n for (const op of this.#cookies.apply(change)) {\n this.#cookieMutations.push(op.op);\n }\n }\n }\n\n commit(\n watermark: string,\n json: string,\n writeTimeMs: number,\n storedChange = extractChangeSubstring(json, 'commit'),\n ): ChangeLogStreamTransactionStats {\n const precommit = this.#requireWatermark();\n assertInvariant(\n watermark === precommit,\n `change-log stream commit ${watermark} does not match begin ${precommit}`,\n );\n const change = storedChange;\n const estimatedBytes = estimateChangeLogStreamRowBytes(\n watermark,\n 'commit',\n change,\n precommit,\n true,\n );\n this.#insertCommit.run(\n watermark,\n ++this.#pos,\n estimatedBytes,\n change,\n precommit,\n writeTimeMs,\n );\n const hasher = this.#hasher;\n hasher?.add({\n watermark,\n pos: this.#pos,\n tag: 'commit',\n change,\n precommit,\n });\n const stats = {\n rows: this.#pos + 1,\n estimatedBytes: this.#estimatedBytes + estimatedBytes,\n hash: hasher?.digest(),\n cookieMutations: this.#cookieMutations,\n };\n // The log is durable at `watermark` from here on, and nothing that can\n // advance the watermark the stream would resume from has run yet. A crash in\n // that window leaves a transaction the resumed stream re-delivers, which\n // reconciliation truncates.\n this.#db.commit();\n this.#reset();\n return stats;\n }\n\n /**\n * Discards the open transaction, if any. Safe to call when none is open — the\n * caller rolls back on an interrupted change stream, which can arrive between\n * transactions as well as inside one.\n */\n rollback(): void {\n const {inTransaction} = this.#db.db;\n this.#reset();\n if (inTransaction) {\n this.#db.rollback();\n }\n }\n\n #reset(): void {\n this.#watermark = undefined;\n this.#pos = 0;\n this.#estimatedBytes = 0;\n this.#hasher = undefined;\n // A fresh array rather than a truncation: the committed stats hold a\n // reference to the previous one.\n this.#cookieMutations = [];\n }\n\n #requireWatermark(): string {\n assertInvariant(\n this.#watermark !== undefined,\n 'change-log stream message received outside of a transaction',\n );\n return this.#watermark;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"change-log-stream-writer.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/change-log-stream-writer.ts"],"sourcesContent":["import type {Statement} from '../../../../zqlite/src/db.ts';\nimport type {StatementRunner} from '../../db/statements.ts';\nimport {\n isSchemaChange,\n type DataOrSchemaChange,\n} from '../change-source/protocol/current/data.ts';\nimport {extractChangeSubstring} from '../change-streamer/change-log-codec.ts';\nimport {ChangeLogTransactionHasher} from '../change-streamer/change-log-transaction-hash.ts';\nimport {ChangeLogCookieWriter, type CookieOpTag} from './change-log-cookies.ts';\nimport {\n CHANGE_LOG_STREAM_TABLE,\n estimateChangeLogStreamRowBytes,\n} from './change-log-db.ts';\n\nexport type ChangeLogStreamTransactionStats = {\n readonly rows: number;\n readonly estimatedBytes: number;\n /** Present only for transactions selected for shadow-write validation. */\n readonly hash?: string | undefined;\n /**\n * The cookie mutations this transaction's schema changes folded in, in stream\n * order. Empty for the overwhelming majority of transactions.\n */\n readonly cookieMutations: readonly CookieOpTag[];\n};\n\nexport class ChangeLogStreamInvariantError extends Error {\n override readonly name = 'ChangeLogStreamInvariantError';\n}\n\nfunction assertInvariant(\n condition: unknown,\n message: string,\n): asserts condition {\n if (!condition) {\n throw new ChangeLogStreamInvariantError(message);\n }\n}\n\n/**\n * Appends the canonical downstream stream to the change-log database, which is\n * a separate file from the replica, and folds its schema changes into the log's\n * cookie jar (`change-log-cookies.ts`) as it goes.\n *\n * This class owns the change log's transaction, and its transaction alone. It is\n * driven from the change-streamer's stream loop, which commits it before\n * forwarding a transaction's `commit` message; see `SQLiteChangeLogWriter` in\n * `change-streamer/sqlite-change-log-writer.ts` for the ordering that makes a\n * hole unreachable, and for the fail-soft policy that wraps every call here.\n *\n * Because the cookies are written in that same transaction, they are atomic with\n * the head and {@link rollback} discards them with the rows they were folded\n * from. Neither needs any handling of its own.\n */\nexport class ChangeLogStreamWriter {\n readonly #db: StatementRunner;\n readonly #insertChange: Statement;\n readonly #insertCommit: Statement;\n readonly #cookies: ChangeLogCookieWriter;\n\n #watermark: string | undefined;\n #pos = 0;\n #estimatedBytes = 0;\n #hasher: ChangeLogTransactionHasher | undefined;\n #cookieMutations: CookieOpTag[] = [];\n\n constructor(db: StatementRunner) {\n this.#db = db;\n this.#insertChange = db.db.prepare(/*sql*/ `\n INSERT INTO \"${CHANGE_LOG_STREAM_TABLE}\"\n (\"watermark\", \"pos\", \"tag\", \"estimatedBytes\", \"change\")\n VALUES (?, ?, ?, ?, ?)\n `);\n this.#insertCommit = db.db.prepare(/*sql*/ `\n INSERT INTO \"${CHANGE_LOG_STREAM_TABLE}\"\n (\"watermark\", \"pos\", \"tag\", \"estimatedBytes\", \"change\", \"precommit\",\n \"writeTimeMs\")\n VALUES (?, ?, 'commit', ?, ?, ?, ?)\n `);\n this.#cookies = new ChangeLogCookieWriter(db.db);\n }\n\n begin(\n watermark: string,\n json: string,\n storedChange = extractChangeSubstring(json, 'begin'),\n hash = true,\n ): void {\n assertInvariant(\n this.#watermark === undefined,\n `change-log stream transaction already open at ${this.#watermark}`,\n );\n // This process is the sole writer of the change-log database, so the write\n // lock is taken up front rather than risking a failed upgrade from a\n // deferred transaction partway through the append.\n this.#db.beginImmediate();\n this.#watermark = watermark;\n this.#pos = 0;\n const change = storedChange;\n const estimatedBytes = estimateChangeLogStreamRowBytes(\n watermark,\n 'begin',\n change,\n );\n this.#insertChange.run(\n watermark,\n this.#pos,\n 'begin',\n estimatedBytes,\n change,\n );\n if (hash) {\n this.#hasher = new ChangeLogTransactionHasher();\n this.#hasher.add({\n watermark,\n pos: this.#pos,\n tag: 'begin',\n change,\n precommit: null,\n });\n }\n this.#estimatedBytes = estimatedBytes;\n }\n\n /**\n * Appends one data or schema change. The parsed message is taken rather than\n * just its tag because a schema change also mutates the cookie jar, and the\n * caller already holds it — so the fold costs no re-parse.\n *\n * The cookie statements run here, i.e. inside this transaction and in stream\n * order relative to the rows they were folded from. The Postgres change log\n * has to flush its buffered batch to get the same ordering\n * (`Storer.#processQueue()`); SQLite gets it from statement order.\n */\n append(\n json: string,\n change: DataOrSchemaChange,\n storedChange = extractChangeSubstring(json, change.tag),\n ): void {\n const watermark = this.#requireWatermark();\n const {tag} = change;\n const stored = storedChange;\n const estimatedBytes = estimateChangeLogStreamRowBytes(\n watermark,\n tag,\n stored,\n );\n this.#insertChange.run(watermark, ++this.#pos, tag, estimatedBytes, stored);\n this.#hasher?.add({\n watermark,\n pos: this.#pos,\n tag,\n change: stored,\n precommit: null,\n });\n this.#estimatedBytes += estimatedBytes;\n\n if (isSchemaChange(change)) {\n for (const op of this.#cookies.apply(change)) {\n this.#cookieMutations.push(op.op);\n }\n }\n }\n\n commit(\n watermark: string,\n json: string,\n writeTimeMs: number,\n storedChange = extractChangeSubstring(json, 'commit'),\n ): ChangeLogStreamTransactionStats {\n const precommit = this.#requireWatermark();\n assertInvariant(\n watermark === precommit,\n `change-log stream commit ${watermark} does not match begin ${precommit}`,\n );\n const change = storedChange;\n const estimatedBytes = estimateChangeLogStreamRowBytes(\n watermark,\n 'commit',\n change,\n precommit,\n true,\n );\n this.#insertCommit.run(\n watermark,\n ++this.#pos,\n estimatedBytes,\n change,\n precommit,\n writeTimeMs,\n );\n const hasher = this.#hasher;\n hasher?.add({\n watermark,\n pos: this.#pos,\n tag: 'commit',\n change,\n precommit,\n });\n const stats = {\n rows: this.#pos + 1,\n estimatedBytes: this.#estimatedBytes + estimatedBytes,\n hash: hasher?.digest(),\n cookieMutations: this.#cookieMutations,\n };\n // The log is durable at `watermark` from here on, and nothing that can\n // advance the watermark the stream would resume from has run yet. A crash in\n // that window leaves a transaction the resumed stream re-delivers, which\n // reconciliation truncates.\n this.#db.commit();\n this.#reset();\n return stats;\n }\n\n /**\n * Discards the open transaction, if any. Safe to call when none is open — the\n * caller rolls back on an interrupted change stream, which can arrive between\n * transactions as well as inside one.\n */\n rollback(): void {\n const {inTransaction} = this.#db.db;\n this.#reset();\n if (inTransaction) {\n this.#db.rollback();\n }\n }\n\n #reset(): void {\n this.#watermark = undefined;\n this.#pos = 0;\n this.#estimatedBytes = 0;\n this.#hasher = undefined;\n // A fresh array rather than a truncation: the committed stats hold a\n // reference to the previous one.\n this.#cookieMutations = [];\n }\n\n #requireWatermark(): string {\n assertInvariant(\n this.#watermark !== undefined,\n 'change-log stream message received outside of a transaction',\n );\n return this.#watermark;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-processor.js","names":["#db","#changeLog","#tableMetadata","#backfilling","#mode","#failService","#tableSpecs","#failure","#currentTx","#fail","#processMessage","#beginTransaction","#lc","#startMs","#version","#jsonFormat","#columnMetadata","#reloadTableSpecs","#tableSpec","#upsert","#getKey","#logSetOp","#logDeleteOp","#delete","#logTruncateOp","#logResetOp","#bumpVersions","#pos","#numChangeLogEntries","#schemaChanged","#trackBackfillProgress","#completedBackfill"],"sources":["../../../../../../zero-cache/src/services/replicator/change-processor.ts"],"sourcesContent":["import {constants as bufferConstants} from 'node:buffer';\nimport type {LogContext} from '@rocicorp/logger';\nimport {SqliteError} from '@rocicorp/zero-sqlite3';\nimport {AbortError} from '../../../../shared/src/abort-error.ts';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport {stringify} from '../../../../shared/src/bigint-json.ts';\nimport {deepEqual} from '../../../../shared/src/json.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {mapEntries} from '../../../../shared/src/objects.ts';\nimport type {DownloadStatus} from '../../../../zero-events/src/status.ts';\nimport {\n createLiteIndexStatement,\n createLiteTableStatement,\n liteColumnDef,\n} from '../../db/create.ts';\nimport {\n computeZqlSpecs,\n listTables,\n type LiteTableSpecWithReplicationStatus,\n} from '../../db/lite-tables.ts';\nimport {\n isArrayColumn,\n isEnumColumn,\n mapPostgresToLite,\n mapPostgresToLiteColumn,\n mapPostgresToLiteIndex,\n} from '../../db/pg-to-lite.ts';\nimport type {ColumnSpec} from '../../db/specs.ts';\nimport type {StatementRunner} from '../../db/statements.ts';\nimport type {LexiVersion} from '../../types/lexi-version.ts';\nimport {\n JSON_PARSED,\n liteRow,\n type JSONFormat,\n type LiteRow,\n type LiteRowKey,\n type LiteValueType,\n} from '../../types/lite.ts';\nimport {liteTableName} from '../../types/names.ts';\nimport {id} from '../../types/sql.ts';\nimport {\n acceptBackfill,\n type AcceptResult,\n} from '../change-source/protocol/backfill-progress.ts';\nimport type {\n BackfillCompleted,\n BackfillProgressMark,\n Change,\n ColumnAdd,\n ColumnDrop,\n ColumnUpdate,\n Identifier,\n IndexCreate,\n IndexDrop,\n MessageBackfill,\n MessageCommit,\n MessageDelete,\n MessageInsert,\n MessageRelation,\n MessageTruncate,\n MessageUpdate,\n TableCreate,\n TableDrop,\n TableRename,\n TableUpdateMetadata,\n} from '../change-source/protocol/current/data.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport type {ReplicatorMode} from './replicator.ts';\nimport {BackfillingTracker} from './schema/backfilling.ts';\nimport {ChangeLog, DEL_OP, SET_OP} from './schema/change-log.ts';\nimport {ColumnMetadataStore} from './schema/column-metadata.ts';\nimport {\n ZERO_VERSION_COLUMN_NAME,\n updateReplicationWatermark,\n} from './schema/replication-state.ts';\nimport {TableMetadataTracker} from './schema/table-metadata.ts';\n\nexport type ChangeProcessorMode = ReplicatorMode | 'initial-sync';\n\nconst BIND_VALUE_TOO_BIG_ERROR =\n 'The bound string, buffer, or bigint is too big';\nconst MAX_SQLITE_BIND_BYTES = Math.min(\n bufferConstants.MAX_LENGTH,\n bufferConstants.MAX_STRING_LENGTH,\n);\n\nexport type CommitResult = {\n watermark: string;\n completedBackfill: DownloadStatus | undefined;\n schemaUpdated: boolean;\n changeLogUpdated: boolean;\n /**\n * Millisecond epoch at which the transaction committed upstream, if the\n * ChangeSource reported one. Propagated to ViewSyncers as the origin\n * timestamp of the end-to-end serving lag measurement.\n */\n upstreamCommitTimeMs?: number | undefined;\n};\n\n/**\n * The ChangeProcessor partitions the stream of messages into transactions\n * by creating a {@link TransactionProcessor} when a transaction begins, and dispatching\n * messages to it until the commit is received.\n *\n * From https://www.postgresql.org/docs/current/protocol-logical-replication.html#PROTOCOL-LOGICAL-MESSAGES-FLOW :\n *\n * \"The logical replication protocol sends individual transactions one by one.\n * This means that all messages between a pair of Begin and Commit messages\n * belong to the same transaction.\"\n */\nexport class ChangeProcessor {\n readonly #db: StatementRunner;\n readonly #changeLog: ChangeLog;\n readonly #tableMetadata: TableMetadataTracker;\n readonly #backfilling: BackfillingTracker;\n readonly #mode: ChangeProcessorMode;\n readonly #failService: (lc: LogContext, err: unknown) => void;\n\n // The TransactionProcessor lazily loads table specs into this Map,\n // and reloads them after a schema change. It is cached here to avoid\n // reading them from the DB on every transaction.\n readonly #tableSpecs = new Map<string, LiteTableSpecWithReplicationStatus>();\n\n #currentTx: TransactionProcessor | null = null;\n\n #failure: Error | undefined;\n\n constructor(\n db: StatementRunner,\n mode: ChangeProcessorMode,\n failService: (lc: LogContext, err: unknown) => void,\n ) {\n this.#db = db;\n this.#changeLog = new ChangeLog(db.db);\n this.#tableMetadata = new TableMetadataTracker(db.db);\n this.#backfilling = new BackfillingTracker(db.db);\n this.#mode = mode;\n this.#failService = failService;\n }\n\n #fail(lc: LogContext, err: unknown) {\n if (!this.#failure) {\n let failureError = err;\n const rollbackErrors = [attempt(() => this.#currentTx?.abort(lc))].filter(\n e => e !== undefined,\n );\n\n if (rollbackErrors.length) {\n const combinedError = new Error(\n `Message processing failed and rollback also failed: operation error = ${String(err)}; rollback error = ${rollbackErrors.map(String).join('; ')}`,\n );\n combinedError.cause = err;\n failureError = combinedError;\n }\n\n this.#failure = ensureError(failureError);\n\n if (!(this.#failure instanceof AbortError)) {\n // Propagate the failure up to the service.\n lc.error?.('Message Processing failed:', this.#failure);\n this.#failService(lc, this.#failure);\n }\n }\n }\n\n abort(lc: LogContext) {\n this.#fail(lc, new AbortError());\n }\n\n /** @return If a transaction was committed. */\n processMessage(\n lc: LogContext,\n downstream: ChangeStreamData,\n ): CommitResult | null {\n const [type, message] = downstream;\n if (this.#failure) {\n lc.debug?.(`Dropping ${message.tag}`);\n return null;\n }\n try {\n const watermark =\n type === 'begin'\n ? downstream[2].commitWatermark\n : type === 'commit'\n ? downstream[2].watermark\n : undefined;\n return this.#processMessage(lc, message, watermark);\n } catch (e) {\n this.#fail(lc, e);\n }\n return null;\n }\n\n #beginTransaction(\n lc: LogContext,\n commitVersion: string,\n jsonFormat: JSONFormat,\n ): TransactionProcessor {\n const start = Date.now();\n\n // litestream can technically hold the lock for an arbitrary amount of time\n // when checkpointing a large commit. Crashing on the busy-timeout in this\n // scenario will either produce a corrupt backup or otherwise prevent\n // replication from proceeding.\n //\n // Instead, retry the lock acquisition indefinitely. If this masks\n // an unknown deadlock situation, manual intervention will be necessary.\n for (let i = 0; ; i++) {\n try {\n return new TransactionProcessor(\n lc,\n this.#db,\n this.#mode,\n this.#changeLog,\n this.#tableMetadata,\n this.#backfilling,\n this.#tableSpecs,\n commitVersion,\n jsonFormat,\n );\n } catch (e) {\n if (e instanceof SqliteError && e.code === 'SQLITE_BUSY') {\n lc.warn?.(\n `SQLITE_BUSY for ${Date.now() - start} ms (attempt ${i + 1}). ` +\n `This is only expected if litestream is performing a large ` +\n `checkpoint.`,\n e,\n );\n continue;\n }\n throw e;\n }\n }\n }\n\n /** @return If a transaction was committed. */\n #processMessage(\n lc: LogContext,\n msg: Change,\n watermark: string | undefined,\n ): CommitResult | null {\n if (msg.tag === 'begin') {\n if (this.#currentTx) {\n throw new Error(`Already in a transaction ${stringify(msg)}`);\n }\n this.#currentTx = this.#beginTransaction(\n lc,\n must(watermark),\n msg.json ?? JSON_PARSED,\n );\n return null;\n }\n\n // For non-begin messages, there should be a #currentTx set.\n const tx = this.#currentTx;\n if (!tx) {\n throw new Error(\n `Received message outside of transaction: ${stringify(msg)}`,\n );\n }\n\n if (msg.tag === 'commit') {\n assert(watermark, 'watermark is required for commit messages');\n const result = tx.processCommit(msg, watermark);\n // Clear only after a successful commit so #fail can roll back a commit\n // path that throws before SQLite has committed.\n this.#currentTx = null;\n return result;\n }\n\n if (msg.tag === 'rollback') {\n tx.abort(lc);\n this.#currentTx = null;\n return null;\n }\n\n switch (msg.tag) {\n case 'insert':\n tx.processInsert(msg);\n break;\n case 'update':\n tx.processUpdate(msg);\n break;\n case 'delete':\n tx.processDelete(msg);\n break;\n case 'truncate':\n tx.processTruncate(msg);\n break;\n case 'create-table':\n tx.processCreateTable(msg);\n break;\n case 'rename-table':\n tx.processRenameTable(msg);\n break;\n case 'update-table-metadata':\n tx.processTableMetadata(msg);\n break;\n case 'add-column':\n tx.processAddColumn(msg);\n break;\n case 'update-column':\n tx.processUpdateColumn(msg);\n break;\n case 'drop-column':\n tx.processDropColumn(msg);\n break;\n case 'drop-table':\n tx.processDropTable(msg);\n break;\n case 'create-index':\n tx.processCreateIndex(msg);\n break;\n case 'drop-index':\n tx.processDropIndex(msg);\n break;\n case 'backfill':\n tx.processBackfill(msg);\n break;\n case 'backfill-completed':\n tx.processBackfillCompleted(msg);\n break;\n default:\n unreachable(msg);\n }\n\n return null;\n }\n}\n\n/**\n * The {@link TransactionProcessor} handles the sequence of messages from\n * upstream, from `BEGIN` to `COMMIT` and executes the corresponding mutations\n * on the {@link postgres.TransactionSql} on the replica.\n *\n * When applying row contents to the replica, the `_0_version` column is added / updated,\n * and a corresponding entry in the `ChangeLog` is added. The version value is derived\n * from the watermark of the preceding transaction (stored as the `nextStateVersion` in the\n * `ReplicationState` table).\n *\n * Side note: For non-streaming Postgres transactions, the commitEndLsn (and thus\n * commit watermark) is available in the `begin` message, so it could theoretically\n * be used for the row version of changes within the transaction. However, the\n * commitEndLsn is not available in the streaming (in-progress) transaction\n * protocol, and may not be available for CDC streams of other upstream types.\n * Therefore, the zero replication protocol is designed to not require the commit\n * watermark when a transaction begins.\n *\n * Also of interest is the fact that all INSERT Messages are logically applied as\n * UPSERTs. See {@link processInsert} for the underlying motivation.\n */\nclass TransactionProcessor {\n readonly #lc: LogContext;\n readonly #startMs: number;\n readonly #db: StatementRunner;\n readonly #mode: ChangeProcessorMode;\n readonly #version: LexiVersion;\n readonly #changeLog: ChangeLog;\n readonly #tableMetadata: TableMetadataTracker;\n readonly #backfilling: BackfillingTracker;\n readonly #tableSpecs: Map<string, LiteTableSpecWithReplicationStatus>;\n readonly #jsonFormat: JSONFormat;\n readonly #columnMetadata: ColumnMetadataStore;\n\n #pos = 0;\n #schemaChanged = false;\n #numChangeLogEntries = 0;\n\n constructor(\n lc: LogContext,\n db: StatementRunner,\n mode: ChangeProcessorMode,\n changeLog: ChangeLog,\n tableMetadata: TableMetadataTracker,\n backfilling: BackfillingTracker,\n tableSpecs: Map<string, LiteTableSpecWithReplicationStatus>,\n commitVersion: LexiVersion,\n jsonFormat: JSONFormat,\n ) {\n this.#startMs = Date.now();\n this.#mode = mode;\n this.#jsonFormat = jsonFormat;\n\n switch (mode) {\n case 'serving':\n // This is the only transaction that commits to the serving replica.\n // Snapshotters use BEGIN CONCURRENT for private changes that are\n // always rolled back, while BEGIN IMMEDIATE lets this writer spill\n // dirty pages during large transactions.\n db.beginImmediate();\n break;\n case 'backup':\n // For the backup-replicator (i.e. replication-manager), there are no View Syncers\n // and thus BEGIN CONCURRENT is not necessary. In fact, BEGIN CONCURRENT can cause\n // deadlocks with forced wal-checkpoints (which `litestream replicate` performs),\n // so it is important to use vanilla transactions in this configuration.\n db.beginImmediate();\n break;\n case 'initial-sync':\n // When the ChangeProcessor is used for initial-sync, the calling code\n // handles the transaction boundaries.\n break;\n default:\n unreachable();\n }\n this.#db = db;\n this.#version = commitVersion;\n this.#lc = lc.withContext('version', commitVersion);\n this.#changeLog = changeLog;\n this.#tableMetadata = tableMetadata;\n this.#backfilling = backfilling;\n this.#tableSpecs = tableSpecs;\n // The column_metadata table is guaranteed to exist since the\n // replica-schema.ts migration to v8.\n this.#columnMetadata = must(ColumnMetadataStore.getInstance(db.db));\n\n if (this.#tableSpecs.size === 0) {\n this.#reloadTableSpecs();\n }\n }\n\n #reloadTableSpecs() {\n this.#tableSpecs.clear();\n // zqlSpecs include the primary key derived from unique indexes\n const zqlSpecs = computeZqlSpecs(this.#lc, this.#db.db, {\n includeBackfillingColumns: true,\n });\n for (let spec of listTables(this.#db.db)) {\n if (!spec.primaryKey) {\n spec = {\n ...spec,\n primaryKey: [\n ...(zqlSpecs.get(spec.name)?.tableSpec.primaryKey ?? []),\n ],\n };\n }\n this.#tableSpecs.set(spec.name, spec);\n }\n }\n\n #tableSpec(name: string) {\n return must(this.#tableSpecs.get(name), `Unknown table ${name}`);\n }\n\n #getKey(\n {row, numCols}: {row: LiteRow; numCols: number},\n {relation}: {relation: MessageRelation},\n ): LiteRowKey {\n const keyColumns =\n relation.rowKey.type !== 'full'\n ? relation.rowKey.columns // already a suitable key\n : this.#tableSpec(liteTableName(relation)).primaryKey;\n if (!keyColumns?.length) {\n throw new Error(\n `Cannot replicate table \"${relation.name}\" without a PRIMARY KEY or UNIQUE INDEX`,\n );\n }\n // For the common case (replica identity default), the row is already the\n // key for deletes and updates, in which case a new object can be avoided.\n if (numCols === keyColumns.length) {\n return row;\n }\n const key: Record<string, LiteValueType> = {};\n for (const col of keyColumns) {\n key[col] = row[col];\n }\n return key;\n }\n\n processInsert(insert: MessageInsert) {\n const table = liteTableName(insert.relation);\n const tableSpec = this.#tableSpec(table);\n const newRow = liteRow(insert.new, tableSpec, this.#jsonFormat);\n\n this.#upsert(table, {\n ...newRow.row,\n [ZERO_VERSION_COLUMN_NAME]: this.#version,\n });\n\n if (insert.relation.rowKey.columns.length === 0) {\n // INSERTs can be replicated for rows without a PRIMARY KEY or a\n // UNIQUE INDEX. These are written to the replica but not recorded\n // in the changeLog, because these rows cannot participate in IVM.\n //\n // (Once the table schema has been corrected to include a key, the\n // associated schema change will reset pipelines and data can be\n // loaded via hydration.)\n return;\n }\n const key = this.#getKey(newRow, insert);\n this.#logSetOp(table, key, getBackfilledColumns(newRow.row, tableSpec));\n }\n\n #upsert(table: string, row: LiteRow) {\n const columns = Object.keys(row).map(c => id(c));\n this.#db.run(\n `\n INSERT OR REPLACE INTO ${id(table)} (${columns.join(',')})\n VALUES (${Array.from({length: columns.length}).fill('?').join(',')})\n `,\n Object.values(row),\n );\n }\n\n // Updates by default are applied as UPDATE commands to support partial\n // row specifications from the change source. In particular, this is needed\n // to handle updates for which unchanged TOASTed values are not sent:\n //\n // https://www.postgresql.org/docs/current/protocol-logicalrep-message-formats.html#PROTOCOL-LOGICALREP-MESSAGE-FORMATS-TUPLEDATA\n //\n // However, in certain cases an UPDATE may be received for a row that\n // was not initially synced, such as when, an existing table is added\n // to the app's publication.\n //\n // In order to facilitate \"resumptive\" replication, the logic falls back to\n // an INSERT if the update did not change any rows.\n processUpdate(update: MessageUpdate) {\n const table = liteTableName(update.relation);\n const tableSpec = this.#tableSpec(table);\n const newRow = liteRow(update.new, tableSpec, this.#jsonFormat);\n const row = {...newRow.row, [ZERO_VERSION_COLUMN_NAME]: this.#version};\n\n // update.key is set with the old values if the key has changed.\n const oldKey = update.key\n ? this.#getKey(\n liteRow(update.key, this.#tableSpec(table), this.#jsonFormat),\n update,\n )\n : null;\n const newKey = this.#getKey(newRow, update);\n\n if (oldKey) {\n this.#logDeleteOp(table, oldKey, tableSpec.backfilling);\n }\n this.#logSetOp(table, newKey, getBackfilledColumns(newRow.row, tableSpec));\n\n const currKey = oldKey ?? newKey;\n const conds = Object.keys(currKey).map(col => `${id(col)}=?`);\n const setExprs = Object.keys(row).map(col => `${id(col)}=?`);\n\n try {\n const {changes} = this.#db.run(\n `\n UPDATE ${id(table)}\n SET ${setExprs.join(',')}\n WHERE ${conds.join(' AND ')}\n `,\n [...Object.values(row), ...Object.values(currKey)],\n );\n\n // If the UPDATE did not affect any rows, perform an UPSERT of the\n // new row for resumptive replication.\n if (changes === 0) {\n this.#upsert(table, row);\n }\n } catch (e) {\n if (\n !(e instanceof RangeError) ||\n e.message !== BIND_VALUE_TOO_BIG_ERROR\n ) {\n throw e;\n }\n\n const binding = [...Object.entries(row), ...Object.entries(currKey)]\n .map(\n ([column, value]) =>\n [column, oversizedValueDescription(value)] as const,\n )\n .find(([, description]) => description);\n const relationOid =\n 'relationOid' in update.relation &&\n typeof update.relation.relationOid === 'number'\n ? ` relationOid=${update.relation.relationOid}`\n : '';\n const error = new Error(\n `Oversized SQLite update binding: tx=${this.#version}${relationOid} table=${update.relation.schema}.${update.relation.name} column=${binding?.[0] ?? 'unknown'}${binding?.[1] ? ` ${binding[1]}` : ''}`,\n {cause: e},\n );\n error.name = 'OversizedUpdateBindingError';\n throw error;\n }\n }\n\n processDelete(del: MessageDelete) {\n const table = liteTableName(del.relation);\n const tableSpec = this.#tableSpec(table);\n const rowKey = this.#getKey(\n liteRow(del.key, tableSpec, this.#jsonFormat),\n del,\n );\n\n this.#delete(table, rowKey);\n this.#logDeleteOp(table, rowKey, tableSpec.backfilling);\n }\n\n #delete(table: string, rowKey: LiteRowKey) {\n const conds = Object.keys(rowKey).map(col => `${id(col)}=?`);\n this.#db.run(\n `DELETE FROM ${id(table)} WHERE ${conds.join(' AND ')}`,\n Object.values(rowKey),\n );\n }\n\n processTruncate(truncate: MessageTruncate) {\n for (const relation of truncate.relations) {\n const table = liteTableName(relation);\n // Update replica data.\n this.#db.run(`DELETE FROM ${id(table)}`);\n\n // Update change log.\n this.#logTruncateOp(table);\n }\n }\n\n processCreateTable(create: TableCreate) {\n if (create.metadata) {\n this.#tableMetadata.setUpstreamMetadata(create.spec, create.metadata);\n }\n this.#backfilling.apply(create);\n const table = mapPostgresToLite(create.spec);\n this.#db.db.exec(createLiteTableStatement(table));\n\n // Write to metadata table\n for (const [colName, colSpec] of Object.entries(create.spec.columns)) {\n this.#columnMetadata.insert(\n table.name,\n colName,\n colSpec,\n create.backfill?.[colName],\n );\n }\n\n this.#logResetOp(table.name);\n this.#lc.info?.(create.tag, table.name);\n }\n\n processTableMetadata(msg: TableUpdateMetadata) {\n this.#tableMetadata.setUpstreamMetadata(msg.table, msg.new);\n // Inert today: the fold's only op for this tag is `upsert-metadata`, which\n // the line above is the replica's interpreter of. Called anyway so that\n // every site that hands a schema change to one cookie tracker hands it to\n // both -- an exception here would be a schema change the shared fold has an\n // opinion about and this store never sees, which is the drift the fold\n // exists to make impossible.\n this.#backfilling.apply(msg);\n }\n\n processRenameTable(rename: TableRename) {\n this.#tableMetadata.rename(rename.old, rename.new);\n this.#backfilling.apply(rename);\n\n const oldName = liteTableName(rename.old);\n const newName = liteTableName(rename.new);\n this.#db.db.exec(`ALTER TABLE ${id(oldName)} RENAME TO ${id(newName)}`);\n\n // Rename in metadata table\n this.#columnMetadata.renameTable(oldName, newName);\n\n this.#bumpVersions(rename.new);\n this.#logResetOp(oldName);\n this.#lc.info?.(rename.tag, oldName, newName);\n }\n\n processAddColumn(msg: ColumnAdd) {\n if (msg.tableMetadata) {\n this.#tableMetadata.setUpstreamMetadata(msg.table, msg.tableMetadata);\n }\n this.#backfilling.apply(msg);\n const table = liteTableName(msg.table);\n const {name} = msg.column;\n const spec = mapPostgresToLiteColumn(table, msg.column);\n this.#db.db.exec(\n `ALTER TABLE ${id(table)} ADD ${id(name)} ${liteColumnDef(spec)}`,\n );\n\n // Write to metadata table\n this.#columnMetadata.insert(table, name, msg.column.spec, msg.backfill);\n\n if (msg.backfill) {\n this.#reloadTableSpecs();\n } else {\n // Make the new column visible immediately if it's not being backfilled.\n // Otherwise, the version bump will happen with the backfill is complete.\n this.#bumpVersions(msg.table);\n }\n this.#lc.info?.(msg.tag, table, msg.column);\n }\n\n processUpdateColumn(msg: ColumnUpdate) {\n // A no-op unless this is a rename, which is the only part of a column\n // update that moves the backfill cookie.\n this.#backfilling.apply(msg);\n\n const table = liteTableName(msg.table);\n let oldName = msg.old.name;\n const newName = msg.new.name;\n\n const storageTypesDiffer = differentStorageTypes(\n msg.old.spec,\n msg.new.spec,\n );\n\n // If neither the column name nor the SQLite data type changes, only the\n // upstream metadata needs to be updated. This includes changes such as a\n // varchar character limit and nullability, which SQLite does not enforce\n // but a freshly built replica still records.\n if (oldName === newName && !storageTypesDiffer) {\n this.#columnMetadata.update(\n table,\n msg.old.name,\n msg.new.name,\n msg.new.spec,\n );\n if (Boolean(msg.old.spec.notNull) !== Boolean(msg.new.spec.notNull)) {\n this.#bumpVersions(msg.table);\n }\n this.#lc.info?.(msg.tag, 'updated metadata only', msg.old, msg.new);\n return;\n }\n // Storage type changes require a new column so that SQLite uses the new\n // representation for existing and future values.\n if (storageTypesDiffer) {\n // update-column can ignore defaults because it does not change the values\n // in existing rows.\n //\n // https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-DESC-SET-DROP-DEFAULT\n //\n // \"The new default value will only apply in subsequent INSERT or UPDATE\n // commands; it does not cause rows already in the table to change.\"\n //\n // This allows support for _changing_ column defaults to any expression,\n // since it does not affect what the replica needs to do.\n const newLiteSpec = mapPostgresToLiteColumn(\n table,\n msg.new,\n 'ignore-default',\n );\n if (oldName !== newName) {\n // Rename the column in place first so that SQLite rewrites every\n // reference to it in the table's index definitions, including the\n // WHERE clauses of partial indexes.\n this.#db.db.exec(\n `ALTER TABLE ${id(table)} RENAME ${id(oldName)} TO ${id(newName)}`,\n );\n oldName = newName;\n }\n const tableSpec = must(\n listTables(this.#db.db, false, false).find(\n tableSpec => tableSpec.name === table,\n ),\n );\n // Recreating the table drops its indexes, so capture their DDL to\n // replay afterwards. Auto-indexes (PRIMARY KEY) have no DDL and are\n // recreated by the CREATE TABLE statement itself.\n const indexDDL = this.#db.db\n .prepare(\n `SELECT sql FROM sqlite_master\n WHERE type = 'index' AND tbl_name = ? AND sql IS NOT NULL\n ORDER BY rowid`,\n )\n .all<{sql: string}>(table)\n .map(({sql}) => sql);\n const tmpTable = `tmp.${table}`;\n const columns = Object.keys(tableSpec.columns);\n const stmts = [\n createLiteTableStatement({\n ...tableSpec,\n name: tmpTable,\n columns: mapEntries(tableSpec.columns, (column, spec) => [\n column,\n column === newName ? {...newLiteSpec, pos: spec.pos} : spec,\n ]),\n }),\n `INSERT INTO ${id(tmpTable)} (${columns.map(id).join(',')})\n SELECT ${columns.map(id).join(',')} FROM ${id(table)};`,\n `DROP TABLE ${id(table)};`,\n `ALTER TABLE ${id(tmpTable)} RENAME TO ${id(table)};`,\n ...indexDDL.map(sql => `${sql};`),\n ];\n this.#db.db.exec(stmts.join(''));\n }\n if (oldName !== newName) {\n this.#db.db.exec(\n `ALTER TABLE ${id(table)} RENAME ${id(oldName)} TO ${id(newName)}`,\n );\n }\n\n // Update metadata table\n this.#columnMetadata.update(\n table,\n msg.old.name,\n msg.new.name,\n msg.new.spec,\n );\n\n this.#bumpVersions(msg.table);\n this.#lc.info?.(msg.tag, table, msg.new);\n }\n\n processDropColumn(msg: ColumnDrop) {\n const table = liteTableName(msg.table);\n const {column} = msg;\n this.#db.db.exec(`ALTER TABLE ${id(table)} DROP ${id(column)}`);\n\n // Delete from metadata table\n this.#columnMetadata.deleteColumn(table, column);\n this.#backfilling.apply(msg);\n\n this.#bumpVersions(msg.table);\n this.#lc.info?.(msg.tag, table, column);\n }\n\n processDropTable(drop: TableDrop) {\n this.#tableMetadata.drop(drop.id);\n this.#backfilling.apply(drop);\n\n const name = liteTableName(drop.id);\n this.#db.db.exec(`DROP TABLE IF EXISTS ${id(name)}`);\n\n // Delete from metadata table\n this.#columnMetadata.deleteTable(name);\n\n this.#logResetOp(name);\n this.#lc.info?.(drop.tag, name);\n }\n\n processCreateIndex(create: IndexCreate) {\n const index = mapPostgresToLiteIndex(create.spec);\n this.#db.db.exec(createLiteIndexStatement(index));\n\n // indexes affect tables visibility (e.g. sync-ability is gated on\n // having a unique index), so reset pipelines to refresh table schemas.\n this.#logResetOp(index.tableName);\n this.#lc.info?.(create.tag, index.name);\n }\n\n processDropIndex(drop: IndexDrop) {\n const name = liteTableName(drop.id);\n const indexExisted = this.#db.db\n .prepare(\n `SELECT tbl_name as tableName FROM sqlite_master WHERE type = 'index' AND name = ?`,\n )\n .get(name) as {tableName: string} | undefined;\n\n this.#db.db.exec(`DROP INDEX IF EXISTS ${id(name)}`);\n\n if (indexExisted) {\n // indexes affect tables visibility (e.g. sync-ability is gated on\n // having a unique index), so reset pipelines to refresh table schemas.\n this.#logResetOp(indexExisted.tableName);\n }\n this.#lc.info?.(drop.tag, name);\n }\n\n #bumpVersions(table: Identifier) {\n this.#tableMetadata.setMinRowVersion(table, this.#version);\n this.#logResetOp(liteTableName(table));\n }\n\n /**\n * @param backfilledColumns `backfilling` columns for which values were set\n */\n #logSetOp(\n table: string,\n key: LiteRowKey,\n backfilledColumns: string[] | undefined,\n ) {\n // The \"serving\" replicator always writes to the change-log (for IVM).\n // The \"backup\" replicator only needs to write to the change log\n // when writing columns that are being backfilled.\n if (this.#mode === 'serving' || backfilledColumns !== undefined) {\n this.#changeLog.logSetOp(\n this.#version,\n this.#pos++,\n table,\n key,\n backfilledColumns,\n );\n this.#numChangeLogEntries++;\n }\n }\n\n #logDeleteOp(table: string, key: LiteRowKey, backfilling?: string[]) {\n // The \"serving\" replicator always writes to the change-log (for IVM).\n // The \"backup\" replicator only needs to write to the change log\n // when writing columns that are being backfilled.\n if (this.#mode === 'serving' || backfilling?.length) {\n this.#changeLog.logDeleteOp(this.#version, this.#pos++, table, key);\n this.#numChangeLogEntries++;\n }\n }\n\n #logTruncateOp(table: string) {\n if (this.#mode === 'serving') {\n this.#changeLog.logTruncateOp(this.#version, table);\n this.#numChangeLogEntries++;\n }\n }\n\n #logResetOp(table: string) {\n this.#schemaChanged = true;\n if (this.#mode === 'serving') {\n this.#changeLog.logResetOp(this.#version, table);\n this.#numChangeLogEntries++;\n }\n this.#reloadTableSpecs();\n }\n\n processBackfill({\n relation,\n watermark,\n columns,\n rowValues,\n progressMarks,\n }: MessageBackfill) {\n const tableName = liteTableName(relation);\n const tableSpec = must(this.#tableSpecs.get(tableName));\n const rowKeyCols = relation.rowKey.columns;\n const cols = [...rowKeyCols, ...columns];\n\n // Only columns that are still being backfilled on _this_ replica are\n // written, and of those, only the columns for which this message is\n // contiguous with the data received so far (whose progress is advanced).\n //\n // * Once a column has been published (backfill completed), its values\n // strictly follow the replication timeline, so a subsequent backfill must\n // never overwrite them. This guards against a redundant backfill, e.g.\n // when the view-syncer (re)connects to a replication-manager that is\n // (re)running a backfill this replica already completed.\n // * Data for non-contiguous (ignored) columns is necessarily re-delivered\n // (by a resumed or from-scratch run) before the column can be published,\n // so writing it would be wasted I/O.\n const {accepted, ignored} = this.#trackBackfillProgress(\n relation,\n cols,\n own => acceptBackfill(own, progressMarks),\n );\n if (accepted.length === 0) {\n this.#lc.debug?.(\n ignored.length\n ? `skipping backfill of ${rowValues.length} rows into ` +\n `${tableName}: not contiguous with the progress of columns ` +\n `[${ignored.join(',')}]`\n : `skipping redundant backfill of ${rowValues.length} rows into ` +\n `${tableName} (all columns already published)`,\n progressMarks,\n );\n return;\n }\n if (ignored.length) {\n this.#lc.debug?.(\n `backfill of ${tableName} is not contiguous with the progress ` +\n `of columns [${ignored.join(',')}]`,\n progressMarks,\n );\n }\n const acceptedSet = new Set(accepted);\n\n // Common parts of the INSERT sql statement.\n const insertColsStr = [...cols, ZERO_VERSION_COLUMN_NAME].map(id).join(',');\n const qMarks = Array.from({length: cols.length + 1})\n .fill('?')\n .join(',');\n const rowKeyColsStr = rowKeyCols.map(id).join(',');\n\n let backfilled = 0;\n let skipped = 0;\n for (const v of rowValues) {\n const row = liteRow(\n Object.fromEntries(cols.map((c, i) => [c, v[i]])),\n tableSpec,\n this.#jsonFormat,\n );\n const rowKey = this.#getKey(row, {relation});\n const rowOp = this.#changeLog.getLatestRowOp(tableName, rowKey);\n if (rowOp?.op === DEL_OP && rowOp.stateVersion > watermark) {\n skipped++;\n continue; // the row was deleted after the backfill snapshot\n }\n // Restrict the DO UPDATE columns to those still being backfilled. Never\n // update an already backfilled (i.e. published) column.\n const updates = (\n rowOp?.op === SET_OP\n ? cols.filter(\n c => (rowOp.backfillingColumnVersions[c] ?? '') <= watermark,\n )\n : cols\n ).filter(c => acceptedSet.has(c));\n if (updates.length === 0) {\n // row already has newer values for all accepted backfilling columns.\n skipped++;\n continue;\n }\n const updateStmts = updates.map(col => `${id(col)}=excluded.${id(col)}`);\n this.#db.run(\n /*sql*/ `\n INSERT INTO ${id(tableName)} (${insertColsStr}) VALUES (${qMarks})\n ON CONFLICT (${rowKeyColsStr})\n DO UPDATE SET ${updateStmts.join(',')};\n `,\n ...Object.values(row.row),\n watermark, // the _0_version for new rows (i.e. table backfill)\n );\n backfilled++;\n }\n\n this.#lc.debug?.(\n `backfilled ${backfilled} rows (skipped ${skipped}) into ${tableName}`,\n );\n }\n\n #completedBackfill: DownloadStatus | undefined;\n\n /**\n * Applies `accept` to the tracked progress of each of the `cols` that are\n * still backfilling on this replica, updating the progress of the accepted\n * ones. Columns that are not backfilling (e.g. already published) are\n * neither accepted nor ignored.\n */\n #trackBackfillProgress(\n relation: Identifier,\n cols: readonly string[],\n accept: (progress: BackfillProgressMark | undefined) => AcceptResult,\n ): {accepted: string[]; ignored: string[]} {\n const tracked = this.#backfilling.getProgress(relation);\n const accepted: string[] = [];\n const ignored: string[] = [];\n for (const col of cols) {\n if (!tracked.has(col)) {\n continue; // not backfilling\n }\n const progress = tracked.get(col);\n const result = accept(progress);\n if (!result.accept) {\n ignored.push(col);\n continue;\n }\n accepted.push(col);\n if (!deepEqual(result.progress, progress)) {\n this.#backfilling.setProgress(relation, col, result.progress);\n }\n }\n return {accepted, ignored};\n }\n\n processBackfillCompleted(msg: BackfillCompleted) {\n const {relation, columns, status, progressMarks} = msg;\n const tableName = liteTableName(relation);\n const rowKeyCols = relation.rowKey.columns;\n\n // Only complete the columns whose backfill progress is contiguous with\n // the completed backfill. The others remain backfilling (and will be\n // re-requested by the change-streamer).\n const {accepted, ignored} = this.#trackBackfillProgress(\n relation,\n [...rowKeyCols, ...columns],\n own =>\n acceptBackfill(\n own,\n progressMarks ? {previous: progressMarks.previous} : undefined,\n ),\n );\n if (ignored.length) {\n this.#lc.info?.(\n `ignoring backfill-completed of ${tableName} for columns ` +\n `[${ignored.join(',')}], which have not received all of the data`,\n progressMarks,\n );\n }\n\n // If none of the columns are still backfilling on this replica, they were\n // already published (e.g. a redundant backfill from a replication-manager\n // re-running a backfill this replica already completed). Bumping versions\n // again would spuriously reset pipelines and re-report completion, so treat\n // the completion as a no-op.\n if (accepted.length === 0) {\n if (ignored.length === 0) {\n this.#lc.debug?.(\n `skipping redundant backfill-completed for ${tableName} ` +\n `(all columns already published)`,\n );\n }\n return;\n }\n const ignoredSet = new Set(ignored);\n const cols = [\n ...rowKeyCols.filter(c => !ignoredSet.has(c)),\n ...columns.filter(c => !ignoredSet.has(c)),\n ];\n\n const columnMetadata = must(ColumnMetadataStore.getInstance(this.#db.db));\n for (const col of cols) {\n columnMetadata.clearBackfilling(tableName, col);\n }\n this.#backfilling.complete(relation, cols);\n // Given that new columns are being exposed for every row in the table, bump the\n // row version for all rows.\n this.#bumpVersions(relation);\n if (status) {\n this.#completedBackfill = {table: tableName, columns: cols, ...status};\n }\n this.#lc.info?.(`finished backfilling ${tableName}`);\n\n // Note that there is no need to clear the backfillingColumnVersions values\n // in the changeLog. It could theoretically be done for clarity but:\n // (1) it could be non-trivial in terms of latency introduced and\n // (2) the data must be preserved if _other_ columns are in the process\n // of being backfilled\n //\n // Thus, for speed and simplicity, the values are left as is. (Note that\n // subsequent replicated changes to those rows will clear the values if\n // no backfills are in progress).\n }\n\n processCommit(commit: MessageCommit, watermark: string): CommitResult {\n if (watermark !== this.#version) {\n throw new Error(\n `'commit' version ${watermark} does not match 'begin' version ${\n this.#version\n }: ${stringify(commit)}`,\n );\n }\n updateReplicationWatermark(this.#db, watermark);\n\n if (this.#schemaChanged) {\n const start = Date.now();\n this.#db.db.pragma('optimize');\n this.#lc.info?.(\n `PRAGMA optimized after schema change (${Date.now() - start} ms)`,\n );\n }\n\n if (this.#mode !== 'initial-sync') {\n this.#db.commit();\n }\n\n const elapsedMs = Date.now() - this.#startMs;\n this.#lc.debug?.(`Committed tx@${this.#version} (${elapsedMs} ms)`);\n\n return {\n watermark,\n completedBackfill: this.#completedBackfill,\n schemaUpdated: this.#schemaChanged,\n changeLogUpdated: this.#numChangeLogEntries > 0,\n upstreamCommitTimeMs: commit.commitTimeMs,\n };\n }\n\n abort(lc: LogContext) {\n lc.info?.(`aborting transaction ${this.#version}`);\n this.#db.rollback();\n // A schema change in the transaction reloaded the table specs, which now\n // describe the schema that the rollback undid. Clearing them makes the\n // next transaction reload them from the database.\n this.#tableSpecs.clear();\n }\n}\n\nfunction differentStorageTypes(\n oldSpec: ColumnSpec,\n newSpec: ColumnSpec,\n): boolean {\n return (\n oldSpec.dataType !== newSpec.dataType ||\n isEnumColumn(oldSpec) !== isEnumColumn(newSpec) ||\n isArrayColumn(oldSpec) !== isArrayColumn(newSpec)\n );\n}\n\nfunction getBackfilledColumns(\n row: LiteRow,\n {backfilling}: LiteTableSpecWithReplicationStatus,\n): string[] | undefined {\n if (!backfilling?.length) {\n return undefined; // common case\n }\n return backfilling.filter(col => col in row);\n}\n\nfunction oversizedValueDescription(value: LiteValueType): string | undefined {\n if (typeof value === 'bigint') {\n return BigInt.asIntN(64, value) !== value\n ? 'valueType=bigint fitsInt64=false'\n : undefined;\n }\n\n const sizeBytes =\n typeof value === 'string'\n ? Buffer.byteLength(value)\n : value instanceof Uint8Array\n ? value.byteLength\n : undefined;\n if (sizeBytes !== undefined && sizeBytes > MAX_SQLITE_BIND_BYTES) {\n const valueType = typeof value === 'string' ? 'string' : 'buffer';\n return `valueType=${valueType} sizeBytes=${sizeBytes} limitBytes=${MAX_SQLITE_BIND_BYTES}`;\n }\n\n return undefined;\n}\n\n/** Runs `fn`, returning its error rather than throwing it. */\nfunction attempt(fn: () => void): unknown {\n try {\n fn();\n return undefined;\n } catch (e) {\n return e ?? new Error('rollback failed');\n }\n}\n\nfunction ensureError(err: unknown): Error {\n if (err instanceof Error) {\n return err;\n }\n const error = new Error();\n error.cause = err;\n return error;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"change-processor.js","names":["bufferConstants"],"sources":["../../../../../../zero-cache/src/services/replicator/change-processor.ts"],"sourcesContent":["import {constants as bufferConstants} from 'node:buffer';\nimport type {LogContext} from '@rocicorp/logger';\nimport {SqliteError} from '@rocicorp/zero-sqlite3';\nimport {AbortError} from '../../../../shared/src/abort-error.ts';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport {stringify} from '../../../../shared/src/bigint-json.ts';\nimport {deepEqual} from '../../../../shared/src/json.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {mapEntries} from '../../../../shared/src/objects.ts';\nimport type {DownloadStatus} from '../../../../zero-events/src/status.ts';\nimport {\n createLiteIndexStatement,\n createLiteTableStatement,\n liteColumnDef,\n} from '../../db/create.ts';\nimport {\n computeZqlSpecs,\n listTables,\n type LiteTableSpecWithReplicationStatus,\n} from '../../db/lite-tables.ts';\nimport {\n isArrayColumn,\n isEnumColumn,\n mapPostgresToLite,\n mapPostgresToLiteColumn,\n mapPostgresToLiteIndex,\n} from '../../db/pg-to-lite.ts';\nimport type {ColumnSpec} from '../../db/specs.ts';\nimport type {StatementRunner} from '../../db/statements.ts';\nimport type {LexiVersion} from '../../types/lexi-version.ts';\nimport {\n JSON_PARSED,\n liteRow,\n type JSONFormat,\n type LiteRow,\n type LiteRowKey,\n type LiteValueType,\n} from '../../types/lite.ts';\nimport {liteTableName} from '../../types/names.ts';\nimport {id} from '../../types/sql.ts';\nimport {\n acceptBackfill,\n type AcceptResult,\n} from '../change-source/protocol/backfill-progress.ts';\nimport type {\n BackfillCompleted,\n BackfillProgressMark,\n Change,\n ColumnAdd,\n ColumnDrop,\n ColumnUpdate,\n Identifier,\n IndexCreate,\n IndexDrop,\n MessageBackfill,\n MessageCommit,\n MessageDelete,\n MessageInsert,\n MessageRelation,\n MessageTruncate,\n MessageUpdate,\n TableCreate,\n TableDrop,\n TableRename,\n TableUpdateMetadata,\n} from '../change-source/protocol/current/data.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport type {ReplicatorMode} from './replicator.ts';\nimport {BackfillingTracker} from './schema/backfilling.ts';\nimport {ChangeLog, DEL_OP, SET_OP} from './schema/change-log.ts';\nimport {ColumnMetadataStore} from './schema/column-metadata.ts';\nimport {\n ZERO_VERSION_COLUMN_NAME,\n updateReplicationWatermark,\n} from './schema/replication-state.ts';\nimport {TableMetadataTracker} from './schema/table-metadata.ts';\n\nexport type ChangeProcessorMode = ReplicatorMode | 'initial-sync';\n\nconst BIND_VALUE_TOO_BIG_ERROR =\n 'The bound string, buffer, or bigint is too big';\nconst MAX_SQLITE_BIND_BYTES = Math.min(\n bufferConstants.MAX_LENGTH,\n bufferConstants.MAX_STRING_LENGTH,\n);\n\nexport type CommitResult = {\n watermark: string;\n completedBackfill: DownloadStatus | undefined;\n schemaUpdated: boolean;\n changeLogUpdated: boolean;\n /**\n * Millisecond epoch at which the transaction committed upstream, if the\n * ChangeSource reported one. Propagated to ViewSyncers as the origin\n * timestamp of the end-to-end serving lag measurement.\n */\n upstreamCommitTimeMs?: number | undefined;\n};\n\n/**\n * The ChangeProcessor partitions the stream of messages into transactions\n * by creating a {@link TransactionProcessor} when a transaction begins, and dispatching\n * messages to it until the commit is received.\n *\n * From https://www.postgresql.org/docs/current/protocol-logical-replication.html#PROTOCOL-LOGICAL-MESSAGES-FLOW :\n *\n * \"The logical replication protocol sends individual transactions one by one.\n * This means that all messages between a pair of Begin and Commit messages\n * belong to the same transaction.\"\n */\nexport class ChangeProcessor {\n readonly #db: StatementRunner;\n readonly #changeLog: ChangeLog;\n readonly #tableMetadata: TableMetadataTracker;\n readonly #backfilling: BackfillingTracker;\n readonly #mode: ChangeProcessorMode;\n readonly #failService: (lc: LogContext, err: unknown) => void;\n\n // The TransactionProcessor lazily loads table specs into this Map,\n // and reloads them after a schema change. It is cached here to avoid\n // reading them from the DB on every transaction.\n readonly #tableSpecs = new Map<string, LiteTableSpecWithReplicationStatus>();\n\n #currentTx: TransactionProcessor | null = null;\n\n #failure: Error | undefined;\n\n constructor(\n db: StatementRunner,\n mode: ChangeProcessorMode,\n failService: (lc: LogContext, err: unknown) => void,\n ) {\n this.#db = db;\n this.#changeLog = new ChangeLog(db.db);\n this.#tableMetadata = new TableMetadataTracker(db.db);\n this.#backfilling = new BackfillingTracker(db.db);\n this.#mode = mode;\n this.#failService = failService;\n }\n\n #fail(lc: LogContext, err: unknown) {\n if (!this.#failure) {\n let failureError = err;\n const rollbackErrors = [attempt(() => this.#currentTx?.abort(lc))].filter(\n e => e !== undefined,\n );\n\n if (rollbackErrors.length) {\n const combinedError = new Error(\n `Message processing failed and rollback also failed: operation error = ${String(err)}; rollback error = ${rollbackErrors.map(String).join('; ')}`,\n );\n combinedError.cause = err;\n failureError = combinedError;\n }\n\n this.#failure = ensureError(failureError);\n\n if (!(this.#failure instanceof AbortError)) {\n // Propagate the failure up to the service.\n lc.error?.('Message Processing failed:', this.#failure);\n this.#failService(lc, this.#failure);\n }\n }\n }\n\n abort(lc: LogContext) {\n this.#fail(lc, new AbortError());\n }\n\n /** @return If a transaction was committed. */\n processMessage(\n lc: LogContext,\n downstream: ChangeStreamData,\n ): CommitResult | null {\n const [type, message] = downstream;\n if (this.#failure) {\n lc.debug?.(`Dropping ${message.tag}`);\n return null;\n }\n try {\n const watermark =\n type === 'begin'\n ? downstream[2].commitWatermark\n : type === 'commit'\n ? downstream[2].watermark\n : undefined;\n return this.#processMessage(lc, message, watermark);\n } catch (e) {\n this.#fail(lc, e);\n }\n return null;\n }\n\n #beginTransaction(\n lc: LogContext,\n commitVersion: string,\n jsonFormat: JSONFormat,\n ): TransactionProcessor {\n const start = Date.now();\n\n // litestream can technically hold the lock for an arbitrary amount of time\n // when checkpointing a large commit. Crashing on the busy-timeout in this\n // scenario will either produce a corrupt backup or otherwise prevent\n // replication from proceeding.\n //\n // Instead, retry the lock acquisition indefinitely. If this masks\n // an unknown deadlock situation, manual intervention will be necessary.\n for (let i = 0; ; i++) {\n try {\n return new TransactionProcessor(\n lc,\n this.#db,\n this.#mode,\n this.#changeLog,\n this.#tableMetadata,\n this.#backfilling,\n this.#tableSpecs,\n commitVersion,\n jsonFormat,\n );\n } catch (e) {\n if (e instanceof SqliteError && e.code === 'SQLITE_BUSY') {\n lc.warn?.(\n `SQLITE_BUSY for ${Date.now() - start} ms (attempt ${i + 1}). ` +\n `This is only expected if litestream is performing a large ` +\n `checkpoint.`,\n e,\n );\n continue;\n }\n throw e;\n }\n }\n }\n\n /** @return If a transaction was committed. */\n #processMessage(\n lc: LogContext,\n msg: Change,\n watermark: string | undefined,\n ): CommitResult | null {\n if (msg.tag === 'begin') {\n if (this.#currentTx) {\n throw new Error(`Already in a transaction ${stringify(msg)}`);\n }\n this.#currentTx = this.#beginTransaction(\n lc,\n must(watermark),\n msg.json ?? JSON_PARSED,\n );\n return null;\n }\n\n // For non-begin messages, there should be a #currentTx set.\n const tx = this.#currentTx;\n if (!tx) {\n throw new Error(\n `Received message outside of transaction: ${stringify(msg)}`,\n );\n }\n\n if (msg.tag === 'commit') {\n assert(watermark, 'watermark is required for commit messages');\n const result = tx.processCommit(msg, watermark);\n // Clear only after a successful commit so #fail can roll back a commit\n // path that throws before SQLite has committed.\n this.#currentTx = null;\n return result;\n }\n\n if (msg.tag === 'rollback') {\n tx.abort(lc);\n this.#currentTx = null;\n return null;\n }\n\n switch (msg.tag) {\n case 'insert':\n tx.processInsert(msg);\n break;\n case 'update':\n tx.processUpdate(msg);\n break;\n case 'delete':\n tx.processDelete(msg);\n break;\n case 'truncate':\n tx.processTruncate(msg);\n break;\n case 'create-table':\n tx.processCreateTable(msg);\n break;\n case 'rename-table':\n tx.processRenameTable(msg);\n break;\n case 'update-table-metadata':\n tx.processTableMetadata(msg);\n break;\n case 'add-column':\n tx.processAddColumn(msg);\n break;\n case 'update-column':\n tx.processUpdateColumn(msg);\n break;\n case 'drop-column':\n tx.processDropColumn(msg);\n break;\n case 'drop-table':\n tx.processDropTable(msg);\n break;\n case 'create-index':\n tx.processCreateIndex(msg);\n break;\n case 'drop-index':\n tx.processDropIndex(msg);\n break;\n case 'backfill':\n tx.processBackfill(msg);\n break;\n case 'backfill-completed':\n tx.processBackfillCompleted(msg);\n break;\n default:\n unreachable(msg);\n }\n\n return null;\n }\n}\n\n/**\n * The {@link TransactionProcessor} handles the sequence of messages from\n * upstream, from `BEGIN` to `COMMIT` and executes the corresponding mutations\n * on the {@link postgres.TransactionSql} on the replica.\n *\n * When applying row contents to the replica, the `_0_version` column is added / updated,\n * and a corresponding entry in the `ChangeLog` is added. The version value is derived\n * from the watermark of the preceding transaction (stored as the `nextStateVersion` in the\n * `ReplicationState` table).\n *\n * Side note: For non-streaming Postgres transactions, the commitEndLsn (and thus\n * commit watermark) is available in the `begin` message, so it could theoretically\n * be used for the row version of changes within the transaction. However, the\n * commitEndLsn is not available in the streaming (in-progress) transaction\n * protocol, and may not be available for CDC streams of other upstream types.\n * Therefore, the zero replication protocol is designed to not require the commit\n * watermark when a transaction begins.\n *\n * Also of interest is the fact that all INSERT Messages are logically applied as\n * UPSERTs. See {@link processInsert} for the underlying motivation.\n */\nclass TransactionProcessor {\n readonly #lc: LogContext;\n readonly #startMs: number;\n readonly #db: StatementRunner;\n readonly #mode: ChangeProcessorMode;\n readonly #version: LexiVersion;\n readonly #changeLog: ChangeLog;\n readonly #tableMetadata: TableMetadataTracker;\n readonly #backfilling: BackfillingTracker;\n readonly #tableSpecs: Map<string, LiteTableSpecWithReplicationStatus>;\n readonly #jsonFormat: JSONFormat;\n readonly #columnMetadata: ColumnMetadataStore;\n\n #pos = 0;\n #schemaChanged = false;\n #numChangeLogEntries = 0;\n\n constructor(\n lc: LogContext,\n db: StatementRunner,\n mode: ChangeProcessorMode,\n changeLog: ChangeLog,\n tableMetadata: TableMetadataTracker,\n backfilling: BackfillingTracker,\n tableSpecs: Map<string, LiteTableSpecWithReplicationStatus>,\n commitVersion: LexiVersion,\n jsonFormat: JSONFormat,\n ) {\n this.#startMs = Date.now();\n this.#mode = mode;\n this.#jsonFormat = jsonFormat;\n\n switch (mode) {\n case 'serving':\n // This is the only transaction that commits to the serving replica.\n // Snapshotters use BEGIN CONCURRENT for private changes that are\n // always rolled back, while BEGIN IMMEDIATE lets this writer spill\n // dirty pages during large transactions.\n db.beginImmediate();\n break;\n case 'backup':\n // For the backup-replicator (i.e. replication-manager), there are no View Syncers\n // and thus BEGIN CONCURRENT is not necessary. In fact, BEGIN CONCURRENT can cause\n // deadlocks with forced wal-checkpoints (which `litestream replicate` performs),\n // so it is important to use vanilla transactions in this configuration.\n db.beginImmediate();\n break;\n case 'initial-sync':\n // When the ChangeProcessor is used for initial-sync, the calling code\n // handles the transaction boundaries.\n break;\n default:\n unreachable();\n }\n this.#db = db;\n this.#version = commitVersion;\n this.#lc = lc.withContext('version', commitVersion);\n this.#changeLog = changeLog;\n this.#tableMetadata = tableMetadata;\n this.#backfilling = backfilling;\n this.#tableSpecs = tableSpecs;\n // The column_metadata table is guaranteed to exist since the\n // replica-schema.ts migration to v8.\n this.#columnMetadata = must(ColumnMetadataStore.getInstance(db.db));\n\n if (this.#tableSpecs.size === 0) {\n this.#reloadTableSpecs();\n }\n }\n\n #reloadTableSpecs() {\n this.#tableSpecs.clear();\n // zqlSpecs include the primary key derived from unique indexes\n const zqlSpecs = computeZqlSpecs(this.#lc, this.#db.db, {\n includeBackfillingColumns: true,\n });\n for (let spec of listTables(this.#db.db)) {\n if (!spec.primaryKey) {\n spec = {\n ...spec,\n primaryKey: [\n ...(zqlSpecs.get(spec.name)?.tableSpec.primaryKey ?? []),\n ],\n };\n }\n this.#tableSpecs.set(spec.name, spec);\n }\n }\n\n #tableSpec(name: string) {\n return must(this.#tableSpecs.get(name), `Unknown table ${name}`);\n }\n\n #getKey(\n {row, numCols}: {row: LiteRow; numCols: number},\n {relation}: {relation: MessageRelation},\n ): LiteRowKey {\n const keyColumns =\n relation.rowKey.type !== 'full'\n ? relation.rowKey.columns // already a suitable key\n : this.#tableSpec(liteTableName(relation)).primaryKey;\n if (!keyColumns?.length) {\n throw new Error(\n `Cannot replicate table \"${relation.name}\" without a PRIMARY KEY or UNIQUE INDEX`,\n );\n }\n // For the common case (replica identity default), the row is already the\n // key for deletes and updates, in which case a new object can be avoided.\n if (numCols === keyColumns.length) {\n return row;\n }\n const key: Record<string, LiteValueType> = {};\n for (const col of keyColumns) {\n key[col] = row[col];\n }\n return key;\n }\n\n processInsert(insert: MessageInsert) {\n const table = liteTableName(insert.relation);\n const tableSpec = this.#tableSpec(table);\n const newRow = liteRow(insert.new, tableSpec, this.#jsonFormat);\n\n this.#upsert(table, {\n ...newRow.row,\n [ZERO_VERSION_COLUMN_NAME]: this.#version,\n });\n\n if (insert.relation.rowKey.columns.length === 0) {\n // INSERTs can be replicated for rows without a PRIMARY KEY or a\n // UNIQUE INDEX. These are written to the replica but not recorded\n // in the changeLog, because these rows cannot participate in IVM.\n //\n // (Once the table schema has been corrected to include a key, the\n // associated schema change will reset pipelines and data can be\n // loaded via hydration.)\n return;\n }\n const key = this.#getKey(newRow, insert);\n this.#logSetOp(table, key, getBackfilledColumns(newRow.row, tableSpec));\n }\n\n #upsert(table: string, row: LiteRow) {\n const columns = Object.keys(row).map(c => id(c));\n this.#db.run(\n `\n INSERT OR REPLACE INTO ${id(table)} (${columns.join(',')})\n VALUES (${Array.from({length: columns.length}).fill('?').join(',')})\n `,\n Object.values(row),\n );\n }\n\n // Updates by default are applied as UPDATE commands to support partial\n // row specifications from the change source. In particular, this is needed\n // to handle updates for which unchanged TOASTed values are not sent:\n //\n // https://www.postgresql.org/docs/current/protocol-logicalrep-message-formats.html#PROTOCOL-LOGICALREP-MESSAGE-FORMATS-TUPLEDATA\n //\n // However, in certain cases an UPDATE may be received for a row that\n // was not initially synced, such as when, an existing table is added\n // to the app's publication.\n //\n // In order to facilitate \"resumptive\" replication, the logic falls back to\n // an INSERT if the update did not change any rows.\n processUpdate(update: MessageUpdate) {\n const table = liteTableName(update.relation);\n const tableSpec = this.#tableSpec(table);\n const newRow = liteRow(update.new, tableSpec, this.#jsonFormat);\n const row = {...newRow.row, [ZERO_VERSION_COLUMN_NAME]: this.#version};\n\n // update.key is set with the old values if the key has changed.\n const oldKey = update.key\n ? this.#getKey(\n liteRow(update.key, this.#tableSpec(table), this.#jsonFormat),\n update,\n )\n : null;\n const newKey = this.#getKey(newRow, update);\n\n if (oldKey) {\n this.#logDeleteOp(table, oldKey, tableSpec.backfilling);\n }\n this.#logSetOp(table, newKey, getBackfilledColumns(newRow.row, tableSpec));\n\n const currKey = oldKey ?? newKey;\n const conds = Object.keys(currKey).map(col => `${id(col)}=?`);\n const setExprs = Object.keys(row).map(col => `${id(col)}=?`);\n\n try {\n const {changes} = this.#db.run(\n `\n UPDATE ${id(table)}\n SET ${setExprs.join(',')}\n WHERE ${conds.join(' AND ')}\n `,\n [...Object.values(row), ...Object.values(currKey)],\n );\n\n // If the UPDATE did not affect any rows, perform an UPSERT of the\n // new row for resumptive replication.\n if (changes === 0) {\n this.#upsert(table, row);\n }\n } catch (e) {\n if (\n !(e instanceof RangeError) ||\n e.message !== BIND_VALUE_TOO_BIG_ERROR\n ) {\n throw e;\n }\n\n const binding = [...Object.entries(row), ...Object.entries(currKey)]\n .map(\n ([column, value]) =>\n [column, oversizedValueDescription(value)] as const,\n )\n .find(([, description]) => description);\n const relationOid =\n 'relationOid' in update.relation &&\n typeof update.relation.relationOid === 'number'\n ? ` relationOid=${update.relation.relationOid}`\n : '';\n const error = new Error(\n `Oversized SQLite update binding: tx=${this.#version}${relationOid} table=${update.relation.schema}.${update.relation.name} column=${binding?.[0] ?? 'unknown'}${binding?.[1] ? ` ${binding[1]}` : ''}`,\n {cause: e},\n );\n error.name = 'OversizedUpdateBindingError';\n throw error;\n }\n }\n\n processDelete(del: MessageDelete) {\n const table = liteTableName(del.relation);\n const tableSpec = this.#tableSpec(table);\n const rowKey = this.#getKey(\n liteRow(del.key, tableSpec, this.#jsonFormat),\n del,\n );\n\n this.#delete(table, rowKey);\n this.#logDeleteOp(table, rowKey, tableSpec.backfilling);\n }\n\n #delete(table: string, rowKey: LiteRowKey) {\n const conds = Object.keys(rowKey).map(col => `${id(col)}=?`);\n this.#db.run(\n `DELETE FROM ${id(table)} WHERE ${conds.join(' AND ')}`,\n Object.values(rowKey),\n );\n }\n\n processTruncate(truncate: MessageTruncate) {\n for (const relation of truncate.relations) {\n const table = liteTableName(relation);\n // Update replica data.\n this.#db.run(`DELETE FROM ${id(table)}`);\n\n // Update change log.\n this.#logTruncateOp(table);\n }\n }\n\n processCreateTable(create: TableCreate) {\n if (create.metadata) {\n this.#tableMetadata.setUpstreamMetadata(create.spec, create.metadata);\n }\n this.#backfilling.apply(create);\n const table = mapPostgresToLite(create.spec);\n this.#db.db.exec(createLiteTableStatement(table));\n\n // Write to metadata table\n for (const [colName, colSpec] of Object.entries(create.spec.columns)) {\n this.#columnMetadata.insert(\n table.name,\n colName,\n colSpec,\n create.backfill?.[colName],\n );\n }\n\n this.#logResetOp(table.name);\n this.#lc.info?.(create.tag, table.name);\n }\n\n processTableMetadata(msg: TableUpdateMetadata) {\n this.#tableMetadata.setUpstreamMetadata(msg.table, msg.new);\n // Inert today: the fold's only op for this tag is `upsert-metadata`, which\n // the line above is the replica's interpreter of. Called anyway so that\n // every site that hands a schema change to one cookie tracker hands it to\n // both -- an exception here would be a schema change the shared fold has an\n // opinion about and this store never sees, which is the drift the fold\n // exists to make impossible.\n this.#backfilling.apply(msg);\n }\n\n processRenameTable(rename: TableRename) {\n this.#tableMetadata.rename(rename.old, rename.new);\n this.#backfilling.apply(rename);\n\n const oldName = liteTableName(rename.old);\n const newName = liteTableName(rename.new);\n this.#db.db.exec(`ALTER TABLE ${id(oldName)} RENAME TO ${id(newName)}`);\n\n // Rename in metadata table\n this.#columnMetadata.renameTable(oldName, newName);\n\n this.#bumpVersions(rename.new);\n this.#logResetOp(oldName);\n this.#lc.info?.(rename.tag, oldName, newName);\n }\n\n processAddColumn(msg: ColumnAdd) {\n if (msg.tableMetadata) {\n this.#tableMetadata.setUpstreamMetadata(msg.table, msg.tableMetadata);\n }\n this.#backfilling.apply(msg);\n const table = liteTableName(msg.table);\n const {name} = msg.column;\n const spec = mapPostgresToLiteColumn(table, msg.column);\n this.#db.db.exec(\n `ALTER TABLE ${id(table)} ADD ${id(name)} ${liteColumnDef(spec)}`,\n );\n\n // Write to metadata table\n this.#columnMetadata.insert(table, name, msg.column.spec, msg.backfill);\n\n if (msg.backfill) {\n this.#reloadTableSpecs();\n } else {\n // Make the new column visible immediately if it's not being backfilled.\n // Otherwise, the version bump will happen with the backfill is complete.\n this.#bumpVersions(msg.table);\n }\n this.#lc.info?.(msg.tag, table, msg.column);\n }\n\n processUpdateColumn(msg: ColumnUpdate) {\n // A no-op unless this is a rename, which is the only part of a column\n // update that moves the backfill cookie.\n this.#backfilling.apply(msg);\n\n const table = liteTableName(msg.table);\n let oldName = msg.old.name;\n const newName = msg.new.name;\n\n const storageTypesDiffer = differentStorageTypes(\n msg.old.spec,\n msg.new.spec,\n );\n\n // If neither the column name nor the SQLite data type changes, only the\n // upstream metadata needs to be updated. This includes changes such as a\n // varchar character limit and nullability, which SQLite does not enforce\n // but a freshly built replica still records.\n if (oldName === newName && !storageTypesDiffer) {\n this.#columnMetadata.update(\n table,\n msg.old.name,\n msg.new.name,\n msg.new.spec,\n );\n if (Boolean(msg.old.spec.notNull) !== Boolean(msg.new.spec.notNull)) {\n this.#bumpVersions(msg.table);\n }\n this.#lc.info?.(msg.tag, 'updated metadata only', msg.old, msg.new);\n return;\n }\n // Storage type changes require a new column so that SQLite uses the new\n // representation for existing and future values.\n if (storageTypesDiffer) {\n // update-column can ignore defaults because it does not change the values\n // in existing rows.\n //\n // https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-DESC-SET-DROP-DEFAULT\n //\n // \"The new default value will only apply in subsequent INSERT or UPDATE\n // commands; it does not cause rows already in the table to change.\"\n //\n // This allows support for _changing_ column defaults to any expression,\n // since it does not affect what the replica needs to do.\n const newLiteSpec = mapPostgresToLiteColumn(\n table,\n msg.new,\n 'ignore-default',\n );\n if (oldName !== newName) {\n // Rename the column in place first so that SQLite rewrites every\n // reference to it in the table's index definitions, including the\n // WHERE clauses of partial indexes.\n this.#db.db.exec(\n `ALTER TABLE ${id(table)} RENAME ${id(oldName)} TO ${id(newName)}`,\n );\n oldName = newName;\n }\n const tableSpec = must(\n listTables(this.#db.db, false, false).find(\n tableSpec => tableSpec.name === table,\n ),\n );\n // Recreating the table drops its indexes, so capture their DDL to\n // replay afterwards. Auto-indexes (PRIMARY KEY) have no DDL and are\n // recreated by the CREATE TABLE statement itself.\n const indexDDL = this.#db.db\n .prepare(\n `SELECT sql FROM sqlite_master\n WHERE type = 'index' AND tbl_name = ? AND sql IS NOT NULL\n ORDER BY rowid`,\n )\n .all<{sql: string}>(table)\n .map(({sql}) => sql);\n const tmpTable = `tmp.${table}`;\n const columns = Object.keys(tableSpec.columns);\n const stmts = [\n createLiteTableStatement({\n ...tableSpec,\n name: tmpTable,\n columns: mapEntries(tableSpec.columns, (column, spec) => [\n column,\n column === newName ? {...newLiteSpec, pos: spec.pos} : spec,\n ]),\n }),\n `INSERT INTO ${id(tmpTable)} (${columns.map(id).join(',')})\n SELECT ${columns.map(id).join(',')} FROM ${id(table)};`,\n `DROP TABLE ${id(table)};`,\n `ALTER TABLE ${id(tmpTable)} RENAME TO ${id(table)};`,\n ...indexDDL.map(sql => `${sql};`),\n ];\n this.#db.db.exec(stmts.join(''));\n }\n if (oldName !== newName) {\n this.#db.db.exec(\n `ALTER TABLE ${id(table)} RENAME ${id(oldName)} TO ${id(newName)}`,\n );\n }\n\n // Update metadata table\n this.#columnMetadata.update(\n table,\n msg.old.name,\n msg.new.name,\n msg.new.spec,\n );\n\n this.#bumpVersions(msg.table);\n this.#lc.info?.(msg.tag, table, msg.new);\n }\n\n processDropColumn(msg: ColumnDrop) {\n const table = liteTableName(msg.table);\n const {column} = msg;\n this.#db.db.exec(`ALTER TABLE ${id(table)} DROP ${id(column)}`);\n\n // Delete from metadata table\n this.#columnMetadata.deleteColumn(table, column);\n this.#backfilling.apply(msg);\n\n this.#bumpVersions(msg.table);\n this.#lc.info?.(msg.tag, table, column);\n }\n\n processDropTable(drop: TableDrop) {\n this.#tableMetadata.drop(drop.id);\n this.#backfilling.apply(drop);\n\n const name = liteTableName(drop.id);\n this.#db.db.exec(`DROP TABLE IF EXISTS ${id(name)}`);\n\n // Delete from metadata table\n this.#columnMetadata.deleteTable(name);\n\n this.#logResetOp(name);\n this.#lc.info?.(drop.tag, name);\n }\n\n processCreateIndex(create: IndexCreate) {\n const index = mapPostgresToLiteIndex(create.spec);\n this.#db.db.exec(createLiteIndexStatement(index));\n\n // indexes affect tables visibility (e.g. sync-ability is gated on\n // having a unique index), so reset pipelines to refresh table schemas.\n this.#logResetOp(index.tableName);\n this.#lc.info?.(create.tag, index.name);\n }\n\n processDropIndex(drop: IndexDrop) {\n const name = liteTableName(drop.id);\n const indexExisted = this.#db.db\n .prepare(\n `SELECT tbl_name as tableName FROM sqlite_master WHERE type = 'index' AND name = ?`,\n )\n .get(name) as {tableName: string} | undefined;\n\n this.#db.db.exec(`DROP INDEX IF EXISTS ${id(name)}`);\n\n if (indexExisted) {\n // indexes affect tables visibility (e.g. sync-ability is gated on\n // having a unique index), so reset pipelines to refresh table schemas.\n this.#logResetOp(indexExisted.tableName);\n }\n this.#lc.info?.(drop.tag, name);\n }\n\n #bumpVersions(table: Identifier) {\n this.#tableMetadata.setMinRowVersion(table, this.#version);\n this.#logResetOp(liteTableName(table));\n }\n\n /**\n * @param backfilledColumns `backfilling` columns for which values were set\n */\n #logSetOp(\n table: string,\n key: LiteRowKey,\n backfilledColumns: string[] | undefined,\n ) {\n // The \"serving\" replicator always writes to the change-log (for IVM).\n // The \"backup\" replicator only needs to write to the change log\n // when writing columns that are being backfilled.\n if (this.#mode === 'serving' || backfilledColumns !== undefined) {\n this.#changeLog.logSetOp(\n this.#version,\n this.#pos++,\n table,\n key,\n backfilledColumns,\n );\n this.#numChangeLogEntries++;\n }\n }\n\n #logDeleteOp(table: string, key: LiteRowKey, backfilling?: string[]) {\n // The \"serving\" replicator always writes to the change-log (for IVM).\n // The \"backup\" replicator only needs to write to the change log\n // when writing columns that are being backfilled.\n if (this.#mode === 'serving' || backfilling?.length) {\n this.#changeLog.logDeleteOp(this.#version, this.#pos++, table, key);\n this.#numChangeLogEntries++;\n }\n }\n\n #logTruncateOp(table: string) {\n if (this.#mode === 'serving') {\n this.#changeLog.logTruncateOp(this.#version, table);\n this.#numChangeLogEntries++;\n }\n }\n\n #logResetOp(table: string) {\n this.#schemaChanged = true;\n if (this.#mode === 'serving') {\n this.#changeLog.logResetOp(this.#version, table);\n this.#numChangeLogEntries++;\n }\n this.#reloadTableSpecs();\n }\n\n processBackfill({\n relation,\n watermark,\n columns,\n rowValues,\n progressMarks,\n }: MessageBackfill) {\n const tableName = liteTableName(relation);\n const tableSpec = must(this.#tableSpecs.get(tableName));\n const rowKeyCols = relation.rowKey.columns;\n const cols = [...rowKeyCols, ...columns];\n\n // Only columns that are still being backfilled on _this_ replica are\n // written, and of those, only the columns for which this message is\n // contiguous with the data received so far (whose progress is advanced).\n //\n // * Once a column has been published (backfill completed), its values\n // strictly follow the replication timeline, so a subsequent backfill must\n // never overwrite them. This guards against a redundant backfill, e.g.\n // when the view-syncer (re)connects to a replication-manager that is\n // (re)running a backfill this replica already completed.\n // * Data for non-contiguous (ignored) columns is necessarily re-delivered\n // (by a resumed or from-scratch run) before the column can be published,\n // so writing it would be wasted I/O.\n const {accepted, ignored} = this.#trackBackfillProgress(\n relation,\n cols,\n own => acceptBackfill(own, progressMarks),\n );\n if (accepted.length === 0) {\n this.#lc.debug?.(\n ignored.length\n ? `skipping backfill of ${rowValues.length} rows into ` +\n `${tableName}: not contiguous with the progress of columns ` +\n `[${ignored.join(',')}]`\n : `skipping redundant backfill of ${rowValues.length} rows into ` +\n `${tableName} (all columns already published)`,\n progressMarks,\n );\n return;\n }\n if (ignored.length) {\n this.#lc.debug?.(\n `backfill of ${tableName} is not contiguous with the progress ` +\n `of columns [${ignored.join(',')}]`,\n progressMarks,\n );\n }\n const acceptedSet = new Set(accepted);\n\n // Common parts of the INSERT sql statement.\n const insertColsStr = [...cols, ZERO_VERSION_COLUMN_NAME].map(id).join(',');\n const qMarks = Array.from({length: cols.length + 1})\n .fill('?')\n .join(',');\n const rowKeyColsStr = rowKeyCols.map(id).join(',');\n\n let backfilled = 0;\n let skipped = 0;\n for (const v of rowValues) {\n const row = liteRow(\n Object.fromEntries(cols.map((c, i) => [c, v[i]])),\n tableSpec,\n this.#jsonFormat,\n );\n const rowKey = this.#getKey(row, {relation});\n const rowOp = this.#changeLog.getLatestRowOp(tableName, rowKey);\n if (rowOp?.op === DEL_OP && rowOp.stateVersion > watermark) {\n skipped++;\n continue; // the row was deleted after the backfill snapshot\n }\n // Restrict the DO UPDATE columns to those still being backfilled. Never\n // update an already backfilled (i.e. published) column.\n const updates = (\n rowOp?.op === SET_OP\n ? cols.filter(\n c => (rowOp.backfillingColumnVersions[c] ?? '') <= watermark,\n )\n : cols\n ).filter(c => acceptedSet.has(c));\n if (updates.length === 0) {\n // row already has newer values for all accepted backfilling columns.\n skipped++;\n continue;\n }\n const updateStmts = updates.map(col => `${id(col)}=excluded.${id(col)}`);\n this.#db.run(\n /*sql*/ `\n INSERT INTO ${id(tableName)} (${insertColsStr}) VALUES (${qMarks})\n ON CONFLICT (${rowKeyColsStr})\n DO UPDATE SET ${updateStmts.join(',')};\n `,\n ...Object.values(row.row),\n watermark, // the _0_version for new rows (i.e. table backfill)\n );\n backfilled++;\n }\n\n this.#lc.debug?.(\n `backfilled ${backfilled} rows (skipped ${skipped}) into ${tableName}`,\n );\n }\n\n #completedBackfill: DownloadStatus | undefined;\n\n /**\n * Applies `accept` to the tracked progress of each of the `cols` that are\n * still backfilling on this replica, updating the progress of the accepted\n * ones. Columns that are not backfilling (e.g. already published) are\n * neither accepted nor ignored.\n */\n #trackBackfillProgress(\n relation: Identifier,\n cols: readonly string[],\n accept: (progress: BackfillProgressMark | undefined) => AcceptResult,\n ): {accepted: string[]; ignored: string[]} {\n const tracked = this.#backfilling.getProgress(relation);\n const accepted: string[] = [];\n const ignored: string[] = [];\n for (const col of cols) {\n if (!tracked.has(col)) {\n continue; // not backfilling\n }\n const progress = tracked.get(col);\n const result = accept(progress);\n if (!result.accept) {\n ignored.push(col);\n continue;\n }\n accepted.push(col);\n if (!deepEqual(result.progress, progress)) {\n this.#backfilling.setProgress(relation, col, result.progress);\n }\n }\n return {accepted, ignored};\n }\n\n processBackfillCompleted(msg: BackfillCompleted) {\n const {relation, columns, status, progressMarks} = msg;\n const tableName = liteTableName(relation);\n const rowKeyCols = relation.rowKey.columns;\n\n // Only complete the columns whose backfill progress is contiguous with\n // the completed backfill. The others remain backfilling (and will be\n // re-requested by the change-streamer).\n const {accepted, ignored} = this.#trackBackfillProgress(\n relation,\n [...rowKeyCols, ...columns],\n own =>\n acceptBackfill(\n own,\n progressMarks ? {previous: progressMarks.previous} : undefined,\n ),\n );\n if (ignored.length) {\n this.#lc.info?.(\n `ignoring backfill-completed of ${tableName} for columns ` +\n `[${ignored.join(',')}], which have not received all of the data`,\n progressMarks,\n );\n }\n\n // If none of the columns are still backfilling on this replica, they were\n // already published (e.g. a redundant backfill from a replication-manager\n // re-running a backfill this replica already completed). Bumping versions\n // again would spuriously reset pipelines and re-report completion, so treat\n // the completion as a no-op.\n if (accepted.length === 0) {\n if (ignored.length === 0) {\n this.#lc.debug?.(\n `skipping redundant backfill-completed for ${tableName} ` +\n `(all columns already published)`,\n );\n }\n return;\n }\n const ignoredSet = new Set(ignored);\n const cols = [\n ...rowKeyCols.filter(c => !ignoredSet.has(c)),\n ...columns.filter(c => !ignoredSet.has(c)),\n ];\n\n const columnMetadata = must(ColumnMetadataStore.getInstance(this.#db.db));\n for (const col of cols) {\n columnMetadata.clearBackfilling(tableName, col);\n }\n this.#backfilling.complete(relation, cols);\n // Given that new columns are being exposed for every row in the table, bump the\n // row version for all rows.\n this.#bumpVersions(relation);\n if (status) {\n this.#completedBackfill = {table: tableName, columns: cols, ...status};\n }\n this.#lc.info?.(`finished backfilling ${tableName}`);\n\n // Note that there is no need to clear the backfillingColumnVersions values\n // in the changeLog. It could theoretically be done for clarity but:\n // (1) it could be non-trivial in terms of latency introduced and\n // (2) the data must be preserved if _other_ columns are in the process\n // of being backfilled\n //\n // Thus, for speed and simplicity, the values are left as is. (Note that\n // subsequent replicated changes to those rows will clear the values if\n // no backfills are in progress).\n }\n\n processCommit(commit: MessageCommit, watermark: string): CommitResult {\n if (watermark !== this.#version) {\n throw new Error(\n `'commit' version ${watermark} does not match 'begin' version ${\n this.#version\n }: ${stringify(commit)}`,\n );\n }\n updateReplicationWatermark(this.#db, watermark);\n\n if (this.#schemaChanged) {\n const start = Date.now();\n this.#db.db.pragma('optimize');\n this.#lc.info?.(\n `PRAGMA optimized after schema change (${Date.now() - start} ms)`,\n );\n }\n\n if (this.#mode !== 'initial-sync') {\n this.#db.commit();\n }\n\n const elapsedMs = Date.now() - this.#startMs;\n this.#lc.debug?.(`Committed tx@${this.#version} (${elapsedMs} ms)`);\n\n return {\n watermark,\n completedBackfill: this.#completedBackfill,\n schemaUpdated: this.#schemaChanged,\n changeLogUpdated: this.#numChangeLogEntries > 0,\n upstreamCommitTimeMs: commit.commitTimeMs,\n };\n }\n\n abort(lc: LogContext) {\n lc.info?.(`aborting transaction ${this.#version}`);\n this.#db.rollback();\n // A schema change in the transaction reloaded the table specs, which now\n // describe the schema that the rollback undid. Clearing them makes the\n // next transaction reload them from the database.\n this.#tableSpecs.clear();\n }\n}\n\nfunction differentStorageTypes(\n oldSpec: ColumnSpec,\n newSpec: ColumnSpec,\n): boolean {\n return (\n oldSpec.dataType !== newSpec.dataType ||\n isEnumColumn(oldSpec) !== isEnumColumn(newSpec) ||\n isArrayColumn(oldSpec) !== isArrayColumn(newSpec)\n );\n}\n\nfunction getBackfilledColumns(\n row: LiteRow,\n {backfilling}: LiteTableSpecWithReplicationStatus,\n): string[] | undefined {\n if (!backfilling?.length) {\n return undefined; // common case\n }\n return backfilling.filter(col => col in row);\n}\n\nfunction oversizedValueDescription(value: LiteValueType): string | undefined {\n if (typeof value === 'bigint') {\n return BigInt.asIntN(64, value) !== value\n ? 'valueType=bigint fitsInt64=false'\n : undefined;\n }\n\n const sizeBytes =\n typeof value === 'string'\n ? Buffer.byteLength(value)\n : value instanceof Uint8Array\n ? value.byteLength\n : undefined;\n if (sizeBytes !== undefined && sizeBytes > MAX_SQLITE_BIND_BYTES) {\n const valueType = typeof value === 'string' ? 'string' : 'buffer';\n return `valueType=${valueType} sizeBytes=${sizeBytes} limitBytes=${MAX_SQLITE_BIND_BYTES}`;\n }\n\n return undefined;\n}\n\n/** Runs `fn`, returning its error rather than throwing it. */\nfunction attempt(fn: () => void): unknown {\n try {\n fn();\n return undefined;\n } catch (e) {\n return e ?? new Error('rollback failed');\n }\n}\n\nfunction ensureError(err: unknown): Error {\n if (err instanceof Error) {\n return err;\n }\n const error = new Error();\n error.cause = err;\n return error;\n}\n"],"mappings":"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"}
|