@rocicorp/zero 1.0.1-canary.0 → 1.1.0
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.115.0 → _@oxc-project_runtime@0.122.0}/helpers/usingCtx.js +1 -1
- package/out/replicache/src/mutation-recovery.js +0 -3
- package/out/zero/package.js +7 -6
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/services/analyze.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js +37 -16
- package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/lsn.js +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +6 -2
- package/out/zero-cache/src/services/life-cycle.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js +1 -1
- package/out/zero-cache/src/workers/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/workers/replicator.js +1 -0
- package/out/zero-cache/src/workers/replicator.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zql/src/builder/builder.d.ts.map +1 -1
- package/out/zql/src/builder/builder.js +15 -5
- package/out/zql/src/builder/builder.js.map +1 -1
- package/out/zql/src/ivm/cap.d.ts +32 -0
- package/out/zql/src/ivm/cap.d.ts.map +1 -0
- package/out/zql/src/ivm/cap.js +226 -0
- package/out/zql/src/ivm/cap.js.map +1 -0
- package/out/zql/src/ivm/join-utils.d.ts +2 -0
- package/out/zql/src/ivm/join-utils.d.ts.map +1 -1
- package/out/zql/src/ivm/join-utils.js +35 -1
- 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 -2
- package/out/zql/src/ivm/join.js.map +1 -1
- package/out/zql/src/ivm/memory-source.d.ts +15 -2
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +69 -8
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/ivm/schema.d.ts +1 -1
- package/out/zql/src/ivm/schema.d.ts.map +1 -1
- package/out/zql/src/ivm/skip.d.ts.map +1 -1
- package/out/zql/src/ivm/skip.js +3 -0
- package/out/zql/src/ivm/skip.js.map +1 -1
- package/out/zql/src/ivm/source.d.ts +1 -1
- package/out/zql/src/ivm/source.d.ts.map +1 -1
- package/out/zql/src/ivm/take.d.ts +4 -1
- package/out/zql/src/ivm/take.d.ts.map +1 -1
- package/out/zql/src/ivm/take.js +4 -2
- package/out/zql/src/ivm/take.js.map +1 -1
- package/out/zql/src/ivm/union-fan-in.d.ts.map +1 -1
- package/out/zql/src/ivm/union-fan-in.js +1 -0
- package/out/zql/src/ivm/union-fan-in.js.map +1 -1
- package/out/zqlite/src/query-builder.d.ts +1 -1
- package/out/zqlite/src/query-builder.d.ts.map +1 -1
- package/out/zqlite/src/query-builder.js +7 -2
- package/out/zqlite/src/query-builder.js.map +1 -1
- package/out/zqlite/src/table-source.d.ts +1 -1
- package/out/zqlite/src/table-source.d.ts.map +1 -1
- package/out/zqlite/src/table-source.js +15 -10
- package/out/zqlite/src/table-source.js.map +1 -1
- package/package.json +7 -6
- package/out/replicache/src/mutation-recovery.js.map +0 -1
|
@@ -22,6 +22,6 @@ function fromBigInt(val) {
|
|
|
22
22
|
return `${high.toString(16).toUpperCase()}/${low.toString(16).toUpperCase()}`;
|
|
23
23
|
}
|
|
24
24
|
//#endregion
|
|
25
|
-
export { fromBigInt, fromStateVersionString, toStateVersionString };
|
|
25
|
+
export { fromBigInt, fromStateVersionString, toBigInt, toStateVersionString };
|
|
26
26
|
|
|
27
27
|
//# sourceMappingURL=lsn.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"life-cycle.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/life-cycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,WAAW,CAAC;AAEnD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,QAAQ,CAAC;AACzC,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,cAAc,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,YAAY,CAAC;AAEtD,eAAO,MAAM,iBAAiB,gCAAiC,CAAC;AAChE,eAAO,MAAM,iBAAiB,sBAAuB,CAAC;AAEtD;;;GAGG;AACH,qBAAa,cAAc;;gBAWb,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY;IAiC9C,IAAI;IAoBJ,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM;
|
|
1
|
+
{"version":3,"file":"life-cycle.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/life-cycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,WAAW,CAAC;AAEnD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,QAAQ,CAAC;AACzC,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,cAAc,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,YAAY,CAAC;AAEtD,eAAO,MAAM,iBAAiB,gCAAiC,CAAC;AAChE,eAAO,MAAM,iBAAiB,sBAAuB,CAAC;AAEtD;;;GAGG;AACH,qBAAa,cAAc;;gBAWb,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY;IAiC9C,IAAI;IAoBJ,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM;IA2B9D,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAiBjE,YAAY,IAAI,MAAM,EAAE;IAIlB,eAAe;IAIrB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM;CAqE3C;AAED;;;;;;GAMG;AAEH,wBAAsB,cAAc,CAClC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,YAAY,EACpB,GAAG,QAAQ,EAAE,gBAAgB,EAAE,GAC9B,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,iBAWvD;AAID;;;;;;;;;;GAUG;AACH,qBAAa,gBAAgB;;gBAQf,EAAE,EAAE,UAAU,EAAE,YAAY,SAA2B;IAKnE,WAAW,CAAC,UAAU,EAAE,mBAAmB;IAsD3C,IAAI;CAML"}
|
|
@@ -45,8 +45,12 @@ var ProcessManager = class {
|
|
|
45
45
|
addSubprocess(proc, type, name) {
|
|
46
46
|
if (type === "user-facing") this.#userFacing.add(proc);
|
|
47
47
|
this.#all.add(proc);
|
|
48
|
-
|
|
49
|
-
proc.on("close", (code, signal) =>
|
|
48
|
+
let isOpen = true;
|
|
49
|
+
proc.on("close", (code, signal) => {
|
|
50
|
+
isOpen = false;
|
|
51
|
+
this.#onExit(code, signal, null, type, name, proc);
|
|
52
|
+
});
|
|
53
|
+
proc.on("error", (err) => this.#lc[isOpen ? "error" : "warn"]?.(`error from ${name} ${proc.pid}`, err));
|
|
50
54
|
}
|
|
51
55
|
#initializing = /* @__PURE__ */ new Map();
|
|
52
56
|
#nextID = 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"life-cycle.js","names":["#lc","#userFacing","#all","#exitImpl","#start","#ready","#startDrain","#kill","#exit","#runningState","#drainStart","#onExit","#initializing","#nextID","#stopInterval","#lastHeartbeat","#checkIntervalTimer","#checkStopInterval","#checkImmediateTimer"],"sources":["../../../../../zero-cache/src/services/life-cycle.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport type {IncomingHttpHeaders} from 'node:http';\nimport {pid} from 'node:process';\nimport type {EventEmitter} from 'stream';\nimport {\n singleProcessMode,\n type Subprocess,\n type Worker,\n} from '../types/processes.ts';\nimport {RunningState} from './running-state.ts';\nimport type {SingletonService} from './service.ts';\n\n/**\n * * `user-facing` workers serve external requests and are the first to\n * receive a `SIGTERM` or `SIGINT` signal for graceful shutdown.\n *\n * * `supporting` workers support `user-facing` workers and are sent\n * the `SIGTERM` signal only after all `user-facing` workers have\n * exited.\n *\n * For other kill signals, such as `SIGQUIT`, all workers\n * are stopped without draining. Additionally, if any worker exits\n * unexpectedly, all workers sent an immediate `SIGQUIT` signal.\n */\nexport type WorkerType = 'user-facing' | 'supporting';\n\nexport const GRACEFUL_SHUTDOWN = ['SIGTERM', 'SIGINT'] as const;\nexport const FORCEFUL_SHUTDOWN = ['SIGQUIT'] as const;\n\n/**\n * Handles readiness, termination signals, and coordination of graceful\n * shutdown.\n */\nexport class ProcessManager {\n readonly #lc: LogContext;\n readonly #userFacing = new Set<Subprocess>();\n readonly #all = new Set<Subprocess>();\n readonly #exitImpl: (code: number) => never;\n readonly #start = Date.now();\n readonly #ready: Promise<void>[] = [];\n\n #runningState = new RunningState('process-manager');\n #drainStart = 0;\n\n constructor(lc: LogContext, proc: EventEmitter) {\n this.#lc = lc.withContext('component', 'process-manager');\n\n // Propagate `SIGTERM` and `SIGINT` to all user-facing workers,\n // initiating a graceful shutdown. The parent process will\n // exit once all user-facing workers have exited ...\n for (const signal of GRACEFUL_SHUTDOWN) {\n proc.on(signal, () => this.#startDrain(signal));\n }\n\n // ... which will result in sending `SIGTERM` to the remaining workers.\n proc.on('exit', code =>\n this.#kill(\n this.#all,\n code === 0 ? GRACEFUL_SHUTDOWN[0] : FORCEFUL_SHUTDOWN[0],\n ),\n );\n\n // For other (catchable) kill signals, exit with a non-zero error code\n // to send a `SIGQUIT` to all workers. For this signal, workers are\n // stopped immediately without draining. See `runUntilKilled()`.\n for (const signal of FORCEFUL_SHUTDOWN) {\n proc.on(signal, () => this.#exit(-1));\n }\n\n this.#exitImpl = (code: number) => {\n if (singleProcessMode()) {\n return proc.emit('exit', code) as never; // For unit / integration tests.\n }\n process.exit(code);\n };\n }\n\n done() {\n return this.#runningState.stopped();\n }\n\n #exit(code: number) {\n this.#lc.info?.('exiting with code', code);\n this.#runningState.stop(this.#lc);\n void this.#lc.flush().finally(() => this.#exitImpl(code));\n }\n\n #startDrain(signal: 'SIGTERM' | 'SIGINT' = 'SIGTERM') {\n this.#lc.info?.(`initiating drain (${signal})`);\n this.#drainStart = Date.now();\n if (this.#userFacing.size) {\n this.#kill(this.#userFacing, signal);\n } else {\n this.#kill(this.#all, signal);\n }\n }\n\n addSubprocess(proc: Subprocess, type: WorkerType, name: string) {\n if (type === 'user-facing') {\n this.#userFacing.add(proc);\n }\n this.#all.add(proc);\n\n proc.on('error', err =>\n this.#lc.error?.(`error from ${name} ${proc.pid}`, err),\n );\n proc.on('close', (code, signal) =>\n this.#onExit(code, signal, null, type, name, proc),\n );\n }\n\n readonly #initializing = new Map<number, string>();\n #nextID = 0;\n\n addWorker(worker: Worker, type: WorkerType, name: string): Worker {\n this.addSubprocess(worker, type, name);\n\n const id = ++this.#nextID;\n this.#initializing.set(id, name);\n const {promise, resolve} = resolver();\n this.#ready.push(promise);\n\n worker.onceMessageType('ready', () => {\n this.#lc.debug?.(`${name} ready (${Date.now() - this.#start} ms)`);\n this.#initializing.delete(id);\n resolve();\n });\n\n return worker;\n }\n\n initializing(): string[] {\n return [...this.#initializing.values()];\n }\n\n async allWorkersReady() {\n await Promise.all(this.#ready);\n }\n\n logErrorAndExit(err: unknown, name: string) {\n // only accessible by the main (i.e. user-facing) process.\n this.#onExit(-1, null, err, 'user-facing', name, undefined);\n }\n\n #onExit(\n code: number,\n sig: NodeJS.Signals | null,\n err: unknown | null,\n type: WorkerType,\n name: string,\n worker: Subprocess | undefined,\n ) {\n // Remove the worker from maps to avoid attempting to send more signals to it.\n if (worker) {\n this.#userFacing.delete(worker);\n this.#all.delete(worker);\n }\n\n const pid = worker?.pid ?? process.pid;\n\n if (type === 'supporting') {\n // The replication-manager has no user-facing workers.\n // In this case, code === 0 shutdowns are not errors.\n // Non-zero exits are warnings (not errors) since they're often transient issues.\n const log = code === 0 && this.#userFacing.size === 0 ? 'info' : 'warn';\n this.#lc[log]?.(`${name} (${pid}) exited with code (${code})`, err ?? '');\n return this.#exit(log === 'info' ? code : -1);\n }\n\n const log = this.#drainStart === 0 ? 'error' : 'warn';\n if (sig) {\n this.#lc[log]?.(`${name} (${pid}) killed with (${sig})`, err ?? '');\n } else if (code !== 0) {\n this.#lc[log]?.(`${name} (${pid}) exited with code (${code})`, err ?? '');\n } else {\n this.#lc.info?.(`${name} (${pid}) exited with code (${code})`);\n }\n\n // user-facing workers exited or finished draining.\n if (this.#userFacing.size === 0) {\n this.#lc.info?.(\n this.#drainStart\n ? `all user-facing workers drained (${\n Date.now() - this.#drainStart\n } ms)`\n : `all user-facing workers exited`,\n );\n return this.#exit(0);\n }\n\n // Exit only if not draining. If a user-facing worker exits unexpectedly\n // during a drain, log a warning but let other user-facing workers drain.\n if (log === 'error') {\n return this.#exit(code || -1);\n }\n\n return undefined;\n }\n\n #kill(workers: Iterable<Subprocess>, signal: NodeJS.Signals) {\n for (const worker of workers) {\n try {\n worker.kill(signal);\n } catch (e) {\n this.#lc.error?.(e);\n }\n }\n }\n}\n\n/**\n * Runs the specified services, stopping them on `SIGTERM` or `SIGINT` with\n * an optional {@link SingletonService.drain drain()}, or stopping them\n * without draining for `SIGQUIT`.\n *\n * @returns a Promise that resolves/rejects when any of the services stops/throws.\n */\n\nexport async function runUntilKilled(\n lc: LogContext,\n parent: EventEmitter,\n ...services: SingletonService[]\n): Promise<void> {\n if (services.length === 0) {\n return;\n }\n for (const signal of [...GRACEFUL_SHUTDOWN, ...FORCEFUL_SHUTDOWN]) {\n parent.once(signal, () => {\n const GRACEFUL_SIGNALS = GRACEFUL_SHUTDOWN as readonly NodeJS.Signals[];\n\n services.forEach(async svc => {\n if (GRACEFUL_SIGNALS.includes(signal) && svc.drain) {\n lc.info?.(`draining ${svc.constructor.name} ${svc.id} (${signal})`);\n await svc.drain();\n }\n lc.info?.(`stopping ${svc.constructor.name} ${svc.id} (${signal})`);\n await svc.stop();\n });\n });\n }\n\n try {\n // Run all services and resolve when any of them stops.\n const svc = await Promise.race(\n services.map(svc => svc.run().then(() => svc)),\n );\n lc.info?.(`${svc.constructor.name} (${svc.id}) stopped`);\n } catch (e) {\n lc.error?.(`exiting on error`, e);\n throw e;\n }\n}\n\nexport async function exitAfter(run: () => Promise<void>) {\n try {\n await run();\n // oxlint-disable-next-line no-console\n console.info(`pid ${pid} exiting normally`);\n process.exit(0);\n } catch (e) {\n // oxlint-disable-next-line no-console\n console.error(`pid ${pid} exiting with error`, e);\n process.exit(-1);\n }\n}\n\nconst DEFAULT_STOP_INTERVAL_MS = 20_000;\n\n/**\n * The HeartbeatMonitor monitors the cadence heartbeats (e.g. \"/keepalive\"\n * health checks made to HttpServices) that signal that the server\n * should continue processing requests. When a configurable `stopInterval`\n * elapses without receiving these heartbeats, the monitor initiates a\n * graceful shutdown of the server. This works with common load balancing\n * frameworks such as AWS Elastic Load Balancing.\n *\n * The HeartbeatMonitor is **opt-in** in that it only kicks in after it\n * starts receiving keepalives.\n */\nexport class HeartbeatMonitor {\n readonly #stopInterval: number;\n\n #lc: LogContext;\n #checkIntervalTimer: NodeJS.Timeout | undefined;\n #checkImmediateTimer: NodeJS.Immediate | undefined;\n #lastHeartbeat = 0;\n\n constructor(lc: LogContext, stopInterval = DEFAULT_STOP_INTERVAL_MS) {\n this.#lc = lc;\n this.#stopInterval = stopInterval;\n }\n\n onHeartbeat(reqHeaders: IncomingHttpHeaders) {\n this.#lastHeartbeat = Date.now();\n if (this.#checkIntervalTimer === undefined) {\n this.#lc.info?.(\n `starting heartbeat monitor at ${\n this.#stopInterval / 1000\n } second interval`,\n reqHeaders,\n );\n // e.g. check every 5 seconds to see if it's been over 20 seconds\n // since the last heartbeat.\n this.#checkIntervalTimer = setInterval(\n this.#checkStopInterval,\n this.#stopInterval / 4,\n );\n }\n }\n\n #checkStopInterval = () => {\n // In the Node.js event loop, timers like setInterval and setTimeout\n // run *before* I/O events coming from network sockets or file reads/writes.\n // When this process gets starved of CPU resources for long periods of time,\n // for example when other processes are monopolizing all available cores,\n // pathological behavior can emerge:\n // - keepalive network request comes in, but is queued in Node internals waiting\n // for time on the event loop\n // - CPU is starved/monopolized by other processes for longer than the time\n // configured via this.#stopInterval\n // - When CPU becomes available and the event loop wakes up, this stop interval\n // check is run *before* the keepalive request is processed. The value of\n // this.#lastHeartbeat is now very stale, and erroneously triggers a shutdown\n // even though keepalive requests were about to be processed and update\n // this.#lastHeartbeat. Downtime ensues.\n //\n // To avoid this, we push the check out to a phase of the event loop *after*\n // I/O events are processed, using setImmediate():\n // https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#setimmediate-vs-settimeout\n //\n // This ensures we see a value for this.#lastHeartbeat that reflects\n // any keepalive requests that came in during the current event loop turn.\n this.#checkImmediateTimer = setImmediate(() => {\n this.#checkImmediateTimer = undefined;\n const timeSinceLastHeartbeat = Date.now() - this.#lastHeartbeat;\n if (timeSinceLastHeartbeat >= this.#stopInterval) {\n this.#lc.info?.(\n `last heartbeat received ${\n timeSinceLastHeartbeat / 1000\n } seconds ago. draining.`,\n );\n process.kill(process.pid, GRACEFUL_SHUTDOWN[0]);\n }\n });\n };\n\n stop() {\n clearTimeout(this.#checkIntervalTimer);\n if (this.#checkImmediateTimer) {\n clearImmediate(this.#checkImmediateTimer);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"life-cycle.js","names":["#lc","#userFacing","#all","#exitImpl","#start","#ready","#startDrain","#kill","#exit","#runningState","#drainStart","#onExit","#initializing","#nextID","#stopInterval","#lastHeartbeat","#checkIntervalTimer","#checkStopInterval","#checkImmediateTimer"],"sources":["../../../../../zero-cache/src/services/life-cycle.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport type {IncomingHttpHeaders} from 'node:http';\nimport {pid} from 'node:process';\nimport type {EventEmitter} from 'stream';\nimport {\n singleProcessMode,\n type Subprocess,\n type Worker,\n} from '../types/processes.ts';\nimport {RunningState} from './running-state.ts';\nimport type {SingletonService} from './service.ts';\n\n/**\n * * `user-facing` workers serve external requests and are the first to\n * receive a `SIGTERM` or `SIGINT` signal for graceful shutdown.\n *\n * * `supporting` workers support `user-facing` workers and are sent\n * the `SIGTERM` signal only after all `user-facing` workers have\n * exited.\n *\n * For other kill signals, such as `SIGQUIT`, all workers\n * are stopped without draining. Additionally, if any worker exits\n * unexpectedly, all workers sent an immediate `SIGQUIT` signal.\n */\nexport type WorkerType = 'user-facing' | 'supporting';\n\nexport const GRACEFUL_SHUTDOWN = ['SIGTERM', 'SIGINT'] as const;\nexport const FORCEFUL_SHUTDOWN = ['SIGQUIT'] as const;\n\n/**\n * Handles readiness, termination signals, and coordination of graceful\n * shutdown.\n */\nexport class ProcessManager {\n readonly #lc: LogContext;\n readonly #userFacing = new Set<Subprocess>();\n readonly #all = new Set<Subprocess>();\n readonly #exitImpl: (code: number) => never;\n readonly #start = Date.now();\n readonly #ready: Promise<void>[] = [];\n\n #runningState = new RunningState('process-manager');\n #drainStart = 0;\n\n constructor(lc: LogContext, proc: EventEmitter) {\n this.#lc = lc.withContext('component', 'process-manager');\n\n // Propagate `SIGTERM` and `SIGINT` to all user-facing workers,\n // initiating a graceful shutdown. The parent process will\n // exit once all user-facing workers have exited ...\n for (const signal of GRACEFUL_SHUTDOWN) {\n proc.on(signal, () => this.#startDrain(signal));\n }\n\n // ... which will result in sending `SIGTERM` to the remaining workers.\n proc.on('exit', code =>\n this.#kill(\n this.#all,\n code === 0 ? GRACEFUL_SHUTDOWN[0] : FORCEFUL_SHUTDOWN[0],\n ),\n );\n\n // For other (catchable) kill signals, exit with a non-zero error code\n // to send a `SIGQUIT` to all workers. For this signal, workers are\n // stopped immediately without draining. See `runUntilKilled()`.\n for (const signal of FORCEFUL_SHUTDOWN) {\n proc.on(signal, () => this.#exit(-1));\n }\n\n this.#exitImpl = (code: number) => {\n if (singleProcessMode()) {\n return proc.emit('exit', code) as never; // For unit / integration tests.\n }\n process.exit(code);\n };\n }\n\n done() {\n return this.#runningState.stopped();\n }\n\n #exit(code: number) {\n this.#lc.info?.('exiting with code', code);\n this.#runningState.stop(this.#lc);\n void this.#lc.flush().finally(() => this.#exitImpl(code));\n }\n\n #startDrain(signal: 'SIGTERM' | 'SIGINT' = 'SIGTERM') {\n this.#lc.info?.(`initiating drain (${signal})`);\n this.#drainStart = Date.now();\n if (this.#userFacing.size) {\n this.#kill(this.#userFacing, signal);\n } else {\n this.#kill(this.#all, signal);\n }\n }\n\n addSubprocess(proc: Subprocess, type: WorkerType, name: string) {\n if (type === 'user-facing') {\n this.#userFacing.add(proc);\n }\n this.#all.add(proc);\n\n let isOpen = true;\n proc.on('close', (code, signal) => {\n isOpen = false;\n this.#onExit(code, signal, null, type, name, proc);\n });\n\n // As per https://nodejs.org/api/child_process.html#event-error\n // 'error' events can happen when sending a message to a child process\n // fails. This is not really an error when the server is shutting down,\n // so log any post-close errors at 'warn'.\n proc.on('error', err =>\n this.#lc[isOpen ? 'error' : 'warn']?.(\n `error from ${name} ${proc.pid}`,\n err,\n ),\n );\n }\n\n readonly #initializing = new Map<number, string>();\n #nextID = 0;\n\n addWorker(worker: Worker, type: WorkerType, name: string): Worker {\n this.addSubprocess(worker, type, name);\n\n const id = ++this.#nextID;\n this.#initializing.set(id, name);\n const {promise, resolve} = resolver();\n this.#ready.push(promise);\n\n worker.onceMessageType('ready', () => {\n this.#lc.debug?.(`${name} ready (${Date.now() - this.#start} ms)`);\n this.#initializing.delete(id);\n resolve();\n });\n\n return worker;\n }\n\n initializing(): string[] {\n return [...this.#initializing.values()];\n }\n\n async allWorkersReady() {\n await Promise.all(this.#ready);\n }\n\n logErrorAndExit(err: unknown, name: string) {\n // only accessible by the main (i.e. user-facing) process.\n this.#onExit(-1, null, err, 'user-facing', name, undefined);\n }\n\n #onExit(\n code: number,\n sig: NodeJS.Signals | null,\n err: unknown | null,\n type: WorkerType,\n name: string,\n worker: Subprocess | undefined,\n ) {\n // Remove the worker from maps to avoid attempting to send more signals to it.\n if (worker) {\n this.#userFacing.delete(worker);\n this.#all.delete(worker);\n }\n\n const pid = worker?.pid ?? process.pid;\n\n if (type === 'supporting') {\n // The replication-manager has no user-facing workers.\n // In this case, code === 0 shutdowns are not errors.\n // Non-zero exits are warnings (not errors) since they're often transient issues.\n const log = code === 0 && this.#userFacing.size === 0 ? 'info' : 'warn';\n this.#lc[log]?.(`${name} (${pid}) exited with code (${code})`, err ?? '');\n return this.#exit(log === 'info' ? code : -1);\n }\n\n const log = this.#drainStart === 0 ? 'error' : 'warn';\n if (sig) {\n this.#lc[log]?.(`${name} (${pid}) killed with (${sig})`, err ?? '');\n } else if (code !== 0) {\n this.#lc[log]?.(`${name} (${pid}) exited with code (${code})`, err ?? '');\n } else {\n this.#lc.info?.(`${name} (${pid}) exited with code (${code})`);\n }\n\n // user-facing workers exited or finished draining.\n if (this.#userFacing.size === 0) {\n this.#lc.info?.(\n this.#drainStart\n ? `all user-facing workers drained (${\n Date.now() - this.#drainStart\n } ms)`\n : `all user-facing workers exited`,\n );\n return this.#exit(0);\n }\n\n // Exit only if not draining. If a user-facing worker exits unexpectedly\n // during a drain, log a warning but let other user-facing workers drain.\n if (log === 'error') {\n return this.#exit(code || -1);\n }\n\n return undefined;\n }\n\n #kill(workers: Iterable<Subprocess>, signal: NodeJS.Signals) {\n for (const worker of workers) {\n try {\n worker.kill(signal);\n } catch (e) {\n this.#lc.error?.(e);\n }\n }\n }\n}\n\n/**\n * Runs the specified services, stopping them on `SIGTERM` or `SIGINT` with\n * an optional {@link SingletonService.drain drain()}, or stopping them\n * without draining for `SIGQUIT`.\n *\n * @returns a Promise that resolves/rejects when any of the services stops/throws.\n */\n\nexport async function runUntilKilled(\n lc: LogContext,\n parent: EventEmitter,\n ...services: SingletonService[]\n): Promise<void> {\n if (services.length === 0) {\n return;\n }\n for (const signal of [...GRACEFUL_SHUTDOWN, ...FORCEFUL_SHUTDOWN]) {\n parent.once(signal, () => {\n const GRACEFUL_SIGNALS = GRACEFUL_SHUTDOWN as readonly NodeJS.Signals[];\n\n services.forEach(async svc => {\n if (GRACEFUL_SIGNALS.includes(signal) && svc.drain) {\n lc.info?.(`draining ${svc.constructor.name} ${svc.id} (${signal})`);\n await svc.drain();\n }\n lc.info?.(`stopping ${svc.constructor.name} ${svc.id} (${signal})`);\n await svc.stop();\n });\n });\n }\n\n try {\n // Run all services and resolve when any of them stops.\n const svc = await Promise.race(\n services.map(svc => svc.run().then(() => svc)),\n );\n lc.info?.(`${svc.constructor.name} (${svc.id}) stopped`);\n } catch (e) {\n lc.error?.(`exiting on error`, e);\n throw e;\n }\n}\n\nexport async function exitAfter(run: () => Promise<void>) {\n try {\n await run();\n // oxlint-disable-next-line no-console\n console.info(`pid ${pid} exiting normally`);\n process.exit(0);\n } catch (e) {\n // oxlint-disable-next-line no-console\n console.error(`pid ${pid} exiting with error`, e);\n process.exit(-1);\n }\n}\n\nconst DEFAULT_STOP_INTERVAL_MS = 20_000;\n\n/**\n * The HeartbeatMonitor monitors the cadence heartbeats (e.g. \"/keepalive\"\n * health checks made to HttpServices) that signal that the server\n * should continue processing requests. When a configurable `stopInterval`\n * elapses without receiving these heartbeats, the monitor initiates a\n * graceful shutdown of the server. This works with common load balancing\n * frameworks such as AWS Elastic Load Balancing.\n *\n * The HeartbeatMonitor is **opt-in** in that it only kicks in after it\n * starts receiving keepalives.\n */\nexport class HeartbeatMonitor {\n readonly #stopInterval: number;\n\n #lc: LogContext;\n #checkIntervalTimer: NodeJS.Timeout | undefined;\n #checkImmediateTimer: NodeJS.Immediate | undefined;\n #lastHeartbeat = 0;\n\n constructor(lc: LogContext, stopInterval = DEFAULT_STOP_INTERVAL_MS) {\n this.#lc = lc;\n this.#stopInterval = stopInterval;\n }\n\n onHeartbeat(reqHeaders: IncomingHttpHeaders) {\n this.#lastHeartbeat = Date.now();\n if (this.#checkIntervalTimer === undefined) {\n this.#lc.info?.(\n `starting heartbeat monitor at ${\n this.#stopInterval / 1000\n } second interval`,\n reqHeaders,\n );\n // e.g. check every 5 seconds to see if it's been over 20 seconds\n // since the last heartbeat.\n this.#checkIntervalTimer = setInterval(\n this.#checkStopInterval,\n this.#stopInterval / 4,\n );\n }\n }\n\n #checkStopInterval = () => {\n // In the Node.js event loop, timers like setInterval and setTimeout\n // run *before* I/O events coming from network sockets or file reads/writes.\n // When this process gets starved of CPU resources for long periods of time,\n // for example when other processes are monopolizing all available cores,\n // pathological behavior can emerge:\n // - keepalive network request comes in, but is queued in Node internals waiting\n // for time on the event loop\n // - CPU is starved/monopolized by other processes for longer than the time\n // configured via this.#stopInterval\n // - When CPU becomes available and the event loop wakes up, this stop interval\n // check is run *before* the keepalive request is processed. The value of\n // this.#lastHeartbeat is now very stale, and erroneously triggers a shutdown\n // even though keepalive requests were about to be processed and update\n // this.#lastHeartbeat. Downtime ensues.\n //\n // To avoid this, we push the check out to a phase of the event loop *after*\n // I/O events are processed, using setImmediate():\n // https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#setimmediate-vs-settimeout\n //\n // This ensures we see a value for this.#lastHeartbeat that reflects\n // any keepalive requests that came in during the current event loop turn.\n this.#checkImmediateTimer = setImmediate(() => {\n this.#checkImmediateTimer = undefined;\n const timeSinceLastHeartbeat = Date.now() - this.#lastHeartbeat;\n if (timeSinceLastHeartbeat >= this.#stopInterval) {\n this.#lc.info?.(\n `last heartbeat received ${\n timeSinceLastHeartbeat / 1000\n } seconds ago. draining.`,\n );\n process.kill(process.pid, GRACEFUL_SHUTDOWN[0]);\n }\n });\n };\n\n stop() {\n clearTimeout(this.#checkIntervalTimer);\n if (this.#checkImmediateTimer) {\n clearImmediate(this.#checkImmediateTimer);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import { Database } from "../../../../zqlite/src/db.js";
|
|
|
4
4
|
import { getServerVersion, isAdminPasswordValid } from "../../config/zero-config.js";
|
|
5
5
|
import { StatementRunner } from "../../db/statements.js";
|
|
6
6
|
import { loadPermissions } from "../../auth/load-permissions.js";
|
|
7
|
-
import { _usingCtx } from "../../../../_virtual/_@oxc-project_runtime@0.
|
|
7
|
+
import { _usingCtx } from "../../../../_virtual/_@oxc-project_runtime@0.122.0/helpers/usingCtx.js";
|
|
8
8
|
import { analyzeQuery } from "../analyze.js";
|
|
9
9
|
//#region ../zero-cache/src/services/view-syncer/inspect-handler.ts
|
|
10
10
|
async function handleInspect(lc, body, cvr, client, inspectorDelegate, clientGroupID, cvrStore, config, headerOptions, userQueryURL, auth) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replicator.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/workers/replicator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,CAAC,MAAM,+BAA+B,CAAC;AACnD,OAAO,EAAC,QAAQ,EAAC,MAAM,2BAA2B,CAAC;AACnD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,0BAA0B,CAAC;AAG7D,OAAO,EAAC,QAAQ,EAAC,MAAM,oCAAoC,CAAC;AAC5D,OAAO,KAAK,EAEV,oBAAoB,EACpB,UAAU,EACX,MAAM,sCAAsC,CAAC;AAK9C,OAAO,EAEL,KAAK,YAAY,EAClB,MAAM,+CAA+C,CAAC;AACvD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,uBAAuB,CAAC;AAElD,eAAO,MAAM,qBAAqB,+CAIjC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,UAEzE;
|
|
1
|
+
{"version":3,"file":"replicator.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/workers/replicator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,CAAC,MAAM,+BAA+B,CAAC;AACnD,OAAO,EAAC,QAAQ,EAAC,MAAM,2BAA2B,CAAC;AACnD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,0BAA0B,CAAC;AAG7D,OAAO,EAAC,QAAQ,EAAC,MAAM,oCAAoC,CAAC;AAC5D,OAAO,KAAK,EAEV,oBAAoB,EACpB,UAAU,EACX,MAAM,sCAAsC,CAAC;AAK9C,OAAO,EAEL,KAAK,YAAY,EAClB,MAAM,+CAA+C,CAAC;AACvD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,uBAAuB,CAAC;AAElD,eAAO,MAAM,qBAAqB,+CAIjC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,UAEzE;AAuED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,YAAY,CAMnE;AAED,wBAAsB,YAAY,CAChC,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,eAAe,EACrB,cAAc,EAAE,cAAc,GAC7B,OAAO,CAAC,QAAQ,CAAC,CA8BnB;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,UAAU,EACd,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,MAAM,QAGf;AAID,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,UAAU,EACd,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,oBAAoB,QA8B/B;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,CAM5E;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,QAE1D"}
|
|
@@ -14,6 +14,7 @@ var MILLIS_PER_HOUR = 1e3 * 60 * 60;
|
|
|
14
14
|
var MB = 1024 * 1024;
|
|
15
15
|
async function connect(lc, { file, vacuumIntervalHours }, walMode, mode) {
|
|
16
16
|
const replica = new Database(lc, file);
|
|
17
|
+
replica.pragma("busy_timeout = 1000");
|
|
17
18
|
await upgradeReplica(lc, `${mode}-replica`, file);
|
|
18
19
|
replica.pragma("journal_mode = delete");
|
|
19
20
|
const [{ page_size: pageSize }] = replica.pragma("page_size");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replicator.js","names":[],"sources":["../../../../../zero-cache/src/workers/replicator.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport * as v from '../../../shared/src/valita.ts';\nimport {Database} from '../../../zqlite/src/db.ts';\nimport type {ReplicaOptions} from '../config/zero-config.ts';\nimport {deleteLiteDB} from '../db/delete-lite-db.ts';\nimport {upgradeReplica} from '../services/change-source/common/replica-schema.ts';\nimport {Notifier} from '../services/replicator/notifier.ts';\nimport type {\n ReplicaState,\n ReplicaStateNotifier,\n Replicator,\n} from '../services/replicator/replicator.ts';\nimport {\n getAscendingEvents,\n recordEvent,\n} from '../services/replicator/schema/replication-state.ts';\nimport {\n applyPragmas,\n type PragmaConfig,\n} from '../services/replicator/write-worker-client.ts';\nimport type {Worker} from '../types/processes.ts';\n\nexport const replicaFileModeSchema = v.literalUnion(\n 'serving',\n 'serving-copy',\n 'backup',\n);\n\nexport type ReplicaFileMode = v.Infer<typeof replicaFileModeSchema>;\n\nexport function replicaFileName(replicaFile: string, mode: ReplicaFileMode) {\n return mode === 'serving-copy' ? `${replicaFile}-serving-copy` : replicaFile;\n}\n\nconst MILLIS_PER_HOUR = 1000 * 60 * 60;\nconst MB = 1024 * 1024;\n\nasync function connect(\n lc: LogContext,\n {file, vacuumIntervalHours}: ReplicaOptions,\n walMode: 'wal' | 'wal2',\n mode: ReplicaFileMode,\n): Promise<Database> {\n const replica = new Database(lc, file);\n\n // Perform any upgrades to the replica in case the backup is an\n // earlier version.\n await upgradeReplica(lc, `${mode}-replica`, file);\n\n // Start by folding any (e.g. restored) WAL(2) files into the main db.\n replica.pragma('journal_mode = delete');\n\n const [{page_size: pageSize}] = replica.pragma<{page_size: number}>(\n 'page_size',\n );\n const [{page_count: pageCount}] = replica.pragma<{page_count: number}>(\n 'page_count',\n );\n const [{freelist_count: freelistCount}] = replica.pragma<{\n freelist_count: number;\n }>('freelist_count');\n\n const dbSize = ((pageCount * pageSize) / MB).toFixed(2);\n const freelistSize = ((freelistCount * pageSize) / MB).toFixed(2);\n\n // TODO: Consider adding a freelist size or ratio based vacuum trigger.\n lc.info?.(`Size of db ${file}: ${dbSize} MB (${freelistSize} MB freeable)`);\n\n // Check for the VACUUM threshold.\n const events = getAscendingEvents(replica);\n lc.debug?.(`Runtime events for db ${file}`, {events});\n if (vacuumIntervalHours !== undefined) {\n const millisSinceLastEvent =\n Date.now() - (events.at(-1)?.timestamp.getTime() ?? 0);\n if (millisSinceLastEvent / MILLIS_PER_HOUR > vacuumIntervalHours) {\n lc.info?.(`Performing maintenance cleanup on ${file}`);\n const t0 = performance.now();\n replica.unsafeMode(true);\n replica.pragma('journal_mode = OFF');\n replica.exec('VACUUM');\n recordEvent(replica, 'vacuum');\n replica.unsafeMode(false);\n const t1 = performance.now();\n lc.info?.(`VACUUM completed (${t1 - t0} ms)`);\n }\n }\n\n lc.info?.(`setting ${file} to ${walMode} mode`);\n replica.pragma(`journal_mode = ${walMode}`);\n\n const pragmas = getPragmaConfig(mode);\n applyPragmas(replica, pragmas);\n\n replica.pragma('optimize = 0x10002');\n lc.info?.(`optimized ${file}`);\n return replica;\n}\n\n/**\n * Returns the PragmaConfig for a given replica file mode.\n * This is used by both the main thread (setupReplica) and\n * the write worker thread to apply the same pragma settings.\n */\nexport function getPragmaConfig(mode: ReplicaFileMode): PragmaConfig {\n return {\n busyTimeout: 30000,\n analysisLimit: 1000,\n walAutocheckpoint: mode === 'backup' ? 0 : undefined,\n };\n}\n\nexport async function setupReplica(\n lc: LogContext,\n mode: ReplicaFileMode,\n replicaOptions: ReplicaOptions,\n): Promise<Database> {\n lc.info?.(`setting up ${mode} replica`);\n\n switch (mode) {\n case 'backup':\n return await connect(lc, replicaOptions, 'wal', mode);\n\n case 'serving-copy': {\n // In 'serving-copy' mode, the original file is being used for 'backup'\n // mode, so we make a copy for servicing sync requests.\n const {file} = replicaOptions;\n const copyLocation = replicaFileName(file, mode);\n deleteLiteDB(copyLocation);\n\n const start = Date.now();\n lc.info?.(`copying ${file} to ${copyLocation}`);\n const replica = new Database(lc, file);\n replica.prepare(`VACUUM INTO ?`).run(copyLocation);\n replica.close();\n lc.info?.(`finished copy (${Date.now() - start} ms)`);\n\n return connect(lc, {...replicaOptions, file: copyLocation}, 'wal2', mode);\n }\n\n case 'serving':\n return connect(lc, replicaOptions, 'wal2', mode);\n\n default:\n throw new Error(`Invalid ReplicaMode ${mode}`);\n }\n}\n\nexport function setUpMessageHandlers(\n lc: LogContext,\n replicator: Replicator,\n parent: Worker,\n) {\n handleSubscriptionsFrom(lc, parent, replicator);\n}\n\ntype Notification = ['notify', ReplicaState];\n\nexport function handleSubscriptionsFrom(\n lc: LogContext,\n subscriber: Worker,\n notifier: ReplicaStateNotifier,\n) {\n subscriber.onMessageType('subscribe', async () => {\n const subscription = notifier.subscribe();\n\n subscriber.on('close', () => {\n lc.debug?.(`closing replication subscription from ${subscriber.pid}`);\n subscription.cancel();\n });\n\n for await (const msg of subscription) {\n try {\n subscriber.send<Notification>(['notify', msg]);\n } catch (e) {\n const log =\n e instanceof Error &&\n 'code' in e &&\n // This can happen in a race condition if the subscribing process\n // is closed before the 'close' message is processed.\n e.code === 'ERR_IPC_CHANNEL_CLOSED'\n ? 'warn'\n : 'error';\n\n lc[log]?.(\n `error sending replicator notification to ${subscriber.pid}: ${String(e)}`,\n e,\n );\n }\n }\n });\n}\n\n/**\n * Creates a Notifier to relay notifications the notifier of another Worker.\n * This does not send the initial subscription message. Use {@link subscribeTo}\n * to initiate the subscription.\n */\nexport function createNotifierFrom(_lc: LogContext, source: Worker): Notifier {\n const notifier = new Notifier();\n source.onMessageType<Notification>('notify', msg =>\n notifier.notifySubscribers(msg),\n );\n return notifier;\n}\n\nexport function subscribeTo(_lc: LogContext, source: Worker) {\n source.send(['subscribe', {}]);\n}\n"],"mappings":";;;;;;;;AAsBA,IAAa,wBAAwB,aACnC,WACA,gBACA,SACD;AAID,SAAgB,gBAAgB,aAAqB,MAAuB;AAC1E,QAAO,SAAS,iBAAiB,GAAG,YAAY,iBAAiB;;AAGnE,IAAM,kBAAkB,MAAO,KAAK;AACpC,IAAM,KAAK,OAAO;AAElB,eAAe,QACb,IACA,EAAC,MAAM,uBACP,SACA,MACmB;CACnB,MAAM,UAAU,IAAI,SAAS,IAAI,KAAK;
|
|
1
|
+
{"version":3,"file":"replicator.js","names":[],"sources":["../../../../../zero-cache/src/workers/replicator.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport * as v from '../../../shared/src/valita.ts';\nimport {Database} from '../../../zqlite/src/db.ts';\nimport type {ReplicaOptions} from '../config/zero-config.ts';\nimport {deleteLiteDB} from '../db/delete-lite-db.ts';\nimport {upgradeReplica} from '../services/change-source/common/replica-schema.ts';\nimport {Notifier} from '../services/replicator/notifier.ts';\nimport type {\n ReplicaState,\n ReplicaStateNotifier,\n Replicator,\n} from '../services/replicator/replicator.ts';\nimport {\n getAscendingEvents,\n recordEvent,\n} from '../services/replicator/schema/replication-state.ts';\nimport {\n applyPragmas,\n type PragmaConfig,\n} from '../services/replicator/write-worker-client.ts';\nimport type {Worker} from '../types/processes.ts';\n\nexport const replicaFileModeSchema = v.literalUnion(\n 'serving',\n 'serving-copy',\n 'backup',\n);\n\nexport type ReplicaFileMode = v.Infer<typeof replicaFileModeSchema>;\n\nexport function replicaFileName(replicaFile: string, mode: ReplicaFileMode) {\n return mode === 'serving-copy' ? `${replicaFile}-serving-copy` : replicaFile;\n}\n\nconst MILLIS_PER_HOUR = 1000 * 60 * 60;\nconst MB = 1024 * 1024;\n\nasync function connect(\n lc: LogContext,\n {file, vacuumIntervalHours}: ReplicaOptions,\n walMode: 'wal' | 'wal2',\n mode: ReplicaFileMode,\n): Promise<Database> {\n const replica = new Database(lc, file);\n\n // To allow other readers (e.g. the change-streamer) to access the replica\n // for stats / event publishing, allow a short busy_timeout for performing\n // locking operations.\n replica.pragma('busy_timeout = 1000');\n\n // Perform any upgrades to the replica in case the backup is an\n // earlier version.\n await upgradeReplica(lc, `${mode}-replica`, file);\n\n // Start by folding any (e.g. restored) WAL(2) files into the main db.\n replica.pragma('journal_mode = delete');\n\n const [{page_size: pageSize}] = replica.pragma<{page_size: number}>(\n 'page_size',\n );\n const [{page_count: pageCount}] = replica.pragma<{page_count: number}>(\n 'page_count',\n );\n const [{freelist_count: freelistCount}] = replica.pragma<{\n freelist_count: number;\n }>('freelist_count');\n\n const dbSize = ((pageCount * pageSize) / MB).toFixed(2);\n const freelistSize = ((freelistCount * pageSize) / MB).toFixed(2);\n\n // TODO: Consider adding a freelist size or ratio based vacuum trigger.\n lc.info?.(`Size of db ${file}: ${dbSize} MB (${freelistSize} MB freeable)`);\n\n // Check for the VACUUM threshold.\n const events = getAscendingEvents(replica);\n lc.debug?.(`Runtime events for db ${file}`, {events});\n if (vacuumIntervalHours !== undefined) {\n const millisSinceLastEvent =\n Date.now() - (events.at(-1)?.timestamp.getTime() ?? 0);\n if (millisSinceLastEvent / MILLIS_PER_HOUR > vacuumIntervalHours) {\n lc.info?.(`Performing maintenance cleanup on ${file}`);\n const t0 = performance.now();\n replica.unsafeMode(true);\n replica.pragma('journal_mode = OFF');\n replica.exec('VACUUM');\n recordEvent(replica, 'vacuum');\n replica.unsafeMode(false);\n const t1 = performance.now();\n lc.info?.(`VACUUM completed (${t1 - t0} ms)`);\n }\n }\n\n lc.info?.(`setting ${file} to ${walMode} mode`);\n replica.pragma(`journal_mode = ${walMode}`);\n\n const pragmas = getPragmaConfig(mode);\n applyPragmas(replica, pragmas);\n\n replica.pragma('optimize = 0x10002');\n lc.info?.(`optimized ${file}`);\n return replica;\n}\n\n/**\n * Returns the PragmaConfig for a given replica file mode.\n * This is used by both the main thread (setupReplica) and\n * the write worker thread to apply the same pragma settings.\n */\nexport function getPragmaConfig(mode: ReplicaFileMode): PragmaConfig {\n return {\n busyTimeout: 30000,\n analysisLimit: 1000,\n walAutocheckpoint: mode === 'backup' ? 0 : undefined,\n };\n}\n\nexport async function setupReplica(\n lc: LogContext,\n mode: ReplicaFileMode,\n replicaOptions: ReplicaOptions,\n): Promise<Database> {\n lc.info?.(`setting up ${mode} replica`);\n\n switch (mode) {\n case 'backup':\n return await connect(lc, replicaOptions, 'wal', mode);\n\n case 'serving-copy': {\n // In 'serving-copy' mode, the original file is being used for 'backup'\n // mode, so we make a copy for servicing sync requests.\n const {file} = replicaOptions;\n const copyLocation = replicaFileName(file, mode);\n deleteLiteDB(copyLocation);\n\n const start = Date.now();\n lc.info?.(`copying ${file} to ${copyLocation}`);\n const replica = new Database(lc, file);\n replica.prepare(`VACUUM INTO ?`).run(copyLocation);\n replica.close();\n lc.info?.(`finished copy (${Date.now() - start} ms)`);\n\n return connect(lc, {...replicaOptions, file: copyLocation}, 'wal2', mode);\n }\n\n case 'serving':\n return connect(lc, replicaOptions, 'wal2', mode);\n\n default:\n throw new Error(`Invalid ReplicaMode ${mode}`);\n }\n}\n\nexport function setUpMessageHandlers(\n lc: LogContext,\n replicator: Replicator,\n parent: Worker,\n) {\n handleSubscriptionsFrom(lc, parent, replicator);\n}\n\ntype Notification = ['notify', ReplicaState];\n\nexport function handleSubscriptionsFrom(\n lc: LogContext,\n subscriber: Worker,\n notifier: ReplicaStateNotifier,\n) {\n subscriber.onMessageType('subscribe', async () => {\n const subscription = notifier.subscribe();\n\n subscriber.on('close', () => {\n lc.debug?.(`closing replication subscription from ${subscriber.pid}`);\n subscription.cancel();\n });\n\n for await (const msg of subscription) {\n try {\n subscriber.send<Notification>(['notify', msg]);\n } catch (e) {\n const log =\n e instanceof Error &&\n 'code' in e &&\n // This can happen in a race condition if the subscribing process\n // is closed before the 'close' message is processed.\n e.code === 'ERR_IPC_CHANNEL_CLOSED'\n ? 'warn'\n : 'error';\n\n lc[log]?.(\n `error sending replicator notification to ${subscriber.pid}: ${String(e)}`,\n e,\n );\n }\n }\n });\n}\n\n/**\n * Creates a Notifier to relay notifications the notifier of another Worker.\n * This does not send the initial subscription message. Use {@link subscribeTo}\n * to initiate the subscription.\n */\nexport function createNotifierFrom(_lc: LogContext, source: Worker): Notifier {\n const notifier = new Notifier();\n source.onMessageType<Notification>('notify', msg =>\n notifier.notifySubscribers(msg),\n );\n return notifier;\n}\n\nexport function subscribeTo(_lc: LogContext, source: Worker) {\n source.send(['subscribe', {}]);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../../../zql/src/builder/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,6BAA6B,CAAC;AAE3D,OAAO,KAAK,EACV,GAAG,EAGH,SAAS,EAIT,WAAW,EAEX,QAAQ,EAIT,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,oCAAoC,CAAC;AAC5D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,2CAA2C,CAAC;AAI1E,OAAO,EAEL,KAAK,WAAW,EACjB,MAAM,4BAA4B,CAAC;AAIpC,OAAO,KAAK,EAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAElE,OAAO,KAAK,EAAC,MAAM,EAAE,WAAW,EAAC,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../../../zql/src/builder/builder.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,6 +10,7 @@ import { Join } from "../ivm/join.js";
|
|
|
10
10
|
import { Skip } from "../ivm/skip.js";
|
|
11
11
|
import { completeOrdering } from "../query/complete-ordering.js";
|
|
12
12
|
import { Take } from "../ivm/take.js";
|
|
13
|
+
import { Cap } from "../ivm/cap.js";
|
|
13
14
|
import { UnionFanIn } from "../ivm/union-fan-in.js";
|
|
14
15
|
import { UnionFanOut } from "../ivm/union-fan-out.js";
|
|
15
16
|
import { planQuery } from "../planner/planner-builder.js";
|
|
@@ -112,7 +113,7 @@ function assertNoNotExists(condition) {
|
|
|
112
113
|
default: unreachable(condition);
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
|
-
function buildPipelineInternal(ast, delegate, queryID, name, partitionKey) {
|
|
116
|
+
function buildPipelineInternal(ast, delegate, queryID, name, partitionKey, isNonFlippedExistsChild) {
|
|
116
117
|
const source = delegate.getSource(ast.table);
|
|
117
118
|
if (!source) throw new Error(`Source not found: ${ast.table}`);
|
|
118
119
|
ast = uniquifyCorrelatedSubqueryConditionAliases(ast);
|
|
@@ -125,7 +126,11 @@ function buildPipelineInternal(ast, delegate, queryID, name, partitionKey) {
|
|
|
125
126
|
for (const key of csq.related.correlation.parentField) splitEditKeys.add(key);
|
|
126
127
|
}
|
|
127
128
|
if (ast.related) for (const csq of ast.related) for (const key of csq.correlation.parentField) splitEditKeys.add(key);
|
|
128
|
-
|
|
129
|
+
if (isNonFlippedExistsChild) {
|
|
130
|
+
assert(ast.start === void 0, "EXISTS subqueries must not have start");
|
|
131
|
+
assert(ast.related === void 0, "EXISTS subqueries must not have related");
|
|
132
|
+
}
|
|
133
|
+
const conn = source.connect(isNonFlippedExistsChild ? void 0 : must(ast.orderBy), ast.where, splitEditKeys, delegate.debug);
|
|
129
134
|
let end = delegate.decorateSourceInput(conn, queryID);
|
|
130
135
|
end = delegate.decorateInput(end, `${name}:source(${ast.table})`);
|
|
131
136
|
const { fullyAppliedFilters } = conn;
|
|
@@ -142,7 +147,12 @@ function buildPipelineInternal(ast, delegate, queryID, name, partitionKey) {
|
|
|
142
147
|
}
|
|
143
148
|
}, delegate, queryID, end, name, true);
|
|
144
149
|
if (ast.where && (!fullyAppliedFilters || delegate.applyFiltersAnyway)) end = applyWhere(end, ast.where, delegate, name);
|
|
145
|
-
if (ast.limit !== void 0) {
|
|
150
|
+
if (ast.limit !== void 0) if (isNonFlippedExistsChild) {
|
|
151
|
+
const capName = `${name}:cap`;
|
|
152
|
+
const cap = new Cap(end, delegate.createStorage(capName), ast.limit, partitionKey);
|
|
153
|
+
delegate.addEdge(end, cap);
|
|
154
|
+
end = delegate.decorateInput(cap, capName);
|
|
155
|
+
} else {
|
|
146
156
|
const takeName = `${name}:take`;
|
|
147
157
|
const take = new Take(end, delegate.createStorage(takeName), ast.limit, partitionKey);
|
|
148
158
|
delegate.addEdge(end, take);
|
|
@@ -192,7 +202,7 @@ function applyFilterWithFlips(input, condition, delegate, name) {
|
|
|
192
202
|
}
|
|
193
203
|
case "correlatedSubquery": {
|
|
194
204
|
const sq = condition.related;
|
|
195
|
-
const child = buildPipelineInternal(sq.subquery, delegate, "", `${name}.${sq.subquery.alias}`, sq.correlation.childField);
|
|
205
|
+
const child = buildPipelineInternal(sq.subquery, delegate, "", `${name}.${sq.subquery.alias}`, sq.correlation.childField, false);
|
|
196
206
|
const flippedJoin = new FlippedJoin({
|
|
197
207
|
parent: end,
|
|
198
208
|
child,
|
|
@@ -275,7 +285,7 @@ function valuePosName(left) {
|
|
|
275
285
|
function applyCorrelatedSubQuery(sq, delegate, queryID, end, name, fromCondition) {
|
|
276
286
|
if (sq.subquery.limit === 0 && fromCondition) return end;
|
|
277
287
|
assert(sq.subquery.alias, "Subquery must have an alias");
|
|
278
|
-
const child = buildPipelineInternal(sq.subquery, delegate, queryID, `${name}.${sq.subquery.alias}`, sq.correlation.childField);
|
|
288
|
+
const child = buildPipelineInternal(sq.subquery, delegate, queryID, `${name}.${sq.subquery.alias}`, sq.correlation.childField, fromCondition);
|
|
279
289
|
const joinName = `${name}:join(${sq.subquery.alias})`;
|
|
280
290
|
const join = new Join({
|
|
281
291
|
parent: end,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder.js","names":[],"sources":["../../../../../zql/src/builder/builder.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert, unreachable} from '../../../shared/src/asserts.ts';\nimport type {JSONValue} from '../../../shared/src/json.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport type {\n AST,\n ColumnReference,\n CompoundKey,\n Condition,\n Conjunction,\n CorrelatedSubquery,\n CorrelatedSubqueryCondition,\n Disjunction,\n LiteralValue,\n Ordering,\n Parameter,\n SimpleCondition,\n ValuePosition,\n} from '../../../zero-protocol/src/ast.ts';\nimport type {Row} from '../../../zero-protocol/src/data.ts';\nimport type {PrimaryKey} from '../../../zero-protocol/src/primary-key.ts';\nimport {Exists} from '../ivm/exists.ts';\nimport {FanIn} from '../ivm/fan-in.ts';\nimport {FanOut} from '../ivm/fan-out.ts';\nimport {\n buildFilterPipeline,\n type FilterInput,\n} from '../ivm/filter-operators.ts';\nimport {Filter} from '../ivm/filter.ts';\nimport {FlippedJoin} from '../ivm/flipped-join.ts';\nimport {Join} from '../ivm/join.ts';\nimport type {Input, InputBase, Storage} from '../ivm/operator.ts';\nimport {Skip} from '../ivm/skip.ts';\nimport type {Source, SourceInput} from '../ivm/source.ts';\nimport {Take} from '../ivm/take.ts';\nimport {UnionFanIn} from '../ivm/union-fan-in.ts';\nimport {UnionFanOut} from '../ivm/union-fan-out.ts';\nimport {planQuery} from '../planner/planner-builder.ts';\nimport type {ConnectionCostModel} from '../planner/planner-connection.ts';\nimport {completeOrdering} from '../query/complete-ordering.ts';\nimport type {PlanDebugger} from '../planner/planner-debug.ts';\nimport type {DebugDelegate} from './debug-delegate.ts';\nimport {createPredicate, type NoSubqueryCondition} from './filter.ts';\n\nexport type StaticQueryParameters = {\n authData: Record<string, JSONValue>;\n preMutationRow?: Row | undefined;\n};\n\n/**\n * Interface required of caller to buildPipeline. Connects to constructed\n * pipeline to delegate environment to provide sources and storage.\n */\nexport interface BuilderDelegate {\n readonly applyFiltersAnyway?: boolean | undefined;\n debug?: DebugDelegate | undefined;\n\n /**\n * When true, allows NOT EXISTS conditions in queries.\n * Defaults to false.\n *\n * We only set this to true on the server.\n * The client-side query engine cannot support NOT EXISTS because:\n * 1. Zero only syncs a subset of data to the client\n * 2. On the client, we can't distinguish between a row not existing vs.\n * a row not being synced to the client\n * 3. NOT EXISTS requires complete knowledge of what doesn't exist\n */\n readonly enableNotExists?: boolean | undefined;\n\n /**\n * Called once for each source needed by the AST.\n * Might be called multiple times with same tableName. It is OK to return\n * same storage instance in that case.\n */\n getSource(tableName: string): Source | undefined;\n\n /**\n * Called once for each operator that requires storage. Should return a new\n * unique storage object for each call.\n */\n createStorage(name: string): Storage;\n\n decorateInput(input: Input, name: string): Input;\n\n addEdge(source: InputBase, dest: InputBase): void;\n\n decorateFilterInput(input: FilterInput, name: string): FilterInput;\n\n decorateSourceInput(input: SourceInput, queryID: string): Input;\n\n /**\n * The AST is mapped on-the-wire between client and server names.\n *\n * There is no \"wire\" for zqlite tests so this function is provided\n * to allow tests to remap the AST.\n */\n mapAst?: ((ast: AST) => AST) | undefined;\n}\n\n/**\n * Builds a pipeline from an AST. Caller must provide a delegate to create source\n * and storage interfaces as necessary.\n *\n * Usage:\n *\n * ```ts\n * class MySink implements Output {\n * readonly #input: Input;\n *\n * constructor(input: Input) {\n * this.#input = input;\n * input.setOutput(this);\n * }\n *\n * push(change: Change, _: Operator) {\n * console.log(change);\n * }\n * }\n *\n * const input = buildPipeline(ast, myDelegate, hash(ast));\n * const sink = new MySink(input);\n * ```\n */\nexport function buildPipeline(\n ast: AST,\n delegate: BuilderDelegate,\n queryID: string,\n costModel?: ConnectionCostModel,\n lc?: LogContext,\n planDebugger?: PlanDebugger,\n): Input {\n ast = delegate.mapAst ? delegate.mapAst(ast) : ast;\n ast = completeOrdering(\n ast,\n tableName => must(delegate.getSource(tableName)).tableSchema.primaryKey,\n );\n\n if (costModel) {\n ast = planQuery(ast, costModel, planDebugger, lc);\n }\n return buildPipelineInternal(ast, delegate, queryID, '');\n}\n\nexport function bindStaticParameters(\n ast: AST,\n staticQueryParameters: StaticQueryParameters | undefined,\n) {\n const visit = (node: AST): AST => ({\n ...node,\n where: node.where ? bindCondition(node.where) : undefined,\n related: node.related?.map(sq => ({\n ...sq,\n subquery: visit(sq.subquery),\n })),\n });\n\n function bindCondition(condition: Condition): Condition {\n if (condition.type === 'simple') {\n return {\n ...condition,\n left: bindValue(condition.left),\n right: bindValue(condition.right) as Exclude<\n ValuePosition,\n ColumnReference\n >,\n };\n }\n if (condition.type === 'correlatedSubquery') {\n return {\n ...condition,\n related: {\n ...condition.related,\n subquery: visit(condition.related.subquery),\n },\n };\n }\n\n return {\n ...condition,\n conditions: condition.conditions.map(bindCondition),\n };\n }\n\n const bindValue = (value: ValuePosition): ValuePosition => {\n if (isParameter(value)) {\n const anchor = must(\n staticQueryParameters,\n 'Static query params do not exist',\n )[value.anchor];\n const resolvedValue = resolveField(anchor, value.field);\n return {\n type: 'literal',\n value: resolvedValue as LiteralValue,\n };\n }\n return value;\n };\n\n return visit(ast);\n}\n\nfunction resolveField(\n anchor: Record<string, JSONValue> | Row | undefined,\n field: string | string[],\n): unknown {\n if (anchor === undefined) {\n return null;\n }\n\n if (Array.isArray(field)) {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n return field.reduce((acc, f) => (acc as any)?.[f], anchor) ?? null;\n }\n\n return anchor[field] ?? null;\n}\n\nfunction isParameter(value: ValuePosition): value is Parameter {\n return value.type === 'static';\n}\n\nconst EXISTS_LIMIT = 3;\nconst PERMISSIONS_EXISTS_LIMIT = 1;\n\n/**\n * Checks if a condition tree contains any NOT EXISTS operations.\n * Recursively checks AND/OR branches but does not recurse into nested subqueries\n * (those are checked when buildPipelineInternal processes them).\n */\nexport function assertNoNotExists(condition: Condition): void {\n switch (condition.type) {\n case 'simple':\n return;\n\n case 'correlatedSubquery':\n if (condition.op === 'NOT EXISTS') {\n throw new Error(\n 'not(exists()) is not supported on the client - see https://bugs.rocicorp.dev/issue/3438',\n );\n }\n return;\n\n case 'and':\n case 'or':\n for (const c of condition.conditions) {\n assertNoNotExists(c);\n }\n return;\n default:\n unreachable(condition);\n }\n}\n\nfunction buildPipelineInternal(\n ast: AST,\n delegate: BuilderDelegate,\n queryID: string,\n name: string,\n partitionKey?: CompoundKey,\n): Input {\n const source = delegate.getSource(ast.table);\n if (!source) {\n throw new Error(`Source not found: ${ast.table}`);\n }\n\n ast = uniquifyCorrelatedSubqueryConditionAliases(ast);\n\n if (!delegate.enableNotExists && ast.where) {\n assertNoNotExists(ast.where);\n }\n\n const csqConditions = gatherCorrelatedSubqueryQueryConditions(ast.where);\n const splitEditKeys: Set<string> = partitionKey\n ? new Set(partitionKey)\n : new Set();\n const aliases = new Set<string>();\n for (const csq of csqConditions) {\n aliases.add(csq.related.subquery.alias || '');\n for (const key of csq.related.correlation.parentField) {\n splitEditKeys.add(key);\n }\n }\n if (ast.related) {\n for (const csq of ast.related) {\n for (const key of csq.correlation.parentField) {\n splitEditKeys.add(key);\n }\n }\n }\n const conn = source.connect(\n must(ast.orderBy),\n ast.where,\n splitEditKeys,\n delegate.debug,\n );\n\n let end: Input = delegate.decorateSourceInput(conn, queryID);\n end = delegate.decorateInput(end, `${name}:source(${ast.table})`);\n const {fullyAppliedFilters} = conn;\n\n if (ast.start) {\n const skip = new Skip(end, ast.start);\n delegate.addEdge(end, skip);\n end = delegate.decorateInput(skip, `${name}:skip)`);\n }\n\n for (const csqCondition of csqConditions) {\n // flipped EXISTS are handled in applyWhere\n if (!csqCondition.flip) {\n end = applyCorrelatedSubQuery(\n {\n ...csqCondition.related,\n subquery: {\n ...csqCondition.related.subquery,\n limit:\n csqCondition.related.system === 'permissions'\n ? PERMISSIONS_EXISTS_LIMIT\n : EXISTS_LIMIT,\n },\n },\n delegate,\n queryID,\n end,\n name,\n true,\n );\n }\n }\n\n if (ast.where && (!fullyAppliedFilters || delegate.applyFiltersAnyway)) {\n end = applyWhere(end, ast.where, delegate, name);\n }\n\n if (ast.limit !== undefined) {\n const takeName = `${name}:take`;\n const take = new Take(\n end,\n delegate.createStorage(takeName),\n ast.limit,\n partitionKey,\n );\n delegate.addEdge(end, take);\n end = delegate.decorateInput(take, takeName);\n }\n\n if (ast.related) {\n // Dedupe by alias - last one wins (LWW), like limit(5).limit(10)\n const byAlias = new Map<string, CorrelatedSubquery>();\n for (const csq of ast.related) {\n byAlias.set(csq.subquery.alias ?? '', csq);\n }\n for (const csq of byAlias.values()) {\n end = applyCorrelatedSubQuery(csq, delegate, queryID, end, name, false);\n }\n }\n\n return end;\n}\n\nfunction applyWhere(\n input: Input,\n condition: Condition,\n delegate: BuilderDelegate,\n name: string,\n): Input {\n if (!conditionIncludesFlippedSubqueryAtAnyLevel(condition)) {\n return buildFilterPipeline(input, delegate, filterInput =>\n applyFilter(filterInput, condition, delegate, name),\n );\n }\n\n return applyFilterWithFlips(input, condition, delegate, name);\n}\n\nfunction applyFilterWithFlips(\n input: Input,\n condition: Condition,\n delegate: BuilderDelegate,\n name: string,\n): Input {\n let end = input;\n assert(condition.type !== 'simple', 'Simple conditions cannot have flips');\n\n switch (condition.type) {\n case 'and': {\n const [withFlipped, withoutFlipped] = partitionBranches(\n condition.conditions,\n conditionIncludesFlippedSubqueryAtAnyLevel,\n );\n if (withoutFlipped.length > 0) {\n end = buildFilterPipeline(input, delegate, filterInput =>\n applyAnd(\n filterInput,\n {\n type: 'and',\n conditions: withoutFlipped,\n },\n delegate,\n name,\n ),\n );\n }\n assert(withFlipped.length > 0, 'Impossible to have no flips here');\n for (const cond of withFlipped) {\n end = applyFilterWithFlips(end, cond, delegate, name);\n }\n break;\n }\n case 'or': {\n const [withFlipped, withoutFlipped] = partitionBranches(\n condition.conditions,\n conditionIncludesFlippedSubqueryAtAnyLevel,\n );\n assert(withFlipped.length > 0, 'Impossible to have no flips here');\n\n const ufo = new UnionFanOut(end);\n delegate.addEdge(end, ufo);\n end = delegate.decorateInput(ufo, `${name}:ufo`);\n\n const branches: Input[] = [];\n if (withoutFlipped.length > 0) {\n branches.push(\n buildFilterPipeline(end, delegate, filterInput =>\n applyOr(\n filterInput,\n {\n type: 'or',\n conditions: withoutFlipped,\n },\n delegate,\n name,\n ),\n ),\n );\n }\n\n for (const cond of withFlipped) {\n branches.push(applyFilterWithFlips(end, cond, delegate, name));\n }\n\n const ufi = new UnionFanIn(ufo, branches);\n for (const branch of branches) {\n delegate.addEdge(branch, ufi);\n }\n end = delegate.decorateInput(ufi, `${name}:ufi`);\n\n break;\n }\n case 'correlatedSubquery': {\n const sq = condition.related;\n const child = buildPipelineInternal(\n sq.subquery,\n delegate,\n '',\n `${name}.${sq.subquery.alias}`,\n sq.correlation.childField,\n );\n const flippedJoin = new FlippedJoin({\n parent: end,\n child,\n parentKey: sq.correlation.parentField,\n childKey: sq.correlation.childField,\n relationshipName: must(\n sq.subquery.alias,\n 'Subquery must have an alias',\n ),\n hidden: sq.hidden ?? false,\n system: sq.system ?? 'client',\n });\n delegate.addEdge(end, flippedJoin);\n delegate.addEdge(child, flippedJoin);\n end = delegate.decorateInput(\n flippedJoin,\n `${name}:flipped-join(${sq.subquery.alias})`,\n );\n break;\n }\n }\n\n return end;\n}\n\nfunction applyFilter(\n input: FilterInput,\n condition: Condition,\n delegate: BuilderDelegate,\n name: string,\n): FilterInput {\n switch (condition.type) {\n case 'and':\n return applyAnd(input, condition, delegate, name);\n case 'or':\n return applyOr(input, condition, delegate, name);\n case 'correlatedSubquery':\n return applyCorrelatedSubqueryCondition(input, condition, delegate, name);\n case 'simple':\n return applySimpleCondition(input, delegate, condition);\n }\n}\n\nfunction applyAnd(\n input: FilterInput,\n condition: Conjunction,\n delegate: BuilderDelegate,\n name: string,\n): FilterInput {\n for (const subCondition of condition.conditions) {\n input = applyFilter(input, subCondition, delegate, name);\n }\n return input;\n}\n\nexport function applyOr(\n input: FilterInput,\n condition: Disjunction,\n delegate: BuilderDelegate,\n name: string,\n): FilterInput {\n const [subqueryConditions, otherConditions] =\n groupSubqueryConditions(condition);\n // if there are no subquery conditions, no fan-in / fan-out is needed\n if (subqueryConditions.length === 0) {\n const filter = new Filter(\n input,\n createPredicate({\n type: 'or',\n conditions: otherConditions,\n }),\n );\n delegate.addEdge(input, filter);\n return filter;\n }\n\n const fanOut = new FanOut(input);\n delegate.addEdge(input, fanOut);\n const branches = subqueryConditions.map(subCondition =>\n applyFilter(fanOut, subCondition, delegate, name),\n );\n if (otherConditions.length > 0) {\n const filter = new Filter(\n fanOut,\n createPredicate({\n type: 'or',\n conditions: otherConditions,\n }),\n );\n delegate.addEdge(fanOut, filter);\n branches.push(filter);\n }\n const ret = new FanIn(fanOut, branches);\n for (const branch of branches) {\n delegate.addEdge(branch, ret);\n }\n fanOut.setFanIn(ret);\n return ret;\n}\n\nexport function groupSubqueryConditions(condition: Disjunction) {\n const partitioned: [\n subqueryConditions: Condition[],\n otherConditions: NoSubqueryCondition[],\n ] = [[], []];\n for (const subCondition of condition.conditions) {\n if (isNotAndDoesNotContainSubquery(subCondition)) {\n partitioned[1].push(subCondition);\n } else {\n partitioned[0].push(subCondition);\n }\n }\n return partitioned;\n}\n\nexport function isNotAndDoesNotContainSubquery(\n condition: Condition,\n): condition is NoSubqueryCondition {\n if (condition.type === 'correlatedSubquery') {\n return false;\n }\n if (condition.type === 'simple') {\n return true;\n }\n return condition.conditions.every(isNotAndDoesNotContainSubquery);\n}\n\nfunction applySimpleCondition(\n input: FilterInput,\n delegate: BuilderDelegate,\n condition: SimpleCondition,\n): FilterInput {\n const filter = new Filter(input, createPredicate(condition));\n delegate.decorateFilterInput(\n filter,\n `${valuePosName(condition.left)}:${condition.op}:${valuePosName(condition.right)}`,\n );\n delegate.addEdge(input, filter);\n return filter;\n}\n\nfunction valuePosName(left: ValuePosition) {\n switch (left.type) {\n case 'static':\n return left.field;\n case 'literal':\n return left.value;\n case 'column':\n return left.name;\n }\n}\n\nfunction applyCorrelatedSubQuery(\n sq: CorrelatedSubquery,\n delegate: BuilderDelegate,\n queryID: string,\n end: Input,\n name: string,\n fromCondition: boolean,\n) {\n // TODO: we only omit the join if the CSQ if from a condition since\n // we want to create an empty array for `related` fields that are `limit(0)`\n if (sq.subquery.limit === 0 && fromCondition) {\n return end;\n }\n\n assert(sq.subquery.alias, 'Subquery must have an alias');\n const child = buildPipelineInternal(\n sq.subquery,\n delegate,\n queryID,\n `${name}.${sq.subquery.alias}`,\n sq.correlation.childField,\n );\n\n const joinName = `${name}:join(${sq.subquery.alias})`;\n const join = new Join({\n parent: end,\n child,\n parentKey: sq.correlation.parentField,\n childKey: sq.correlation.childField,\n relationshipName: sq.subquery.alias,\n hidden: sq.hidden ?? false,\n system: sq.system ?? 'client',\n });\n delegate.addEdge(end, join);\n delegate.addEdge(child, join);\n return delegate.decorateInput(join, joinName);\n}\n\nfunction applyCorrelatedSubqueryCondition(\n input: FilterInput,\n condition: CorrelatedSubqueryCondition,\n delegate: BuilderDelegate,\n name: string,\n): FilterInput {\n assert(\n condition.op === 'EXISTS' || condition.op === 'NOT EXISTS',\n 'Expected EXISTS or NOT EXISTS operator',\n );\n if (condition.related.subquery.limit === 0) {\n if (condition.op === 'EXISTS') {\n const filter = new Filter(input, () => false);\n delegate.addEdge(input, filter);\n return filter;\n }\n const filter = new Filter(input, () => true);\n delegate.addEdge(input, filter);\n return filter;\n }\n const existsName = `${name}:exists(${condition.related.subquery.alias})`;\n const exists = new Exists(\n input,\n must(condition.related.subquery.alias),\n condition.related.correlation.parentField,\n condition.op,\n );\n delegate.addEdge(input, exists);\n return delegate.decorateFilterInput(exists, existsName);\n}\n\nfunction gatherCorrelatedSubqueryQueryConditions(\n condition: Condition | undefined,\n) {\n const csqs: CorrelatedSubqueryCondition[] = [];\n const gather = (condition: Condition) => {\n if (condition.type === 'correlatedSubquery') {\n csqs.push(condition);\n return;\n }\n if (condition.type === 'and' || condition.type === 'or') {\n for (const c of condition.conditions) {\n gather(c);\n }\n return;\n }\n };\n if (condition) {\n gather(condition);\n }\n return csqs;\n}\n\nexport function assertOrderingIncludesPK(\n ordering: Ordering,\n pk: PrimaryKey,\n): void {\n // oxlint-disable-next-line unicorn/prefer-set-has -- Array is more appropriate here for small collections\n const orderingFields = ordering.map(([field]) => field);\n const missingFields = pk.filter(pkField => !orderingFields.includes(pkField));\n\n if (missingFields.length > 0) {\n throw new Error(\n `Ordering must include all primary key fields. Missing: ${missingFields.join(\n ', ',\n )}. ZQL automatically appends primary key fields to the ordering if they are missing \n so a common cause of this error is a casing mismatch between Postgres and ZQL.\n E.g., \"userid\" vs \"userID\".\n You may want to add double-quotes around your Postgres column names to prevent Postgres from lower-casing them:\n https://www.postgresql.org/docs/current/sql-syntax-lexical.htm`,\n );\n }\n}\n\nfunction uniquifyCorrelatedSubqueryConditionAliases(ast: AST): AST {\n if (!ast.where) {\n return ast;\n }\n const {where} = ast;\n if (where.type !== 'and' && where.type !== 'or') {\n return ast;\n }\n\n let count = 0;\n const uniquifyCorrelatedSubquery = (csqc: CorrelatedSubqueryCondition) => ({\n ...csqc,\n related: {\n ...csqc.related,\n subquery: {\n ...csqc.related.subquery,\n alias: (csqc.related.subquery.alias ?? '') + '_' + count++,\n },\n },\n });\n\n const uniquify = (cond: Condition): Condition => {\n if (cond.type === 'simple') {\n return cond;\n } else if (cond.type === 'correlatedSubquery') {\n return uniquifyCorrelatedSubquery(cond);\n }\n const conditions = [];\n for (const c of cond.conditions) {\n conditions.push(uniquify(c));\n }\n return {\n type: cond.type,\n conditions,\n };\n };\n\n const result = {\n ...ast,\n where: uniquify(where),\n };\n return result;\n}\n\nexport function conditionIncludesFlippedSubqueryAtAnyLevel(\n cond: Condition,\n): boolean {\n if (cond.type === 'correlatedSubquery') {\n return !!cond.flip;\n }\n if (cond.type === 'and' || cond.type === 'or') {\n return cond.conditions.some(c =>\n conditionIncludesFlippedSubqueryAtAnyLevel(c),\n );\n }\n // simple conditions don't have flips\n return false;\n}\n\nexport function partitionBranches(\n conditions: readonly Condition[],\n predicate: (c: Condition) => boolean,\n) {\n const matched: Condition[] = [];\n const notMatched: Condition[] = [];\n for (const c of conditions) {\n if (predicate(c)) {\n matched.push(c);\n } else {\n notMatched.push(c);\n }\n }\n return [matched, notMatched] as const;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"builder.js","names":[],"sources":["../../../../../zql/src/builder/builder.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert, unreachable} from '../../../shared/src/asserts.ts';\nimport type {JSONValue} from '../../../shared/src/json.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport type {\n AST,\n ColumnReference,\n CompoundKey,\n Condition,\n Conjunction,\n CorrelatedSubquery,\n CorrelatedSubqueryCondition,\n Disjunction,\n LiteralValue,\n Ordering,\n Parameter,\n SimpleCondition,\n ValuePosition,\n} from '../../../zero-protocol/src/ast.ts';\nimport type {Row} from '../../../zero-protocol/src/data.ts';\nimport type {PrimaryKey} from '../../../zero-protocol/src/primary-key.ts';\nimport {Exists} from '../ivm/exists.ts';\nimport {FanIn} from '../ivm/fan-in.ts';\nimport {FanOut} from '../ivm/fan-out.ts';\nimport {\n buildFilterPipeline,\n type FilterInput,\n} from '../ivm/filter-operators.ts';\nimport {Filter} from '../ivm/filter.ts';\nimport {FlippedJoin} from '../ivm/flipped-join.ts';\nimport {Join} from '../ivm/join.ts';\nimport type {Input, InputBase, Storage} from '../ivm/operator.ts';\nimport {Skip} from '../ivm/skip.ts';\nimport type {Source, SourceInput} from '../ivm/source.ts';\nimport {Cap} from '../ivm/cap.ts';\nimport {Take} from '../ivm/take.ts';\nimport {UnionFanIn} from '../ivm/union-fan-in.ts';\nimport {UnionFanOut} from '../ivm/union-fan-out.ts';\nimport {planQuery} from '../planner/planner-builder.ts';\nimport type {ConnectionCostModel} from '../planner/planner-connection.ts';\nimport {completeOrdering} from '../query/complete-ordering.ts';\nimport type {PlanDebugger} from '../planner/planner-debug.ts';\nimport type {DebugDelegate} from './debug-delegate.ts';\nimport {createPredicate, type NoSubqueryCondition} from './filter.ts';\n\nexport type StaticQueryParameters = {\n authData: Record<string, JSONValue>;\n preMutationRow?: Row | undefined;\n};\n\n/**\n * Interface required of caller to buildPipeline. Connects to constructed\n * pipeline to delegate environment to provide sources and storage.\n */\nexport interface BuilderDelegate {\n readonly applyFiltersAnyway?: boolean | undefined;\n debug?: DebugDelegate | undefined;\n\n /**\n * When true, allows NOT EXISTS conditions in queries.\n * Defaults to false.\n *\n * We only set this to true on the server.\n * The client-side query engine cannot support NOT EXISTS because:\n * 1. Zero only syncs a subset of data to the client\n * 2. On the client, we can't distinguish between a row not existing vs.\n * a row not being synced to the client\n * 3. NOT EXISTS requires complete knowledge of what doesn't exist\n */\n readonly enableNotExists?: boolean | undefined;\n\n /**\n * Called once for each source needed by the AST.\n * Might be called multiple times with same tableName. It is OK to return\n * same storage instance in that case.\n */\n getSource(tableName: string): Source | undefined;\n\n /**\n * Called once for each operator that requires storage. Should return a new\n * unique storage object for each call.\n */\n createStorage(name: string): Storage;\n\n decorateInput(input: Input, name: string): Input;\n\n addEdge(source: InputBase, dest: InputBase): void;\n\n decorateFilterInput(input: FilterInput, name: string): FilterInput;\n\n decorateSourceInput(input: SourceInput, queryID: string): Input;\n\n /**\n * The AST is mapped on-the-wire between client and server names.\n *\n * There is no \"wire\" for zqlite tests so this function is provided\n * to allow tests to remap the AST.\n */\n mapAst?: ((ast: AST) => AST) | undefined;\n}\n\n/**\n * Builds a pipeline from an AST. Caller must provide a delegate to create source\n * and storage interfaces as necessary.\n *\n * Usage:\n *\n * ```ts\n * class MySink implements Output {\n * readonly #input: Input;\n *\n * constructor(input: Input) {\n * this.#input = input;\n * input.setOutput(this);\n * }\n *\n * push(change: Change, _: Operator) {\n * console.log(change);\n * }\n * }\n *\n * const input = buildPipeline(ast, myDelegate, hash(ast));\n * const sink = new MySink(input);\n * ```\n */\nexport function buildPipeline(\n ast: AST,\n delegate: BuilderDelegate,\n queryID: string,\n costModel?: ConnectionCostModel,\n lc?: LogContext,\n planDebugger?: PlanDebugger,\n): Input {\n ast = delegate.mapAst ? delegate.mapAst(ast) : ast;\n ast = completeOrdering(\n ast,\n tableName => must(delegate.getSource(tableName)).tableSchema.primaryKey,\n );\n\n if (costModel) {\n ast = planQuery(ast, costModel, planDebugger, lc);\n }\n return buildPipelineInternal(ast, delegate, queryID, '');\n}\n\nexport function bindStaticParameters(\n ast: AST,\n staticQueryParameters: StaticQueryParameters | undefined,\n) {\n const visit = (node: AST): AST => ({\n ...node,\n where: node.where ? bindCondition(node.where) : undefined,\n related: node.related?.map(sq => ({\n ...sq,\n subquery: visit(sq.subquery),\n })),\n });\n\n function bindCondition(condition: Condition): Condition {\n if (condition.type === 'simple') {\n return {\n ...condition,\n left: bindValue(condition.left),\n right: bindValue(condition.right) as Exclude<\n ValuePosition,\n ColumnReference\n >,\n };\n }\n if (condition.type === 'correlatedSubquery') {\n return {\n ...condition,\n related: {\n ...condition.related,\n subquery: visit(condition.related.subquery),\n },\n };\n }\n\n return {\n ...condition,\n conditions: condition.conditions.map(bindCondition),\n };\n }\n\n const bindValue = (value: ValuePosition): ValuePosition => {\n if (isParameter(value)) {\n const anchor = must(\n staticQueryParameters,\n 'Static query params do not exist',\n )[value.anchor];\n const resolvedValue = resolveField(anchor, value.field);\n return {\n type: 'literal',\n value: resolvedValue as LiteralValue,\n };\n }\n return value;\n };\n\n return visit(ast);\n}\n\nfunction resolveField(\n anchor: Record<string, JSONValue> | Row | undefined,\n field: string | string[],\n): unknown {\n if (anchor === undefined) {\n return null;\n }\n\n if (Array.isArray(field)) {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n return field.reduce((acc, f) => (acc as any)?.[f], anchor) ?? null;\n }\n\n return anchor[field] ?? null;\n}\n\nfunction isParameter(value: ValuePosition): value is Parameter {\n return value.type === 'static';\n}\n\nconst EXISTS_LIMIT = 3;\nconst PERMISSIONS_EXISTS_LIMIT = 1;\n\n/**\n * Checks if a condition tree contains any NOT EXISTS operations.\n * Recursively checks AND/OR branches but does not recurse into nested subqueries\n * (those are checked when buildPipelineInternal processes them).\n */\nexport function assertNoNotExists(condition: Condition): void {\n switch (condition.type) {\n case 'simple':\n return;\n\n case 'correlatedSubquery':\n if (condition.op === 'NOT EXISTS') {\n throw new Error(\n 'not(exists()) is not supported on the client - see https://bugs.rocicorp.dev/issue/3438',\n );\n }\n return;\n\n case 'and':\n case 'or':\n for (const c of condition.conditions) {\n assertNoNotExists(c);\n }\n return;\n default:\n unreachable(condition);\n }\n}\n\nfunction buildPipelineInternal(\n ast: AST,\n delegate: BuilderDelegate,\n queryID: string,\n name: string,\n partitionKey?: CompoundKey,\n isNonFlippedExistsChild?: boolean | undefined,\n): Input {\n const source = delegate.getSource(ast.table);\n if (!source) {\n throw new Error(`Source not found: ${ast.table}`);\n }\n\n ast = uniquifyCorrelatedSubqueryConditionAliases(ast);\n\n if (!delegate.enableNotExists && ast.where) {\n assertNoNotExists(ast.where);\n }\n\n const csqConditions = gatherCorrelatedSubqueryQueryConditions(ast.where);\n const splitEditKeys: Set<string> = partitionKey\n ? new Set(partitionKey)\n : new Set();\n const aliases = new Set<string>();\n for (const csq of csqConditions) {\n aliases.add(csq.related.subquery.alias || '');\n for (const key of csq.related.correlation.parentField) {\n splitEditKeys.add(key);\n }\n }\n if (ast.related) {\n for (const csq of ast.related) {\n for (const key of csq.correlation.parentField) {\n splitEditKeys.add(key);\n }\n }\n }\n if (isNonFlippedExistsChild) {\n assert(ast.start === undefined, 'EXISTS subqueries must not have start');\n assert(\n ast.related === undefined,\n 'EXISTS subqueries must not have related',\n );\n }\n\n const conn = source.connect(\n // exists pipelines are unordered — orderBy is ignored here.\n // Non-exists pipelines always have orderBy completed with PKs.\n isNonFlippedExistsChild ? undefined : must(ast.orderBy),\n ast.where,\n splitEditKeys,\n delegate.debug,\n );\n\n let end: Input = delegate.decorateSourceInput(conn, queryID);\n end = delegate.decorateInput(end, `${name}:source(${ast.table})`);\n const {fullyAppliedFilters} = conn;\n\n if (ast.start) {\n const skip = new Skip(end, ast.start);\n delegate.addEdge(end, skip);\n end = delegate.decorateInput(skip, `${name}:skip)`);\n }\n\n for (const csqCondition of csqConditions) {\n // flipped EXISTS are handled in applyWhere\n if (!csqCondition.flip) {\n end = applyCorrelatedSubQuery(\n {\n ...csqCondition.related,\n subquery: {\n ...csqCondition.related.subquery,\n limit:\n csqCondition.related.system === 'permissions'\n ? PERMISSIONS_EXISTS_LIMIT\n : EXISTS_LIMIT,\n },\n },\n delegate,\n queryID,\n end,\n name,\n true,\n );\n }\n }\n\n if (ast.where && (!fullyAppliedFilters || delegate.applyFiltersAnyway)) {\n end = applyWhere(end, ast.where, delegate, name);\n }\n\n if (ast.limit !== undefined) {\n // We end `exists` pipelines with `cap`\n // The reason is that `cap` does not care about the order of the pipeline.\n // This allows SQLite to chose the order and never end up creating temp b-trees.\n // The problem with SQLite creating a temp b-tree is it will incur a scan of the entire\n // result set where exists only needs the first row.\n if (isNonFlippedExistsChild) {\n const capName = `${name}:cap`;\n const cap = new Cap(\n end,\n delegate.createStorage(capName),\n ast.limit,\n partitionKey,\n );\n delegate.addEdge(end, cap);\n end = delegate.decorateInput(cap, capName);\n } else {\n const takeName = `${name}:take`;\n const take = new Take(\n end,\n delegate.createStorage(takeName),\n ast.limit,\n partitionKey,\n );\n delegate.addEdge(end, take);\n end = delegate.decorateInput(take, takeName);\n }\n }\n\n if (ast.related) {\n // Dedupe by alias - last one wins (LWW), like limit(5).limit(10)\n const byAlias = new Map<string, CorrelatedSubquery>();\n for (const csq of ast.related) {\n byAlias.set(csq.subquery.alias ?? '', csq);\n }\n for (const csq of byAlias.values()) {\n end = applyCorrelatedSubQuery(csq, delegate, queryID, end, name, false);\n }\n }\n\n return end;\n}\n\nfunction applyWhere(\n input: Input,\n condition: Condition,\n delegate: BuilderDelegate,\n name: string,\n): Input {\n if (!conditionIncludesFlippedSubqueryAtAnyLevel(condition)) {\n return buildFilterPipeline(input, delegate, filterInput =>\n applyFilter(filterInput, condition, delegate, name),\n );\n }\n\n return applyFilterWithFlips(input, condition, delegate, name);\n}\n\nfunction applyFilterWithFlips(\n input: Input,\n condition: Condition,\n delegate: BuilderDelegate,\n name: string,\n): Input {\n let end = input;\n assert(condition.type !== 'simple', 'Simple conditions cannot have flips');\n\n switch (condition.type) {\n case 'and': {\n const [withFlipped, withoutFlipped] = partitionBranches(\n condition.conditions,\n conditionIncludesFlippedSubqueryAtAnyLevel,\n );\n if (withoutFlipped.length > 0) {\n end = buildFilterPipeline(input, delegate, filterInput =>\n applyAnd(\n filterInput,\n {\n type: 'and',\n conditions: withoutFlipped,\n },\n delegate,\n name,\n ),\n );\n }\n assert(withFlipped.length > 0, 'Impossible to have no flips here');\n for (const cond of withFlipped) {\n end = applyFilterWithFlips(end, cond, delegate, name);\n }\n break;\n }\n case 'or': {\n const [withFlipped, withoutFlipped] = partitionBranches(\n condition.conditions,\n conditionIncludesFlippedSubqueryAtAnyLevel,\n );\n assert(withFlipped.length > 0, 'Impossible to have no flips here');\n\n const ufo = new UnionFanOut(end);\n delegate.addEdge(end, ufo);\n end = delegate.decorateInput(ufo, `${name}:ufo`);\n\n const branches: Input[] = [];\n if (withoutFlipped.length > 0) {\n branches.push(\n buildFilterPipeline(end, delegate, filterInput =>\n applyOr(\n filterInput,\n {\n type: 'or',\n conditions: withoutFlipped,\n },\n delegate,\n name,\n ),\n ),\n );\n }\n\n for (const cond of withFlipped) {\n branches.push(applyFilterWithFlips(end, cond, delegate, name));\n }\n\n const ufi = new UnionFanIn(ufo, branches);\n for (const branch of branches) {\n delegate.addEdge(branch, ufi);\n }\n end = delegate.decorateInput(ufi, `${name}:ufi`);\n\n break;\n }\n case 'correlatedSubquery': {\n const sq = condition.related;\n const child = buildPipelineInternal(\n sq.subquery,\n delegate,\n '',\n `${name}.${sq.subquery.alias}`,\n sq.correlation.childField,\n false,\n );\n const flippedJoin = new FlippedJoin({\n parent: end,\n child,\n parentKey: sq.correlation.parentField,\n childKey: sq.correlation.childField,\n relationshipName: must(\n sq.subquery.alias,\n 'Subquery must have an alias',\n ),\n hidden: sq.hidden ?? false,\n system: sq.system ?? 'client',\n });\n delegate.addEdge(end, flippedJoin);\n delegate.addEdge(child, flippedJoin);\n end = delegate.decorateInput(\n flippedJoin,\n `${name}:flipped-join(${sq.subquery.alias})`,\n );\n break;\n }\n }\n\n return end;\n}\n\nfunction applyFilter(\n input: FilterInput,\n condition: Condition,\n delegate: BuilderDelegate,\n name: string,\n): FilterInput {\n switch (condition.type) {\n case 'and':\n return applyAnd(input, condition, delegate, name);\n case 'or':\n return applyOr(input, condition, delegate, name);\n case 'correlatedSubquery':\n return applyCorrelatedSubqueryCondition(input, condition, delegate, name);\n case 'simple':\n return applySimpleCondition(input, delegate, condition);\n }\n}\n\nfunction applyAnd(\n input: FilterInput,\n condition: Conjunction,\n delegate: BuilderDelegate,\n name: string,\n): FilterInput {\n for (const subCondition of condition.conditions) {\n input = applyFilter(input, subCondition, delegate, name);\n }\n return input;\n}\n\nexport function applyOr(\n input: FilterInput,\n condition: Disjunction,\n delegate: BuilderDelegate,\n name: string,\n): FilterInput {\n const [subqueryConditions, otherConditions] =\n groupSubqueryConditions(condition);\n // if there are no subquery conditions, no fan-in / fan-out is needed\n if (subqueryConditions.length === 0) {\n const filter = new Filter(\n input,\n createPredicate({\n type: 'or',\n conditions: otherConditions,\n }),\n );\n delegate.addEdge(input, filter);\n return filter;\n }\n\n const fanOut = new FanOut(input);\n delegate.addEdge(input, fanOut);\n const branches = subqueryConditions.map(subCondition =>\n applyFilter(fanOut, subCondition, delegate, name),\n );\n if (otherConditions.length > 0) {\n const filter = new Filter(\n fanOut,\n createPredicate({\n type: 'or',\n conditions: otherConditions,\n }),\n );\n delegate.addEdge(fanOut, filter);\n branches.push(filter);\n }\n const ret = new FanIn(fanOut, branches);\n for (const branch of branches) {\n delegate.addEdge(branch, ret);\n }\n fanOut.setFanIn(ret);\n return ret;\n}\n\nexport function groupSubqueryConditions(condition: Disjunction) {\n const partitioned: [\n subqueryConditions: Condition[],\n otherConditions: NoSubqueryCondition[],\n ] = [[], []];\n for (const subCondition of condition.conditions) {\n if (isNotAndDoesNotContainSubquery(subCondition)) {\n partitioned[1].push(subCondition);\n } else {\n partitioned[0].push(subCondition);\n }\n }\n return partitioned;\n}\n\nexport function isNotAndDoesNotContainSubquery(\n condition: Condition,\n): condition is NoSubqueryCondition {\n if (condition.type === 'correlatedSubquery') {\n return false;\n }\n if (condition.type === 'simple') {\n return true;\n }\n return condition.conditions.every(isNotAndDoesNotContainSubquery);\n}\n\nfunction applySimpleCondition(\n input: FilterInput,\n delegate: BuilderDelegate,\n condition: SimpleCondition,\n): FilterInput {\n const filter = new Filter(input, createPredicate(condition));\n delegate.decorateFilterInput(\n filter,\n `${valuePosName(condition.left)}:${condition.op}:${valuePosName(condition.right)}`,\n );\n delegate.addEdge(input, filter);\n return filter;\n}\n\nfunction valuePosName(left: ValuePosition) {\n switch (left.type) {\n case 'static':\n return left.field;\n case 'literal':\n return left.value;\n case 'column':\n return left.name;\n }\n}\n\nfunction applyCorrelatedSubQuery(\n sq: CorrelatedSubquery,\n delegate: BuilderDelegate,\n queryID: string,\n end: Input,\n name: string,\n fromCondition: boolean,\n) {\n // TODO: we only omit the join if the CSQ if from a condition since\n // we want to create an empty array for `related` fields that are `limit(0)`\n if (sq.subquery.limit === 0 && fromCondition) {\n return end;\n }\n\n assert(sq.subquery.alias, 'Subquery must have an alias');\n const child = buildPipelineInternal(\n sq.subquery,\n delegate,\n queryID,\n `${name}.${sq.subquery.alias}`,\n sq.correlation.childField,\n fromCondition,\n );\n\n const joinName = `${name}:join(${sq.subquery.alias})`;\n const join = new Join({\n parent: end,\n child,\n parentKey: sq.correlation.parentField,\n childKey: sq.correlation.childField,\n relationshipName: sq.subquery.alias,\n hidden: sq.hidden ?? false,\n system: sq.system ?? 'client',\n });\n delegate.addEdge(end, join);\n delegate.addEdge(child, join);\n return delegate.decorateInput(join, joinName);\n}\n\nfunction applyCorrelatedSubqueryCondition(\n input: FilterInput,\n condition: CorrelatedSubqueryCondition,\n delegate: BuilderDelegate,\n name: string,\n): FilterInput {\n assert(\n condition.op === 'EXISTS' || condition.op === 'NOT EXISTS',\n 'Expected EXISTS or NOT EXISTS operator',\n );\n if (condition.related.subquery.limit === 0) {\n if (condition.op === 'EXISTS') {\n const filter = new Filter(input, () => false);\n delegate.addEdge(input, filter);\n return filter;\n }\n const filter = new Filter(input, () => true);\n delegate.addEdge(input, filter);\n return filter;\n }\n const existsName = `${name}:exists(${condition.related.subquery.alias})`;\n const exists = new Exists(\n input,\n must(condition.related.subquery.alias),\n condition.related.correlation.parentField,\n condition.op,\n );\n delegate.addEdge(input, exists);\n return delegate.decorateFilterInput(exists, existsName);\n}\n\nfunction gatherCorrelatedSubqueryQueryConditions(\n condition: Condition | undefined,\n) {\n const csqs: CorrelatedSubqueryCondition[] = [];\n const gather = (condition: Condition) => {\n if (condition.type === 'correlatedSubquery') {\n csqs.push(condition);\n return;\n }\n if (condition.type === 'and' || condition.type === 'or') {\n for (const c of condition.conditions) {\n gather(c);\n }\n return;\n }\n };\n if (condition) {\n gather(condition);\n }\n return csqs;\n}\n\nexport function assertOrderingIncludesPK(\n ordering: Ordering,\n pk: PrimaryKey,\n): void {\n // oxlint-disable-next-line unicorn/prefer-set-has -- Array is more appropriate here for small collections\n const orderingFields = ordering.map(([field]) => field);\n const missingFields = pk.filter(pkField => !orderingFields.includes(pkField));\n\n if (missingFields.length > 0) {\n throw new Error(\n `Ordering must include all primary key fields. Missing: ${missingFields.join(\n ', ',\n )}. ZQL automatically appends primary key fields to the ordering if they are missing \n so a common cause of this error is a casing mismatch between Postgres and ZQL.\n E.g., \"userid\" vs \"userID\".\n You may want to add double-quotes around your Postgres column names to prevent Postgres from lower-casing them:\n https://www.postgresql.org/docs/current/sql-syntax-lexical.htm`,\n );\n }\n}\n\nfunction uniquifyCorrelatedSubqueryConditionAliases(ast: AST): AST {\n if (!ast.where) {\n return ast;\n }\n const {where} = ast;\n if (where.type !== 'and' && where.type !== 'or') {\n return ast;\n }\n\n let count = 0;\n const uniquifyCorrelatedSubquery = (csqc: CorrelatedSubqueryCondition) => ({\n ...csqc,\n related: {\n ...csqc.related,\n subquery: {\n ...csqc.related.subquery,\n alias: (csqc.related.subquery.alias ?? '') + '_' + count++,\n },\n },\n });\n\n const uniquify = (cond: Condition): Condition => {\n if (cond.type === 'simple') {\n return cond;\n } else if (cond.type === 'correlatedSubquery') {\n return uniquifyCorrelatedSubquery(cond);\n }\n const conditions = [];\n for (const c of cond.conditions) {\n conditions.push(uniquify(c));\n }\n return {\n type: cond.type,\n conditions,\n };\n };\n\n const result = {\n ...ast,\n where: uniquify(where),\n };\n return result;\n}\n\nexport function conditionIncludesFlippedSubqueryAtAnyLevel(\n cond: Condition,\n): boolean {\n if (cond.type === 'correlatedSubquery') {\n return !!cond.flip;\n }\n if (cond.type === 'and' || cond.type === 'or') {\n return cond.conditions.some(c =>\n conditionIncludesFlippedSubqueryAtAnyLevel(c),\n );\n }\n // simple conditions don't have flips\n return false;\n}\n\nexport function partitionBranches(\n conditions: readonly Condition[],\n predicate: (c: Condition) => boolean,\n) {\n const matched: Condition[] = [];\n const notMatched: Condition[] = [];\n for (const c of conditions) {\n if (predicate(c)) {\n matched.push(c);\n } else {\n notMatched.push(c);\n }\n }\n return [matched, notMatched] as const;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Change } from './change.ts';
|
|
2
|
+
import type { Node } from './data.ts';
|
|
3
|
+
import { type FetchRequest, type Input, type Operator, type Output, type Storage } from './operator.ts';
|
|
4
|
+
import type { SourceSchema } from './schema.ts';
|
|
5
|
+
import { type Stream } from './stream.ts';
|
|
6
|
+
import { type PartitionKey } from './take.ts';
|
|
7
|
+
/**
|
|
8
|
+
* The Cap operator is a count-based limiter for EXISTS subqueries that
|
|
9
|
+
* does not require ordering. Unlike Take, it tracks membership by primary
|
|
10
|
+
* key set rather than by a sorted bound. This means:
|
|
11
|
+
*
|
|
12
|
+
* - No comparator needed (no ordering requirement)
|
|
13
|
+
* - No `start` or `reverse` fetch support
|
|
14
|
+
* - No `#rowHiddenFromFetch` complexity (we can defer when adding to the pk set)
|
|
15
|
+
*
|
|
16
|
+
* Cap is used in EXISTS child pipelines where only the count of matching
|
|
17
|
+
* rows matters, not their order. This allows SQLite to skip ORDER BY
|
|
18
|
+
* entirely, enabling much faster query plans.
|
|
19
|
+
*
|
|
20
|
+
* Cap can count rows globally or by unique value of some partition key
|
|
21
|
+
* (same as Take).
|
|
22
|
+
*/
|
|
23
|
+
export declare class Cap implements Operator {
|
|
24
|
+
#private;
|
|
25
|
+
constructor(input: Input, storage: Storage, limit: number, partitionKey?: PartitionKey);
|
|
26
|
+
setOutput(output: Output): void;
|
|
27
|
+
getSchema(): SourceSchema;
|
|
28
|
+
fetch(req: FetchRequest): Stream<Node | 'yield'>;
|
|
29
|
+
push(change: Change): Stream<'yield'>;
|
|
30
|
+
destroy(): void;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=cap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cap.d.ts","sourceRoot":"","sources":["../../../../../zql/src/ivm/cap.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,KAAK,MAAM,EAAkB,MAAM,aAAa,CAAC;AAEzD,OAAO,KAAK,EAAa,IAAI,EAAC,MAAM,WAAW,CAAC;AAChD,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,KAAK,EACV,KAAK,QAAQ,EACb,KAAK,MAAM,EACX,KAAK,OAAO,EACb,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAC,KAAK,MAAM,EAAC,MAAM,aAAa,CAAC;AACxC,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,WAAW,CAAC;AAanB;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,GAAI,YAAW,QAAQ;;gBAWhC,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,YAAY;IAa7B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI/B,SAAS,IAAI,YAAY;IAIxB,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC,IAAI,GAAG,OAAO,CAAC;IA8GhD,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC;IAwGtC,OAAO,IAAI,IAAI;CAGhB"}
|