@rocicorp/zero 1.8.0-canary.3 → 1.8.0-canary.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/out/z2s/src/compiler.d.ts +2 -1
  2. package/out/z2s/src/compiler.d.ts.map +1 -1
  3. package/out/z2s/src/compiler.js +41 -4
  4. package/out/z2s/src/compiler.js.map +1 -1
  5. package/out/zero/package.js +1 -1
  6. package/out/zero/package.js.map +1 -1
  7. package/out/zero-cache/src/custom/fetch.d.ts +6 -1
  8. package/out/zero-cache/src/custom/fetch.d.ts.map +1 -1
  9. package/out/zero-cache/src/custom/fetch.js +255 -119
  10. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  11. package/out/zero-cache/src/custom/metrics.d.ts +31 -0
  12. package/out/zero-cache/src/custom/metrics.d.ts.map +1 -0
  13. package/out/zero-cache/src/custom/metrics.js +47 -0
  14. package/out/zero-cache/src/custom/metrics.js.map +1 -0
  15. package/out/zero-cache/src/custom-queries/transform-query.d.ts.map +1 -1
  16. package/out/zero-cache/src/custom-queries/transform-query.js +1 -1
  17. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  18. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.d.ts.map +1 -1
  19. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.js +22 -3
  20. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.js.map +1 -1
  21. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.d.ts.map +1 -1
  22. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.js +21 -2
  23. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.js.map +1 -1
  24. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  25. package/out/zero-cache/src/services/litestream/commands.js +166 -46
  26. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  27. package/out/zero-cache/src/services/litestream/metrics.d.ts +30 -0
  28. package/out/zero-cache/src/services/litestream/metrics.d.ts.map +1 -0
  29. package/out/zero-cache/src/services/litestream/metrics.js +112 -0
  30. package/out/zero-cache/src/services/litestream/metrics.js.map +1 -0
  31. package/out/zero-cache/src/services/mutagen/pusher.d.ts.map +1 -1
  32. package/out/zero-cache/src/services/mutagen/pusher.js +9 -3
  33. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  34. package/out/zero-client/src/client/version.js +1 -1
  35. package/out/zero-protocol/src/error.js +1 -1
  36. package/out/zqlite/src/query-builder.js +5 -3
  37. package/out/zqlite/src/query-builder.js.map +1 -1
  38. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"pusher.js","names":["#connContextManager","#pusher","#queue","#config","#lc","#isStopped","#refCount","#stopped","#clients","#customMutations","#pushes","#processPush","#fanOutResponses","#failDownstream"],"sources":["../../../../../../zero-cache/src/services/mutagen/pusher.ts"],"sourcesContent":["import {context, propagation, ROOT_CONTEXT} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {groupBy} from '../../../../shared/src/arrays.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {getErrorMessage} from '../../../../shared/src/error.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {Queue} from '../../../../shared/src/queue.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {ErrorReason} from '../../../../zero-protocol/src/error-reason.ts';\nimport {\n isProtocolError,\n type PushFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport {\n mutateResponseSchema,\n type MutateResponse,\n} from '../../../../zero-protocol/src/mutate-server.ts';\nimport type {MutationID} from '../../../../zero-protocol/src/mutation-id.ts';\nimport * as MutationType from '../../../../zero-protocol/src/mutation-type-enum.ts';\nimport {CLEANUP_RESULTS_MUTATION_NAME} from '../../../../zero-protocol/src/mutation.ts';\nimport {type PushBody} from '../../../../zero-protocol/src/push.ts';\nimport {authEquals, isAuthErrorBody} from '../../auth/auth.ts';\nimport {type ZeroConfig} from '../../config/zero-config.ts';\nimport {fetchFromAPIServer} from '../../custom/fetch.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {recordMutation} from '../../server/anonymous-otel-start.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {HandlerResult, StreamResult} from '../../workers/connection.ts';\nimport type {RefCountedService, Service} from '../service.ts';\nimport type {\n ConnectionContext,\n ConnectionContextManager,\n ConnectionSelector,\n} from '../view-syncer/connection-context-manager.ts';\n\nexport interface Pusher extends RefCountedService {\n initConnection(selector: ConnectionSelector): Source<Downstream>;\n enqueuePush(selector: ConnectionSelector, push: PushBody): HandlerResult;\n ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void>;\n deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void>;\n}\n\ntype Config = Pick<ZeroConfig, 'app' | 'shard'>;\n\n/**\n * Receives push messages from zero-client and forwards\n * them the the user's API server.\n *\n * If the user's API server is taking too long to process\n * the push, the PusherService will add the push to a queue\n * and send pushes in bulk the next time the user's API server\n * is available.\n *\n * - One PusherService exists per client group.\n * - Mutations for a given client are always sent in-order\n * - Mutations for different clients in the same group may be interleaved\n */\nexport class PusherService implements Service, Pusher {\n readonly id: string;\n readonly #connContextManager: ConnectionContextManager;\n readonly #pusher: PushWorker;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #config: Config;\n readonly #lc: LogContext;\n #stopped: Promise<void> | undefined;\n #refCount = 0;\n #isStopped = false;\n\n constructor(\n appConfig: Config,\n lc: LogContext,\n clientGroupID: string,\n connContextManager: ConnectionContextManager,\n ) {\n this.#connContextManager = connContextManager;\n this.#config = appConfig;\n this.#lc = lc.withContext('component', 'pusherService');\n this.#queue = new Queue();\n this.#pusher = new PushWorker(\n appConfig,\n lc,\n this.#connContextManager,\n this.#queue,\n );\n this.id = clientGroupID;\n }\n\n initConnection(selector: ConnectionSelector) {\n return this.#pusher.initConnection(selector);\n }\n\n enqueuePush(\n selector: ConnectionSelector,\n push: PushBody,\n ): Exclude<HandlerResult, StreamResult> {\n this.#pusher.enqueuePush(\n this.#connContextManager.mustGetConnectionContext(selector),\n push,\n );\n\n return {\n type: 'ok',\n };\n }\n\n async ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void> {\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: upToID.clientID,\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'single',\n clientGroupID: this.id,\n clientID: upToID.clientID,\n upToMutationID: upToID.id,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-${this.id}-${upToID.clientID}-${upToID.id}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n /**\n * Bulk cleanup is routed through the requester's push context.\n *\n * This assumes the client group shares a compatible push endpoint/auth\n * context.\n */\n async deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void> {\n if (clientIDs.length === 0) {\n return;\n }\n\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: clientIDs[0], // Use first client as sender\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'bulk',\n clientGroupID: this.id,\n clientIDs,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-bulk-${this.id}-${Date.now()}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send bulk cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n ref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n ++this.#refCount;\n }\n\n unref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n --this.#refCount;\n if (this.#refCount <= 0) {\n void this.stop();\n }\n }\n\n hasRefs(): boolean {\n return this.#refCount > 0;\n }\n\n run(): Promise<void> {\n this.#stopped = this.#pusher.run();\n return this.#stopped;\n }\n\n stop(): Promise<void> {\n if (this.#isStopped) {\n return must(this.#stopped, 'Stop was called before `run`');\n }\n this.#isStopped = true;\n this.#queue.enqueue('stop');\n return must(this.#stopped, 'Stop was called before `run`');\n }\n}\n\ntype PusherEntry = {\n push: PushBody;\n connCtx: ConnectionContext;\n};\ntype PusherEntryOrStop = PusherEntry | 'stop';\n\n/**\n * Awaits items in the queue then drains and sends them all\n * to the user's API server.\n */\nclass PushWorker {\n readonly #connContextManager: ConnectionContextManager;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #lc: LogContext;\n readonly #config: Config;\n readonly #clients: Map<\n string,\n {wsID: string; downstream: Subscription<Downstream>}\n >;\n\n readonly #customMutations = getOrCreateCounter(\n 'mutation',\n 'custom',\n 'Number of custom mutations processed',\n );\n readonly #pushes = getOrCreateCounter(\n 'mutation',\n 'pushes',\n 'Number of pushes processed by the pusher',\n );\n\n constructor(\n config: Config,\n lc: LogContext,\n connContextManager: ConnectionContextManager,\n queue: Queue<PusherEntryOrStop>,\n ) {\n this.#lc = lc.withContext('component', 'pusher');\n this.#connContextManager = connContextManager;\n this.#queue = queue;\n this.#config = config;\n this.#clients = new Map();\n }\n\n /**\n * Returns a new downstream stream if the clientID,wsID pair has not been seen before.\n * If a clientID already exists with a different wsID, that client's downstream is cancelled.\n */\n initConnection(selector: ConnectionSelector) {\n const existing = this.#clients.get(selector.clientID);\n if (existing && existing.wsID === selector.wsID) {\n // already initialized for this socket\n throw new Error('Connection was already initialized');\n }\n\n // client is back on a new connection\n if (existing) {\n existing.downstream.cancel();\n }\n\n const downstream = Subscription.create<Downstream>({\n cleanup: () => {\n this.#clients.delete(selector.clientID);\n },\n });\n this.#clients.set(selector.clientID, {\n wsID: selector.wsID,\n downstream,\n });\n return downstream;\n }\n\n enqueuePush(connCtx: ConnectionContext, push: PushBody) {\n this.#queue.enqueue({\n push,\n connCtx,\n });\n }\n\n async run() {\n for (;;) {\n const task = await this.#queue.dequeue();\n const rest = this.#queue.drain();\n const [pushes, terminate] = combinePushes([task, ...rest]);\n for (const push of pushes) {\n const parentContext = push.push.traceparent\n ? propagation.extract(ROOT_CONTEXT, {\n traceparent: push.push.traceparent,\n })\n : context.active();\n const response = await context.with(parentContext, () =>\n this.#processPush(push),\n );\n await this.#fanOutResponses(response);\n }\n\n if (terminate) {\n break;\n }\n }\n }\n\n /**\n * 1. If the entire `push` fails, we send the error to relevant clients.\n * 2. If the push succeeds, we look for any mutation failure that should cause the connection to terminate\n * and terminate the connection for those clients.\n */\n #fanOutResponses(response: MutateResponse) {\n const connectionTerminations: (() => void)[] = [];\n\n // if the entire push failed, send that to the client.\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a push error.',\n response,\n );\n // TODO(0xcadams): Fanout is keyed only by clientID here. If a response arrives\n // after reconnect or re-auth, `#clients.get(clientID)` may point at a\n // newer wsID/revision and fail the replacement downstream instead.\n const groupedMutationIDs = groupBy(\n response.mutationIDs ?? [],\n m => m.clientID,\n );\n for (const [clientID, mutationIDs] of groupedMutationIDs) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n // We do not resolve mutations on the client if the push fails\n // as those mutations will be retried.\n if ('error' in response) {\n // This error code path will eventually be removed when we\n // no longer support the legacy push error format.\n const pushFailedBody: PushFailedBody =\n response.error === 'http'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n status: response.status,\n bodyPreview: response.details,\n mutationIDs,\n message: `Fetch from API server returned non-OK status ${response.status}`,\n }\n : response.error === 'unsupportedPushVersion'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.UnsupportedPushVersion,\n mutationIDs,\n message: `Unsupported push version`,\n }\n : {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Internal,\n mutationIDs,\n message:\n response.error === 'zeroPusher'\n ? response.details\n : response.error === 'unsupportedSchemaVersion'\n ? 'Unsupported schema version'\n : 'An unknown error occurred while pushing to the API server',\n };\n\n this.#failDownstream(client.downstream, pushFailedBody);\n } else {\n this.#failDownstream(client.downstream, response);\n }\n }\n } else {\n // Look for mutations results that should cause us to terminate the connection\n // TODO(0xcadams): Same stale-routing issue as above: fatal mutation results are\n // still mapped to the current downstream by clientID only.\n const groupedMutations = groupBy(response.mutations, m => m.id.clientID);\n for (const [clientID, mutations] of groupedMutations) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n let failure: PushFailedBody | undefined;\n let i = 0;\n for (; i < mutations.length; i++) {\n const m = mutations[i];\n if ('error' in m.result) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a mutation error.',\n m.result,\n );\n }\n // This error code path will eventually be removed,\n // keeping this for backwards compatibility, but the server\n // should now return a PushFailedBody with the mutationIDs\n if ('error' in m.result && m.result.error === 'oooMutation') {\n failure = {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.OutOfOrderMutation,\n message: 'mutation was out of order',\n details: m.result.details,\n mutationIDs: mutations.map(m => ({\n clientID: m.id.clientID,\n id: m.id.id,\n })),\n };\n break;\n }\n }\n\n if (failure && i < mutations.length - 1) {\n this.#lc.warn?.(\n 'push-response contains mutations after a mutation which should fatal the connection',\n );\n }\n\n if (failure) {\n connectionTerminations.push(() =>\n this.#failDownstream(client.downstream, failure),\n );\n }\n }\n }\n\n connectionTerminations.forEach(cb => cb());\n }\n\n async #processPush(entry: PusherEntry): Promise<MutateResponse> {\n this.#customMutations.add(entry.push.mutations.length, {\n clientGroupID: entry.push.clientGroupID,\n });\n this.#pushes.add(1, {\n clientGroupID: entry.push.clientGroupID,\n });\n\n // Record custom mutations for telemetry\n recordMutation('custom', entry.push.mutations.length);\n\n const url = must(\n entry.connCtx.mutateContext.url,\n 'ZERO_MUTATE_URL is not set',\n );\n\n this.#lc.debug?.(\n 'pushing to',\n url,\n 'with',\n entry.push.mutations.length,\n 'mutations',\n );\n\n let mutationIDs: MutationID[] = [];\n\n try {\n mutationIDs = entry.push.mutations.map(m => ({\n id: m.id,\n clientID: m.clientID,\n }));\n\n const response = await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n entry.connCtx,\n {\n appID: this.#config.app.id,\n shardNum: this.#config.shard.num,\n },\n entry.push,\n );\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n // A successful push also validates this connection's current auth snapshot.\n // That lets later shared work reuse it without trusting stale credentials.\n this.#connContextManager.validateConnection(\n entry.connCtx,\n entry.connCtx.revision,\n 'kind' in response &&\n response.kind === 'MutateResponse' &&\n response?.userID !== undefined\n ? {kind: 'server-validated', validatedUserID: response.userID}\n : {kind: 'client-fallback'},\n );\n return response;\n } catch (e) {\n if (isProtocolError(e) && e.errorBody.kind === ErrorKind.PushFailed) {\n const response = {\n ...e.errorBody,\n mutationIDs,\n } as const satisfies PushFailedBody;\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n\n if (isProtocolError(e) && isAuthErrorBody(e.errorBody)) {\n // The push completed far enough for local validation to reject the\n // connection, so invalidate it and surface the result as PushFailed.\n this.#lc.warn?.('Push validation failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: e.message,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n message: e.message,\n status: 401,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Failed to push: ${getErrorMessage(e)}`,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n }\n\n #failDownstream(\n downstream: Subscription<Downstream>,\n errorBody: PushFailedBody,\n ): void {\n downstream.fail(new ProtocolErrorWithLevel(errorBody, 'warn'));\n }\n}\n\n/**\n * Pushes for different clients, sockets, or auth revisions could be interleaved.\n *\n * In order to batch safely, we only combine pushes from the same\n * clientID/wsID/revision snapshot.\n */\nexport function combinePushes(\n entries: readonly (PusherEntryOrStop | undefined)[],\n): [PusherEntry[], boolean] {\n const pushesByConnection = new Map<string, PusherEntry[]>();\n\n function collect() {\n const ret: PusherEntry[] = [];\n for (const entries of pushesByConnection.values()) {\n const composite: PusherEntry = {\n ...entries[0],\n push: {\n ...entries[0].push,\n mutations: [],\n },\n };\n ret.push(composite);\n for (const entry of entries) {\n assertAreCompatiblePushes(composite, entry);\n composite.push.mutations.push(...entry.push.mutations);\n }\n }\n return ret;\n }\n\n for (const entry of entries) {\n if (entry === 'stop' || entry === undefined) {\n return [collect(), true];\n }\n\n const key = `${entry.connCtx.clientID}:${entry.connCtx.wsID}:${entry.connCtx.revision}`;\n const existing = pushesByConnection.get(key);\n if (existing) {\n existing.push(entry);\n } else {\n pushesByConnection.set(key, [entry]);\n }\n }\n\n return [collect(), false] as const;\n}\n\n// These invariants should always be true for a given clientID.\n// If they are not, we have a bug in the code somewhere.\nfunction assertAreCompatiblePushes(left: PusherEntry, right: PusherEntry) {\n assert(\n left.connCtx.clientID === right.connCtx.clientID,\n 'clientID must be the same for all pushes',\n );\n assert(\n left.connCtx.wsID === right.connCtx.wsID,\n 'wsID must be the same for all pushes',\n );\n assert(\n left.connCtx.revision === right.connCtx.revision,\n 'revision must be the same for all pushes',\n );\n assert(\n authEquals(left.connCtx.auth, right.connCtx.auth),\n 'auth must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.schemaVersion === right.push.schemaVersion,\n 'schemaVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.pushVersion === right.push.pushVersion,\n 'pushVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.cookie ===\n right.connCtx.mutateContext.headerOptions.cookie,\n 'httpCookie must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.origin ===\n right.connCtx.mutateContext.headerOptions.origin,\n 'origin must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.user.id === right.connCtx.user.id,\n 'userID must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.url === right.connCtx.mutateContext.url,\n 'userPushURL must be the same for all pushes with the same clientID',\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pusher.js","names":["#connContextManager","#pusher","#queue","#config","#lc","#isStopped","#refCount","#stopped","#clients","#customMutations","#pushes","#processPush","#fanOutResponses","#failDownstream"],"sources":["../../../../../../zero-cache/src/services/mutagen/pusher.ts"],"sourcesContent":["import {context, propagation, ROOT_CONTEXT} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {groupBy} from '../../../../shared/src/arrays.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {getErrorMessage} from '../../../../shared/src/error.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {Queue} from '../../../../shared/src/queue.ts';\nimport type {Downstream} from '../../../../zero-protocol/src/down.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {ErrorReason} from '../../../../zero-protocol/src/error-reason.ts';\nimport {\n isProtocolError,\n type PushFailedBody,\n} from '../../../../zero-protocol/src/error.ts';\nimport {\n mutateResponseSchema,\n type MutateResponse,\n} from '../../../../zero-protocol/src/mutate-server.ts';\nimport type {MutationID} from '../../../../zero-protocol/src/mutation-id.ts';\nimport * as MutationType from '../../../../zero-protocol/src/mutation-type-enum.ts';\nimport {CLEANUP_RESULTS_MUTATION_NAME} from '../../../../zero-protocol/src/mutation.ts';\nimport {type PushBody} from '../../../../zero-protocol/src/push.ts';\nimport {authEquals, isAuthErrorBody} from '../../auth/auth.ts';\nimport {type ZeroConfig} from '../../config/zero-config.ts';\nimport {fetchFromAPIServer} from '../../custom/fetch.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {recordMutation} from '../../server/anonymous-otel-start.ts';\nimport {ProtocolErrorWithLevel} from '../../types/error-with-level.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {HandlerResult, StreamResult} from '../../workers/connection.ts';\nimport type {RefCountedService, Service} from '../service.ts';\nimport type {\n ConnectionContext,\n ConnectionContextManager,\n ConnectionSelector,\n} from '../view-syncer/connection-context-manager.ts';\n\nexport interface Pusher extends RefCountedService {\n initConnection(selector: ConnectionSelector): Source<Downstream>;\n enqueuePush(selector: ConnectionSelector, push: PushBody): HandlerResult;\n ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void>;\n deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void>;\n}\n\ntype Config = Pick<ZeroConfig, 'app' | 'shard'>;\n\n/**\n * Receives push messages from zero-client and forwards\n * them the the user's API server.\n *\n * If the user's API server is taking too long to process\n * the push, the PusherService will add the push to a queue\n * and send pushes in bulk the next time the user's API server\n * is available.\n *\n * - One PusherService exists per client group.\n * - Mutations for a given client are always sent in-order\n * - Mutations for different clients in the same group may be interleaved\n */\nexport class PusherService implements Service, Pusher {\n readonly id: string;\n readonly #connContextManager: ConnectionContextManager;\n readonly #pusher: PushWorker;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #config: Config;\n readonly #lc: LogContext;\n #stopped: Promise<void> | undefined;\n #refCount = 0;\n #isStopped = false;\n\n constructor(\n appConfig: Config,\n lc: LogContext,\n clientGroupID: string,\n connContextManager: ConnectionContextManager,\n ) {\n this.#connContextManager = connContextManager;\n this.#config = appConfig;\n this.#lc = lc.withContext('component', 'pusherService');\n this.#queue = new Queue();\n this.#pusher = new PushWorker(\n appConfig,\n lc,\n this.#connContextManager,\n this.#queue,\n );\n this.id = clientGroupID;\n }\n\n initConnection(selector: ConnectionSelector) {\n return this.#pusher.initConnection(selector);\n }\n\n enqueuePush(\n selector: ConnectionSelector,\n push: PushBody,\n ): Exclude<HandlerResult, StreamResult> {\n this.#pusher.enqueuePush(\n this.#connContextManager.mustGetConnectionContext(selector),\n push,\n );\n\n return {\n type: 'ok',\n };\n }\n\n async ackMutationResponses(\n requester: ConnectionSelector,\n upToID: MutationID,\n ): Promise<void> {\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: upToID.clientID,\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'single',\n clientGroupID: this.id,\n clientID: upToID.clientID,\n upToMutationID: upToID.id,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-${this.id}-${upToID.clientID}-${upToID.id}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n {operation: 'cleanup', cleanupType: 'single'},\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n /**\n * Bulk cleanup is routed through the requester's push context.\n *\n * This assumes the client group shares a compatible push endpoint/auth\n * context.\n */\n async deleteClientMutations(\n requester: ConnectionSelector,\n clientIDs: string[],\n ): Promise<void> {\n if (clientIDs.length === 0) {\n return;\n }\n\n const connCtx = this.#connContextManager.getConnectionContext(requester);\n if (!connCtx?.mutateContext?.url) {\n // No push URL configured, skip cleanup\n return;\n }\n\n const cleanupBody: PushBody = {\n clientGroupID: this.id,\n mutations: [\n {\n type: MutationType.Custom,\n id: 0, // Not tracked - this is fire-and-forget\n clientID: clientIDs[0], // Use first client as sender\n name: CLEANUP_RESULTS_MUTATION_NAME,\n args: [\n {\n type: 'bulk',\n clientGroupID: this.id,\n clientIDs,\n },\n ],\n timestamp: Date.now(),\n },\n ],\n pushVersion: 1,\n timestamp: Date.now(),\n requestID: `cleanup-bulk-${this.id}-${Date.now()}`,\n };\n\n try {\n await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n connCtx,\n {appID: this.#config.app.id, shardNum: this.#config.shard.num},\n cleanupBody,\n {operation: 'cleanup', cleanupType: 'bulk'},\n );\n } catch (e) {\n this.#lc.warn?.('Failed to send bulk cleanup mutation', {\n error: getErrorMessage(e),\n });\n }\n }\n\n ref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n ++this.#refCount;\n }\n\n unref() {\n assert(!this.#isStopped, 'PusherService is already stopped');\n --this.#refCount;\n if (this.#refCount <= 0) {\n void this.stop();\n }\n }\n\n hasRefs(): boolean {\n return this.#refCount > 0;\n }\n\n run(): Promise<void> {\n this.#stopped = this.#pusher.run();\n return this.#stopped;\n }\n\n stop(): Promise<void> {\n if (this.#isStopped) {\n return must(this.#stopped, 'Stop was called before `run`');\n }\n this.#isStopped = true;\n this.#queue.enqueue('stop');\n return must(this.#stopped, 'Stop was called before `run`');\n }\n}\n\ntype PusherEntry = {\n push: PushBody;\n connCtx: ConnectionContext;\n};\ntype PusherEntryOrStop = PusherEntry | 'stop';\n\n/**\n * Awaits items in the queue then drains and sends them all\n * to the user's API server.\n */\nclass PushWorker {\n readonly #connContextManager: ConnectionContextManager;\n readonly #queue: Queue<PusherEntryOrStop>;\n readonly #lc: LogContext;\n readonly #config: Config;\n readonly #clients: Map<\n string,\n {wsID: string; downstream: Subscription<Downstream>}\n >;\n\n readonly #customMutations = getOrCreateCounter(\n 'mutation',\n 'custom',\n 'Number of custom mutations processed',\n );\n readonly #pushes = getOrCreateCounter(\n 'mutation',\n 'pushes',\n 'Number of pushes processed by the pusher',\n );\n\n constructor(\n config: Config,\n lc: LogContext,\n connContextManager: ConnectionContextManager,\n queue: Queue<PusherEntryOrStop>,\n ) {\n this.#lc = lc.withContext('component', 'pusher');\n this.#connContextManager = connContextManager;\n this.#queue = queue;\n this.#config = config;\n this.#clients = new Map();\n }\n\n /**\n * Returns a new downstream stream if the clientID,wsID pair has not been seen before.\n * If a clientID already exists with a different wsID, that client's downstream is cancelled.\n */\n initConnection(selector: ConnectionSelector) {\n const existing = this.#clients.get(selector.clientID);\n if (existing && existing.wsID === selector.wsID) {\n // already initialized for this socket\n throw new Error('Connection was already initialized');\n }\n\n // client is back on a new connection\n if (existing) {\n existing.downstream.cancel();\n }\n\n const downstream = Subscription.create<Downstream>({\n cleanup: () => {\n this.#clients.delete(selector.clientID);\n },\n });\n this.#clients.set(selector.clientID, {\n wsID: selector.wsID,\n downstream,\n });\n return downstream;\n }\n\n enqueuePush(connCtx: ConnectionContext, push: PushBody) {\n this.#queue.enqueue({\n push,\n connCtx,\n });\n }\n\n async run() {\n for (;;) {\n const task = await this.#queue.dequeue();\n const rest = this.#queue.drain();\n const [pushes, terminate] = combinePushes([task, ...rest]);\n for (const push of pushes) {\n const parentContext = push.push.traceparent\n ? propagation.extract(ROOT_CONTEXT, {\n traceparent: push.push.traceparent,\n })\n : context.active();\n const response = await context.with(parentContext, () =>\n this.#processPush(push),\n );\n await this.#fanOutResponses(response);\n }\n\n if (terminate) {\n break;\n }\n }\n }\n\n /**\n * 1. If the entire `push` fails, we send the error to relevant clients.\n * 2. If the push succeeds, we look for any mutation failure that should cause the connection to terminate\n * and terminate the connection for those clients.\n */\n #fanOutResponses(response: MutateResponse) {\n const connectionTerminations: (() => void)[] = [];\n\n // if the entire push failed, send that to the client.\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a push error.',\n response,\n );\n // TODO(0xcadams): Fanout is keyed only by clientID here. If a response arrives\n // after reconnect or re-auth, `#clients.get(clientID)` may point at a\n // newer wsID/revision and fail the replacement downstream instead.\n const groupedMutationIDs = groupBy(\n response.mutationIDs ?? [],\n m => m.clientID,\n );\n for (const [clientID, mutationIDs] of groupedMutationIDs) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n // We do not resolve mutations on the client if the push fails\n // as those mutations will be retried.\n if ('error' in response) {\n // This error code path will eventually be removed when we\n // no longer support the legacy push error format.\n const pushFailedBody: PushFailedBody =\n response.error === 'http'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n status: response.status,\n bodyPreview: response.details,\n mutationIDs,\n message: `Fetch from API server returned non-OK status ${response.status}`,\n }\n : response.error === 'unsupportedPushVersion'\n ? {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.UnsupportedPushVersion,\n mutationIDs,\n message: `Unsupported push version`,\n }\n : {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.Internal,\n mutationIDs,\n message:\n response.error === 'zeroPusher'\n ? response.details\n : response.error === 'unsupportedSchemaVersion'\n ? 'Unsupported schema version'\n : 'An unknown error occurred while pushing to the API server',\n };\n\n this.#failDownstream(client.downstream, pushFailedBody);\n } else {\n this.#failDownstream(client.downstream, response);\n }\n }\n } else {\n // Look for mutations results that should cause us to terminate the connection\n // TODO(0xcadams): Same stale-routing issue as above: fatal mutation results are\n // still mapped to the current downstream by clientID only.\n const groupedMutations = groupBy(response.mutations, m => m.id.clientID);\n for (const [clientID, mutations] of groupedMutations) {\n const client = this.#clients.get(clientID);\n if (!client) {\n continue;\n }\n\n let failure: PushFailedBody | undefined;\n let i = 0;\n for (; i < mutations.length; i++) {\n const m = mutations[i];\n if ('error' in m.result) {\n this.#lc.warn?.(\n 'The server behind ZERO_MUTATE_URL returned a mutation error.',\n m.result,\n );\n }\n // This error code path will eventually be removed,\n // keeping this for backwards compatibility, but the server\n // should now return a PushFailedBody with the mutationIDs\n if ('error' in m.result && m.result.error === 'oooMutation') {\n failure = {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.Server,\n reason: ErrorReason.OutOfOrderMutation,\n message: 'mutation was out of order',\n details: m.result.details,\n mutationIDs: mutations.map(m => ({\n clientID: m.id.clientID,\n id: m.id.id,\n })),\n };\n break;\n }\n }\n\n if (failure && i < mutations.length - 1) {\n this.#lc.warn?.(\n 'push-response contains mutations after a mutation which should fatal the connection',\n );\n }\n\n if (failure) {\n connectionTerminations.push(() =>\n this.#failDownstream(client.downstream, failure),\n );\n }\n }\n }\n\n connectionTerminations.forEach(cb => cb());\n }\n\n async #processPush(entry: PusherEntry): Promise<MutateResponse> {\n this.#customMutations.add(entry.push.mutations.length, {\n clientGroupID: entry.push.clientGroupID,\n });\n this.#pushes.add(1, {\n clientGroupID: entry.push.clientGroupID,\n });\n\n // Record custom mutations for telemetry\n recordMutation('custom', entry.push.mutations.length);\n\n const url = must(\n entry.connCtx.mutateContext.url,\n 'ZERO_MUTATE_URL is not set',\n );\n\n this.#lc.debug?.(\n 'pushing to',\n url,\n 'with',\n entry.push.mutations.length,\n 'mutations',\n );\n\n let mutationIDs: MutationID[] = [];\n\n try {\n mutationIDs = entry.push.mutations.map(m => ({\n id: m.id,\n clientID: m.clientID,\n }));\n\n const response = await fetchFromAPIServer(\n mutateResponseSchema,\n 'push',\n this.#lc,\n entry.connCtx,\n {\n appID: this.#config.app.id,\n shardNum: this.#config.shard.num,\n },\n entry.push,\n {operation: 'mutate'},\n );\n if (\n ('kind' in response && response.kind === ErrorKind.PushFailed) ||\n 'error' in response\n ) {\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n // A successful push also validates this connection's current auth snapshot.\n // That lets later shared work reuse it without trusting stale credentials.\n this.#connContextManager.validateConnection(\n entry.connCtx,\n entry.connCtx.revision,\n 'kind' in response &&\n response.kind === 'MutateResponse' &&\n response?.userID !== undefined\n ? {kind: 'server-validated', validatedUserID: response.userID}\n : {kind: 'client-fallback'},\n );\n return response;\n } catch (e) {\n if (isProtocolError(e) && e.errorBody.kind === ErrorKind.PushFailed) {\n const response = {\n ...e.errorBody,\n mutationIDs,\n } as const satisfies PushFailedBody;\n if (isAuthErrorBody(response)) {\n this.#lc.warn?.('Push auth failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: 'kind' in response ? response.message : undefined,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n }\n return response;\n }\n\n if (isProtocolError(e) && isAuthErrorBody(e.errorBody)) {\n // The push completed far enough for local validation to reject the\n // connection, so invalidate it and surface the result as PushFailed.\n this.#lc.warn?.('Push validation failed; invalidating connection', {\n clientID: entry.connCtx.clientID,\n response: e.message,\n });\n this.#connContextManager.failConnection(\n entry.connCtx,\n entry.connCtx.revision,\n );\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.HTTP,\n message: e.message,\n status: 401,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n\n return {\n kind: ErrorKind.PushFailed,\n origin: ErrorOrigin.ZeroCache,\n reason: ErrorReason.Internal,\n message: `Failed to push: ${getErrorMessage(e)}`,\n mutationIDs,\n } as const satisfies PushFailedBody;\n }\n }\n\n #failDownstream(\n downstream: Subscription<Downstream>,\n errorBody: PushFailedBody,\n ): void {\n downstream.fail(new ProtocolErrorWithLevel(errorBody, 'warn'));\n }\n}\n\n/**\n * Pushes for different clients, sockets, or auth revisions could be interleaved.\n *\n * In order to batch safely, we only combine pushes from the same\n * clientID/wsID/revision snapshot.\n */\nexport function combinePushes(\n entries: readonly (PusherEntryOrStop | undefined)[],\n): [PusherEntry[], boolean] {\n const pushesByConnection = new Map<string, PusherEntry[]>();\n\n function collect() {\n const ret: PusherEntry[] = [];\n for (const entries of pushesByConnection.values()) {\n const composite: PusherEntry = {\n ...entries[0],\n push: {\n ...entries[0].push,\n mutations: [],\n },\n };\n ret.push(composite);\n for (const entry of entries) {\n assertAreCompatiblePushes(composite, entry);\n composite.push.mutations.push(...entry.push.mutations);\n }\n }\n return ret;\n }\n\n for (const entry of entries) {\n if (entry === 'stop' || entry === undefined) {\n return [collect(), true];\n }\n\n const key = `${entry.connCtx.clientID}:${entry.connCtx.wsID}:${entry.connCtx.revision}`;\n const existing = pushesByConnection.get(key);\n if (existing) {\n existing.push(entry);\n } else {\n pushesByConnection.set(key, [entry]);\n }\n }\n\n return [collect(), false] as const;\n}\n\n// These invariants should always be true for a given clientID.\n// If they are not, we have a bug in the code somewhere.\nfunction assertAreCompatiblePushes(left: PusherEntry, right: PusherEntry) {\n assert(\n left.connCtx.clientID === right.connCtx.clientID,\n 'clientID must be the same for all pushes',\n );\n assert(\n left.connCtx.wsID === right.connCtx.wsID,\n 'wsID must be the same for all pushes',\n );\n assert(\n left.connCtx.revision === right.connCtx.revision,\n 'revision must be the same for all pushes',\n );\n assert(\n authEquals(left.connCtx.auth, right.connCtx.auth),\n 'auth must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.schemaVersion === right.push.schemaVersion,\n 'schemaVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.push.pushVersion === right.push.pushVersion,\n 'pushVersion must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.cookie ===\n right.connCtx.mutateContext.headerOptions.cookie,\n 'httpCookie must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.headerOptions.origin ===\n right.connCtx.mutateContext.headerOptions.origin,\n 'origin must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.user.id === right.connCtx.user.id,\n 'userID must be the same for all pushes with the same clientID',\n );\n assert(\n left.connCtx.mutateContext.url === right.connCtx.mutateContext.url,\n 'userPushURL must be the same for all pushes with the same clientID',\n );\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * The current version of Zero.
4
4
  */
5
- var version = "1.8.0-canary.3";
5
+ var version = "1.8.0-canary.5";
6
6
  //#endregion
7
7
  export { version };
8
8
 
@@ -86,6 +86,6 @@ function isProtocolError(error) {
86
86
  return error instanceof ProtocolError;
87
87
  }
88
88
  //#endregion
89
- export { ProtocolError, errorKindSchema, errorMessageSchema, isProtocolError, pushFailedBodySchema, transformFailedBodySchema };
89
+ export { ProtocolError, errorBodySchema, errorKindSchema, errorMessageSchema, isProtocolError, pushFailedBodySchema, transformFailedBodySchema };
90
90
 
91
91
  //# sourceMappingURL=error.js.map
@@ -108,11 +108,13 @@ function nullableAwareEquality(field, value, columnType) {
108
108
  return columnType.optional === true ? sql`${sql.ident(field)} IS ${value}` : sql`${sql.ident(field)} = ${value}`;
109
109
  }
110
110
  function nullableAwareRangeComparison(field, value, operator, columnType) {
111
+ if (value === null) return operator === ">" ? sql`${sql.ident(field)} IS NOT NULL` : sql`FALSE`;
111
112
  const comparison = sql`${sql.ident(field)} ${sql.__dangerous__rawValue(operator)} ${value}`;
112
113
  if (columnType.optional !== true) return comparison;
113
114
  return operator === ">" ? sql`(${value} IS NULL OR ${comparison})` : sql`(${sql.ident(field)} IS NULL OR ${comparison})`;
114
115
  }
115
116
  function sargableLeadingStartBound(field, value, operator, columnType) {
117
+ if (value === null) return;
116
118
  if (columnType.optional === true) return;
117
119
  const inclusiveOperator = operator === ">" ? ">=" : "<=";
118
120
  return sql`${sql.ident(field)} ${sql.__dangerous__rawValue(inclusiveOperator)} ${value}`;
@@ -142,21 +144,21 @@ function gatherStartConstraints(start, reverse, order, columnTypes) {
142
144
  const [iField, iDirection] = order[i];
143
145
  for (let j = 0; j <= i; j++) if (j === i) {
144
146
  const columnType = columnTypes[iField];
145
- const constraintValue = toSQLiteType(from[iField], columnType.type);
147
+ const constraintValue = toSQLiteType(from[iField] ?? null, columnType.type);
146
148
  const operator = iDirection === "asc" ? reverse ? "<" : ">" : reverse ? ">" : "<";
147
149
  if (i === 0) leadingBound = sargableLeadingStartBound(iField, constraintValue, operator, columnType);
148
150
  group.push(nullableAwareRangeComparison(iField, constraintValue, operator, columnType));
149
151
  } else {
150
152
  const [jField] = order[j];
151
153
  const columnType = columnTypes[jField];
152
- const value = toSQLiteType(from[jField], columnType.type);
154
+ const value = toSQLiteType(from[jField] ?? null, columnType.type);
153
155
  group.push(nullableAwareEquality(jField, value, columnType));
154
156
  }
155
157
  constraints.push(sql`(${sql.join(group, sql` AND `)})`);
156
158
  }
157
159
  if (basis === "at") constraints.push(sql`(${sql.join(order.map(([field]) => {
158
160
  const columnType = columnTypes[field];
159
- return nullableAwareEquality(field, toSQLiteType(from[field], columnType.type), columnType);
161
+ return nullableAwareEquality(field, toSQLiteType(from[field] ?? null, columnType.type), columnType);
160
162
  }), sql` AND `)})`);
161
163
  const lexicographicStart = sql`(${sql.join(constraints, sql` OR `)})`;
162
164
  return leadingBound === void 0 ? lexicographicStart : sql`(${leadingBound} AND ${lexicographicStart})`;
@@ -1 +1 @@
1
- {"version":3,"file":"query-builder.js","names":[],"sources":["../../../../zqlite/src/query-builder.ts"],"sourcesContent":["import type {SQLQuery} from '@databases/sql';\nimport {assert} from '../../shared/src/asserts.ts';\nimport type {\n Condition,\n Ordering,\n SimpleCondition,\n ValuePosition,\n} from '../../zero-protocol/src/ast.ts';\nimport type {\n SchemaValue,\n ValueType,\n} from '../../zero-schema/src/table-schema.ts';\nimport type {Constraint} from '../../zql/src/ivm/constraint.ts';\nimport type {MultiConstraint, Start} from '../../zql/src/ivm/operator.ts';\nimport {sql} from './internal/sql.ts';\n\n/**\n * Condition type without correlated subqueries.\n * This matches the output of transformFilters from zql/builder/filter.ts\n */\nexport type NoSubqueryCondition = Exclude<\n Condition,\n {type: 'correlatedSubquery'}\n>;\n\nexport function buildSelectQuery(\n tableName: string,\n columns: Record<string, SchemaValue>,\n constraint: Constraint | undefined,\n filters: NoSubqueryCondition | undefined,\n order: Ordering | undefined,\n reverse: boolean | undefined,\n start: Start | undefined,\n multiConstraints?: readonly MultiConstraint[] | undefined,\n) {\n let query = sql`SELECT ${sql.join(\n Object.keys(columns).map(c => sql.ident(c)),\n sql`,`,\n )} FROM ${sql.ident(tableName)}`;\n const constraints: SQLQuery[] = constraintsToSQL(constraint, columns);\n\n if (multiConstraints) {\n for (const mc of multiConstraints) {\n if (mc.length > 0) {\n constraints.push(multiConstraintToSQL(mc, columns));\n }\n }\n }\n\n if (start) {\n assert(order !== undefined, 'start requires ordering');\n constraints.push(gatherStartConstraints(start, reverse, order, columns));\n }\n\n if (filters) {\n constraints.push(filtersToSQL(filters));\n }\n\n if (constraints.length > 0) {\n query = sql`${query} WHERE ${sql.join(constraints, sql` AND `)}`;\n }\n\n if (order && order.length > 0) {\n return sql`${query} ${orderByToSQL(order, !!reverse)}`;\n }\n return query;\n}\n\nexport function constraintsToSQL(\n constraint: Constraint | undefined,\n columns: Record<string, SchemaValue>,\n) {\n if (!constraint) {\n return [];\n }\n\n const constraints: SQLQuery[] = [];\n for (const [key, value] of Object.entries(constraint)) {\n constraints.push(\n sql`${sql.ident(key)} = ${toSQLiteType(value, columns[key].type)}`,\n );\n }\n\n return constraints;\n}\n\n/**\n * Builds a single batched IN clause from a `MultiConstraint`. All entries\n * are assumed to share the same shape (the keys of the first entry);\n * FlippedJoin derives them from the same parentKey for all children.\n *\n * Single-column form: `col IN (?, ?, ?)`\n * Compound form: `(a, b) IN (VALUES (?, ?), (?, ?), …)`\n *\n * NOTE: SQLite optimizes `col IN (literal-list)` using the column's index;\n * verified via EXPLAIN QUERY PLAN — see query-builder.test.ts.\n */\nexport function multiConstraintToSQL(\n multiConstraint: MultiConstraint,\n columns: Record<string, SchemaValue>,\n): SQLQuery {\n assert(multiConstraint.length > 0, 'multiConstraint must be non-empty');\n // All entries share the same keys; pull the column list from the first.\n const keys = Object.keys(multiConstraint[0]);\n assert(keys.length > 0, 'multiConstraint entries must have at least one key');\n // Subsequent entries must share the first entry's shape — the SQL form\n // is `(col_a, col_b, …) IN VALUES (…)`, with one binding per key per\n // entry. Heterogeneous keys would silently produce incorrect bindings.\n for (let i = 1; i < multiConstraint.length; i++) {\n const entry = multiConstraint[i];\n assert(\n Object.keys(entry).length === keys.length && keys.every(k => k in entry),\n () =>\n `multiConstraint entries must share the same keys (entry 0: [${keys.join(\n ',',\n )}], entry ${i}: [${Object.keys(entry).join(',')}])`,\n );\n }\n\n if (keys.length === 1) {\n const key = keys[0];\n const colType = columns[key].type;\n return sql`${sql.ident(key)} IN (${sql.join(\n multiConstraint.map(c => sql`${toSQLiteType(c[key], colType)}`),\n sql`,`,\n )})`;\n }\n\n // Compound: `(col_a, col_b, …) IN (VALUES (?, ?, …), …)`\n const colList = sql`(${sql.join(\n keys.map(k => sql.ident(k)),\n sql`,`,\n )})`;\n const rows = multiConstraint.map(\n c =>\n sql`(${sql.join(\n keys.map(k => sql`${toSQLiteType(c[k], columns[k].type)}`),\n sql`,`,\n )})`,\n );\n return sql`${colList} IN (VALUES ${sql.join(rows, sql`,`)})`;\n}\n\nexport function orderByToSQL(order: Ordering, reverse: boolean): SQLQuery {\n if (reverse) {\n return sql`ORDER BY ${sql.join(\n order.map(\n s =>\n sql`${sql.ident(s[0])} ${sql.__dangerous__rawValue(\n s[1] === 'asc' ? 'desc' : 'asc',\n )}`,\n ),\n sql`, `,\n )}`;\n } else {\n return sql`ORDER BY ${sql.join(\n order.map(\n s => sql`${sql.ident(s[0])} ${sql.__dangerous__rawValue(s[1])}`,\n ),\n sql`, `,\n )}`;\n }\n}\n\n/**\n * Converts filters (conditions) to SQL WHERE clause.\n * This applies all filters present in the AST for a query to the source.\n */\nexport function filtersToSQL(filters: NoSubqueryCondition): SQLQuery {\n switch (filters.type) {\n case 'simple':\n return simpleConditionToSQL(filters);\n case 'and':\n return filters.conditions.length > 0\n ? sql`(${sql.join(\n filters.conditions.map(condition =>\n filtersToSQL(condition as NoSubqueryCondition),\n ),\n sql` AND `,\n )})`\n : sql`TRUE`;\n case 'or':\n return filters.conditions.length > 0\n ? sql`(${sql.join(\n filters.conditions.map(condition =>\n filtersToSQL(condition as NoSubqueryCondition),\n ),\n sql` OR `,\n )})`\n : sql`FALSE`;\n }\n}\n\nfunction simpleConditionToSQL(filter: SimpleCondition): SQLQuery {\n const {op} = filter;\n if (op === 'IN' || op === 'NOT IN') {\n switch (filter.right.type) {\n case 'literal':\n return sql`${valuePositionToSQL(\n filter.left,\n )} ${sql.__dangerous__rawValue(\n filter.op,\n )} (SELECT value FROM json_each(${JSON.stringify(\n filter.right.value,\n )}))`;\n case 'static':\n throw new Error(\n 'Static parameters must be replaced before conversion to SQL',\n );\n }\n }\n if (\n op === 'LIKE' ||\n op === 'NOT LIKE' ||\n op === 'ILIKE' ||\n op === 'NOT ILIKE'\n ) {\n return likeConditionToSQL(filter);\n }\n\n return sql`${valuePositionToSQL(filter.left)} ${sql.__dangerous__rawValue(\n filter.op,\n )} ${valuePositionToSQL(filter.right)}`;\n}\n\nfunction likeConditionToSQL(filter: SimpleCondition): SQLQuery {\n const {op} = filter;\n // Mirror Postgres pattern-matching semantics:\n // * LIKE is case-sensitive. The replica connection runs with\n // `PRAGMA case_sensitive_like = ON` (see db.ts), so the bare LIKE\n // operator is case-sensitive.\n // * ILIKE is case-insensitive. We lower() both operands using the\n // Unicode-aware lower() that @rocicorp/zero-sqlite3 provides via ICU,\n // mirroring the toLowerCase() used by the in-memory IVM matcher\n // (see zql/src/builder/like.ts).\n // * Backslash is the default escape character in Postgres and in the IVM\n // matcher, but SQLite has no default, so we specify `ESCAPE '\\'`\n // explicitly. The SQL literal '\\' is a single backslash (SQLite does not\n // process backslash escapes inside string literals).\n const caseInsensitive = op === 'ILIKE' || op === 'NOT ILIKE';\n const negated = op === 'NOT LIKE' || op === 'NOT ILIKE';\n const likeOp = sql.__dangerous__rawValue(negated ? 'NOT LIKE' : 'LIKE');\n\n const left = valuePositionToSQL(filter.left);\n const right = valuePositionToSQL(filter.right);\n if (caseInsensitive) {\n return sql`lower(${left}) ${likeOp} lower(${right}) ESCAPE '\\\\'`;\n }\n return sql`${left} ${likeOp} ${right} ESCAPE '\\\\'`;\n}\n\nfunction valuePositionToSQL(value: ValuePosition): SQLQuery {\n switch (value.type) {\n case 'column':\n return sql.ident(value.name);\n case 'literal':\n return sql`${toSQLiteType(value.value, getJsType(value.value))}`;\n case 'static':\n throw new Error(\n 'Static parameters must be replaced before conversion to SQL',\n );\n }\n}\n\nfunction getJsType(value: unknown): ValueType {\n if (value === null) {\n return 'null';\n }\n return typeof value === 'string'\n ? 'string'\n : typeof value === 'number'\n ? 'number'\n : typeof value === 'boolean'\n ? 'boolean'\n : 'json';\n}\n\nexport function toSQLiteType(v: unknown, type: ValueType): unknown {\n switch (type) {\n case 'boolean':\n return v === null ? null : v ? 1 : 0;\n case 'number':\n case 'string':\n case 'null':\n return v;\n case 'json':\n return JSON.stringify(v);\n }\n}\n\nfunction nullableAwareEquality(\n field: string,\n value: unknown,\n columnType: SchemaValue,\n): SQLQuery {\n // Use = instead of IS for non-nullable columns to enable better\n // index usage in SQLite.\n return columnType.optional === true\n ? sql`${sql.ident(field)} IS ${value}`\n : sql`${sql.ident(field)} = ${value}`;\n}\n\nfunction nullableAwareRangeComparison(\n field: string,\n value: unknown,\n operator: '>' | '<',\n columnType: SchemaValue,\n): SQLQuery {\n // For non-nullable columns, skip IS NULL checks to avoid breaking\n // SQLite's MULTI-INDEX OR optimization, which falls back to a full\n // table scan when any OR branch involves NULL.\n // See: https://github.com/rocicorp/mono/pull/5542\n const comparison = sql`${sql.ident(field)} ${sql.__dangerous__rawValue(\n operator,\n )} ${value}`;\n if (columnType.optional !== true) {\n return comparison;\n }\n\n return operator === '>'\n ? sql`(${value} IS NULL OR ${comparison})`\n : sql`(${sql.ident(field)} IS NULL OR ${comparison})`;\n}\n\nfunction sargableLeadingStartBound(\n field: string,\n value: unknown,\n operator: '>' | '<',\n columnType: SchemaValue,\n): SQLQuery | undefined {\n if (columnType.optional === true) {\n return undefined;\n }\n\n const inclusiveOperator = operator === '>' ? '>=' : '<=';\n return sql`${sql.ident(field)} ${sql.__dangerous__rawValue(\n inclusiveOperator,\n )} ${value}`;\n}\n\n/**\n * The ordering could be complex such as:\n * `ORDER BY a ASC, b DESC, c ASC`\n *\n * In those cases, we need to encode the constraints as various\n * `OR` clauses.\n *\n * E.g.,\n *\n * to get the row after (a = 1, b = 2, c = 3) would be:\n *\n * `WHERE a > 1 OR (a = 1 AND b < 2) OR (a = 1 AND b = 2 AND c > 3)`\n *\n * - after vs before flips the comparison operators.\n * - inclusive adds a final `OR` clause for the exact match.\n */\nfunction gatherStartConstraints(\n start: Start,\n reverse: boolean | undefined,\n order: Ordering,\n columnTypes: Record<string, SchemaValue>,\n): SQLQuery {\n const constraints: SQLQuery[] = [];\n const {row: from, basis} = start;\n let leadingBound: SQLQuery | undefined;\n\n for (let i = 0; i < order.length; i++) {\n const group: SQLQuery[] = [];\n const [iField, iDirection] = order[i];\n for (let j = 0; j <= i; j++) {\n if (j === i) {\n const columnType = columnTypes[iField];\n const constraintValue = toSQLiteType(from[iField], columnType.type);\n const operator =\n iDirection === 'asc' ? (reverse ? '<' : '>') : reverse ? '>' : '<';\n if (i === 0) {\n leadingBound = sargableLeadingStartBound(\n iField,\n constraintValue,\n operator,\n columnType,\n );\n }\n group.push(\n nullableAwareRangeComparison(\n iField,\n constraintValue,\n operator,\n columnType,\n ),\n );\n } else {\n const [jField] = order[j];\n const columnType = columnTypes[jField];\n const value = toSQLiteType(from[jField], columnType.type);\n group.push(nullableAwareEquality(jField, value, columnType));\n }\n }\n constraints.push(sql`(${sql.join(group, sql` AND `)})`);\n }\n\n if (basis === 'at') {\n constraints.push(\n sql`(${sql.join(\n order.map(([field]) => {\n const columnType = columnTypes[field];\n const value = toSQLiteType(from[field], columnType.type);\n return nullableAwareEquality(field, value, columnType);\n }),\n sql` AND `,\n )})`,\n );\n }\n\n const lexicographicStart = sql`(${sql.join(constraints, sql` OR `)})`;\n return leadingBound === undefined\n ? lexicographicStart\n : sql`(${leadingBound} AND ${lexicographicStart})`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"query-builder.js","names":[],"sources":["../../../../zqlite/src/query-builder.ts"],"sourcesContent":["import type {SQLQuery} from '@databases/sql';\nimport {assert} from '../../shared/src/asserts.ts';\nimport type {\n Condition,\n Ordering,\n SimpleCondition,\n ValuePosition,\n} from '../../zero-protocol/src/ast.ts';\nimport type {\n SchemaValue,\n ValueType,\n} from '../../zero-schema/src/table-schema.ts';\nimport type {Constraint} from '../../zql/src/ivm/constraint.ts';\nimport type {MultiConstraint, Start} from '../../zql/src/ivm/operator.ts';\nimport {sql} from './internal/sql.ts';\n\n/**\n * Condition type without correlated subqueries.\n * This matches the output of transformFilters from zql/builder/filter.ts\n */\nexport type NoSubqueryCondition = Exclude<\n Condition,\n {type: 'correlatedSubquery'}\n>;\n\nexport function buildSelectQuery(\n tableName: string,\n columns: Record<string, SchemaValue>,\n constraint: Constraint | undefined,\n filters: NoSubqueryCondition | undefined,\n order: Ordering | undefined,\n reverse: boolean | undefined,\n start: Start | undefined,\n multiConstraints?: readonly MultiConstraint[] | undefined,\n) {\n let query = sql`SELECT ${sql.join(\n Object.keys(columns).map(c => sql.ident(c)),\n sql`,`,\n )} FROM ${sql.ident(tableName)}`;\n const constraints: SQLQuery[] = constraintsToSQL(constraint, columns);\n\n if (multiConstraints) {\n for (const mc of multiConstraints) {\n if (mc.length > 0) {\n constraints.push(multiConstraintToSQL(mc, columns));\n }\n }\n }\n\n if (start) {\n assert(order !== undefined, 'start requires ordering');\n constraints.push(gatherStartConstraints(start, reverse, order, columns));\n }\n\n if (filters) {\n constraints.push(filtersToSQL(filters));\n }\n\n if (constraints.length > 0) {\n query = sql`${query} WHERE ${sql.join(constraints, sql` AND `)}`;\n }\n\n if (order && order.length > 0) {\n return sql`${query} ${orderByToSQL(order, !!reverse)}`;\n }\n return query;\n}\n\nexport function constraintsToSQL(\n constraint: Constraint | undefined,\n columns: Record<string, SchemaValue>,\n) {\n if (!constraint) {\n return [];\n }\n\n const constraints: SQLQuery[] = [];\n for (const [key, value] of Object.entries(constraint)) {\n constraints.push(\n sql`${sql.ident(key)} = ${toSQLiteType(value, columns[key].type)}`,\n );\n }\n\n return constraints;\n}\n\n/**\n * Builds a single batched IN clause from a `MultiConstraint`. All entries\n * are assumed to share the same shape (the keys of the first entry);\n * FlippedJoin derives them from the same parentKey for all children.\n *\n * Single-column form: `col IN (?, ?, ?)`\n * Compound form: `(a, b) IN (VALUES (?, ?), (?, ?), …)`\n *\n * NOTE: SQLite optimizes `col IN (literal-list)` using the column's index;\n * verified via EXPLAIN QUERY PLAN — see query-builder.test.ts.\n */\nexport function multiConstraintToSQL(\n multiConstraint: MultiConstraint,\n columns: Record<string, SchemaValue>,\n): SQLQuery {\n assert(multiConstraint.length > 0, 'multiConstraint must be non-empty');\n // All entries share the same keys; pull the column list from the first.\n const keys = Object.keys(multiConstraint[0]);\n assert(keys.length > 0, 'multiConstraint entries must have at least one key');\n // Subsequent entries must share the first entry's shape — the SQL form\n // is `(col_a, col_b, …) IN VALUES (…)`, with one binding per key per\n // entry. Heterogeneous keys would silently produce incorrect bindings.\n for (let i = 1; i < multiConstraint.length; i++) {\n const entry = multiConstraint[i];\n assert(\n Object.keys(entry).length === keys.length && keys.every(k => k in entry),\n () =>\n `multiConstraint entries must share the same keys (entry 0: [${keys.join(\n ',',\n )}], entry ${i}: [${Object.keys(entry).join(',')}])`,\n );\n }\n\n if (keys.length === 1) {\n const key = keys[0];\n const colType = columns[key].type;\n return sql`${sql.ident(key)} IN (${sql.join(\n multiConstraint.map(c => sql`${toSQLiteType(c[key], colType)}`),\n sql`,`,\n )})`;\n }\n\n // Compound: `(col_a, col_b, …) IN (VALUES (?, ?, …), …)`\n const colList = sql`(${sql.join(\n keys.map(k => sql.ident(k)),\n sql`,`,\n )})`;\n const rows = multiConstraint.map(\n c =>\n sql`(${sql.join(\n keys.map(k => sql`${toSQLiteType(c[k], columns[k].type)}`),\n sql`,`,\n )})`,\n );\n return sql`${colList} IN (VALUES ${sql.join(rows, sql`,`)})`;\n}\n\nexport function orderByToSQL(order: Ordering, reverse: boolean): SQLQuery {\n if (reverse) {\n return sql`ORDER BY ${sql.join(\n order.map(\n s =>\n sql`${sql.ident(s[0])} ${sql.__dangerous__rawValue(\n s[1] === 'asc' ? 'desc' : 'asc',\n )}`,\n ),\n sql`, `,\n )}`;\n } else {\n return sql`ORDER BY ${sql.join(\n order.map(\n s => sql`${sql.ident(s[0])} ${sql.__dangerous__rawValue(s[1])}`,\n ),\n sql`, `,\n )}`;\n }\n}\n\n/**\n * Converts filters (conditions) to SQL WHERE clause.\n * This applies all filters present in the AST for a query to the source.\n */\nexport function filtersToSQL(filters: NoSubqueryCondition): SQLQuery {\n switch (filters.type) {\n case 'simple':\n return simpleConditionToSQL(filters);\n case 'and':\n return filters.conditions.length > 0\n ? sql`(${sql.join(\n filters.conditions.map(condition =>\n filtersToSQL(condition as NoSubqueryCondition),\n ),\n sql` AND `,\n )})`\n : sql`TRUE`;\n case 'or':\n return filters.conditions.length > 0\n ? sql`(${sql.join(\n filters.conditions.map(condition =>\n filtersToSQL(condition as NoSubqueryCondition),\n ),\n sql` OR `,\n )})`\n : sql`FALSE`;\n }\n}\n\nfunction simpleConditionToSQL(filter: SimpleCondition): SQLQuery {\n const {op} = filter;\n if (op === 'IN' || op === 'NOT IN') {\n switch (filter.right.type) {\n case 'literal':\n return sql`${valuePositionToSQL(\n filter.left,\n )} ${sql.__dangerous__rawValue(\n filter.op,\n )} (SELECT value FROM json_each(${JSON.stringify(\n filter.right.value,\n )}))`;\n case 'static':\n throw new Error(\n 'Static parameters must be replaced before conversion to SQL',\n );\n }\n }\n if (\n op === 'LIKE' ||\n op === 'NOT LIKE' ||\n op === 'ILIKE' ||\n op === 'NOT ILIKE'\n ) {\n return likeConditionToSQL(filter);\n }\n\n return sql`${valuePositionToSQL(filter.left)} ${sql.__dangerous__rawValue(\n filter.op,\n )} ${valuePositionToSQL(filter.right)}`;\n}\n\nfunction likeConditionToSQL(filter: SimpleCondition): SQLQuery {\n const {op} = filter;\n // Mirror Postgres pattern-matching semantics:\n // * LIKE is case-sensitive. The replica connection runs with\n // `PRAGMA case_sensitive_like = ON` (see db.ts), so the bare LIKE\n // operator is case-sensitive.\n // * ILIKE is case-insensitive. We lower() both operands using the\n // Unicode-aware lower() that @rocicorp/zero-sqlite3 provides via ICU,\n // mirroring the toLowerCase() used by the in-memory IVM matcher\n // (see zql/src/builder/like.ts).\n // * Backslash is the default escape character in Postgres and in the IVM\n // matcher, but SQLite has no default, so we specify `ESCAPE '\\'`\n // explicitly. The SQL literal '\\' is a single backslash (SQLite does not\n // process backslash escapes inside string literals).\n const caseInsensitive = op === 'ILIKE' || op === 'NOT ILIKE';\n const negated = op === 'NOT LIKE' || op === 'NOT ILIKE';\n const likeOp = sql.__dangerous__rawValue(negated ? 'NOT LIKE' : 'LIKE');\n\n const left = valuePositionToSQL(filter.left);\n const right = valuePositionToSQL(filter.right);\n if (caseInsensitive) {\n return sql`lower(${left}) ${likeOp} lower(${right}) ESCAPE '\\\\'`;\n }\n return sql`${left} ${likeOp} ${right} ESCAPE '\\\\'`;\n}\n\nfunction valuePositionToSQL(value: ValuePosition): SQLQuery {\n switch (value.type) {\n case 'column':\n return sql.ident(value.name);\n case 'literal':\n return sql`${toSQLiteType(value.value, getJsType(value.value))}`;\n case 'static':\n throw new Error(\n 'Static parameters must be replaced before conversion to SQL',\n );\n }\n}\n\nfunction getJsType(value: unknown): ValueType {\n if (value === null) {\n return 'null';\n }\n return typeof value === 'string'\n ? 'string'\n : typeof value === 'number'\n ? 'number'\n : typeof value === 'boolean'\n ? 'boolean'\n : 'json';\n}\n\nexport function toSQLiteType(v: unknown, type: ValueType): unknown {\n switch (type) {\n case 'boolean':\n return v === null ? null : v ? 1 : 0;\n case 'number':\n case 'string':\n case 'null':\n return v;\n case 'json':\n return JSON.stringify(v);\n }\n}\n\nfunction nullableAwareEquality(\n field: string,\n value: unknown,\n columnType: SchemaValue,\n): SQLQuery {\n // Use = instead of IS for non-nullable columns to enable better\n // index usage in SQLite.\n return columnType.optional === true\n ? sql`${sql.ident(field)} IS ${value}`\n : sql`${sql.ident(field)} = ${value}`;\n}\n\nfunction nullableAwareRangeComparison(\n field: string,\n value: unknown,\n operator: '>' | '<',\n columnType: SchemaValue,\n): SQLQuery {\n if (value === null) {\n return operator === '>' ? sql`${sql.ident(field)} IS NOT NULL` : sql`FALSE`;\n }\n\n // For non-nullable columns, skip IS NULL checks to avoid breaking\n // SQLite's MULTI-INDEX OR optimization, which falls back to a full\n // table scan when any OR branch involves NULL.\n // See: https://github.com/rocicorp/mono/pull/5542\n const comparison = sql`${sql.ident(field)} ${sql.__dangerous__rawValue(\n operator,\n )} ${value}`;\n if (columnType.optional !== true) {\n return comparison;\n }\n\n return operator === '>'\n ? sql`(${value} IS NULL OR ${comparison})`\n : sql`(${sql.ident(field)} IS NULL OR ${comparison})`;\n}\n\nfunction sargableLeadingStartBound(\n field: string,\n value: unknown,\n operator: '>' | '<',\n columnType: SchemaValue,\n): SQLQuery | undefined {\n if (value === null) {\n return undefined;\n }\n\n if (columnType.optional === true) {\n return undefined;\n }\n\n const inclusiveOperator = operator === '>' ? '>=' : '<=';\n return sql`${sql.ident(field)} ${sql.__dangerous__rawValue(\n inclusiveOperator,\n )} ${value}`;\n}\n\n/**\n * The ordering could be complex such as:\n * `ORDER BY a ASC, b DESC, c ASC`\n *\n * In those cases, we need to encode the constraints as various\n * `OR` clauses.\n *\n * E.g.,\n *\n * to get the row after (a = 1, b = 2, c = 3) would be:\n *\n * `WHERE a > 1 OR (a = 1 AND b < 2) OR (a = 1 AND b = 2 AND c > 3)`\n *\n * - after vs before flips the comparison operators.\n * - inclusive adds a final `OR` clause for the exact match.\n */\nfunction gatherStartConstraints(\n start: Start,\n reverse: boolean | undefined,\n order: Ordering,\n columnTypes: Record<string, SchemaValue>,\n): SQLQuery {\n const constraints: SQLQuery[] = [];\n const {row: from, basis} = start;\n let leadingBound: SQLQuery | undefined;\n\n for (let i = 0; i < order.length; i++) {\n const group: SQLQuery[] = [];\n const [iField, iDirection] = order[i];\n for (let j = 0; j <= i; j++) {\n if (j === i) {\n const columnType = columnTypes[iField];\n const constraintValue = toSQLiteType(\n from[iField] ?? null,\n columnType.type,\n );\n const operator =\n iDirection === 'asc' ? (reverse ? '<' : '>') : reverse ? '>' : '<';\n if (i === 0) {\n leadingBound = sargableLeadingStartBound(\n iField,\n constraintValue,\n operator,\n columnType,\n );\n }\n group.push(\n nullableAwareRangeComparison(\n iField,\n constraintValue,\n operator,\n columnType,\n ),\n );\n } else {\n const [jField] = order[j];\n const columnType = columnTypes[jField];\n const value = toSQLiteType(from[jField] ?? null, columnType.type);\n group.push(nullableAwareEquality(jField, value, columnType));\n }\n }\n constraints.push(sql`(${sql.join(group, sql` AND `)})`);\n }\n\n if (basis === 'at') {\n constraints.push(\n sql`(${sql.join(\n order.map(([field]) => {\n const columnType = columnTypes[field];\n const value = toSQLiteType(from[field] ?? null, columnType.type);\n return nullableAwareEquality(field, value, columnType);\n }),\n sql` AND `,\n )})`,\n );\n }\n\n const lexicographicStart = sql`(${sql.join(constraints, sql` OR `)})`;\n return leadingBound === undefined\n ? lexicographicStart\n : sql`(${leadingBound} AND ${lexicographicStart})`;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocicorp/zero",
3
- "version": "1.8.0-canary.3",
3
+ "version": "1.8.0-canary.5",
4
4
  "description": "Zero is a web framework for serverless web development.",
5
5
  "homepage": "https://zero.rocicorp.dev",
6
6
  "bugs": {