@rocicorp/zero 0.25.0-canary.13 → 0.25.0-canary.15
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/shared/src/record-proxy.d.ts +13 -0
- package/out/shared/src/record-proxy.d.ts.map +1 -0
- package/out/shared/src/record-proxy.js +59 -0
- package/out/shared/src/record-proxy.js.map +1 -0
- package/out/zero/package.json.js +1 -1
- package/out/zero-cache/src/auth/write-authorizer.d.ts.map +1 -1
- package/out/zero-cache/src/auth/write-authorizer.js +20 -13
- package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +8 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +12 -0
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/server/otel-start.d.ts.map +1 -1
- package/out/zero-cache/src/server/otel-start.js +1 -5
- package/out/zero-cache/src/server/otel-start.js.map +1 -1
- package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/server/syncer.js +6 -1
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +8 -9
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +17 -11
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +2 -2
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js +19 -4
- package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +1 -7
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-client/src/client/crud.d.ts +3 -3
- package/out/zero-client/src/client/crud.d.ts.map +1 -1
- package/out/zero-client/src/client/crud.js +23 -13
- package/out/zero-client/src/client/crud.js.map +1 -1
- package/out/zero-client/src/client/custom.d.ts.map +1 -1
- package/out/zero-client/src/client/custom.js +4 -11
- package/out/zero-client/src/client/custom.js.map +1 -1
- package/out/zero-client/src/client/ivm-branch.d.ts.map +1 -1
- package/out/zero-client/src/client/ivm-branch.js +20 -13
- package/out/zero-client/src/client/ivm-branch.js.map +1 -1
- package/out/zero-client/src/client/make-mutate-property.d.ts +1 -1
- package/out/zero-client/src/client/make-mutate-property.d.ts.map +1 -1
- package/out/zero-client/src/client/make-mutate-property.js.map +1 -1
- package/out/zero-client/src/client/make-replicache-mutators.d.ts.map +1 -1
- package/out/zero-client/src/client/make-replicache-mutators.js +10 -6
- package/out/zero-client/src/client/make-replicache-mutators.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero.js +1 -1
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-server/src/custom.d.ts +2 -2
- package/out/zero-server/src/custom.d.ts.map +1 -1
- package/out/zero-server/src/custom.js +52 -32
- package/out/zero-server/src/custom.js.map +1 -1
- package/out/zero-server/src/zql-database.d.ts.map +1 -1
- package/out/zero-server/src/zql-database.js +1 -5
- package/out/zero-server/src/zql-database.js.map +1 -1
- package/out/zero-solid/src/solid-view.d.ts +1 -1
- package/out/zero-solid/src/solid-view.d.ts.map +1 -1
- package/out/zero-solid/src/solid-view.js +2 -0
- package/out/zero-solid/src/solid-view.js.map +1 -1
- package/out/zero-types/src/schema.d.ts +4 -4
- package/out/zql/src/builder/builder.d.ts.map +1 -1
- package/out/zql/src/builder/builder.js +1 -11
- package/out/zql/src/builder/builder.js.map +1 -1
- package/out/zql/src/error.js +1 -10
- package/out/zql/src/error.js.map +1 -1
- package/out/zql/src/ivm/array-view.d.ts +1 -1
- package/out/zql/src/ivm/array-view.d.ts.map +1 -1
- package/out/zql/src/ivm/array-view.js +2 -0
- package/out/zql/src/ivm/array-view.js.map +1 -1
- package/out/zql/src/ivm/exists.d.ts +3 -2
- package/out/zql/src/ivm/exists.d.ts.map +1 -1
- package/out/zql/src/ivm/exists.js +25 -23
- package/out/zql/src/ivm/exists.js.map +1 -1
- package/out/zql/src/ivm/fan-in.d.ts +3 -3
- package/out/zql/src/ivm/fan-in.d.ts.map +1 -1
- package/out/zql/src/ivm/fan-in.js +6 -5
- package/out/zql/src/ivm/fan-in.js.map +1 -1
- package/out/zql/src/ivm/fan-out.d.ts +2 -2
- package/out/zql/src/ivm/fan-out.d.ts.map +1 -1
- package/out/zql/src/ivm/fan-out.js +5 -5
- package/out/zql/src/ivm/fan-out.js.map +1 -1
- package/out/zql/src/ivm/filter-operators.d.ts +5 -5
- package/out/zql/src/ivm/filter-operators.d.ts.map +1 -1
- package/out/zql/src/ivm/filter-operators.js +8 -8
- package/out/zql/src/ivm/filter-operators.js.map +1 -1
- package/out/zql/src/ivm/filter-push.d.ts +2 -1
- package/out/zql/src/ivm/filter-push.d.ts.map +1 -1
- package/out/zql/src/ivm/filter-push.js +5 -5
- package/out/zql/src/ivm/filter-push.js.map +1 -1
- package/out/zql/src/ivm/filter.d.ts +2 -2
- package/out/zql/src/ivm/filter.d.ts.map +1 -1
- package/out/zql/src/ivm/filter.js +4 -4
- package/out/zql/src/ivm/filter.js.map +1 -1
- package/out/zql/src/ivm/flipped-join.d.ts.map +1 -1
- package/out/zql/src/ivm/flipped-join.js +100 -83
- package/out/zql/src/ivm/flipped-join.js.map +1 -1
- package/out/zql/src/ivm/join.d.ts.map +1 -1
- package/out/zql/src/ivm/join.js +52 -50
- package/out/zql/src/ivm/join.js.map +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.d.ts +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.d.ts.map +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.js +4 -4
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.js.map +1 -1
- package/out/zql/src/ivm/memory-source.d.ts +3 -3
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +7 -4
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/ivm/operator.d.ts +10 -3
- package/out/zql/src/ivm/operator.d.ts.map +1 -1
- package/out/zql/src/ivm/operator.js.map +1 -1
- package/out/zql/src/ivm/push-accumulated.d.ts +1 -1
- package/out/zql/src/ivm/push-accumulated.d.ts.map +1 -1
- package/out/zql/src/ivm/push-accumulated.js +8 -8
- package/out/zql/src/ivm/push-accumulated.js.map +1 -1
- package/out/zql/src/ivm/skip.d.ts +1 -1
- package/out/zql/src/ivm/skip.d.ts.map +1 -1
- package/out/zql/src/ivm/skip.js +8 -3
- package/out/zql/src/ivm/skip.js.map +1 -1
- package/out/zql/src/ivm/source.d.ts +15 -7
- package/out/zql/src/ivm/source.d.ts.map +1 -1
- package/out/zql/src/ivm/stream.d.ts +2 -0
- package/out/zql/src/ivm/stream.d.ts.map +1 -1
- package/out/zql/src/ivm/stream.js +5 -14
- package/out/zql/src/ivm/stream.js.map +1 -1
- package/out/zql/src/ivm/take.d.ts +1 -1
- package/out/zql/src/ivm/take.d.ts.map +1 -1
- package/out/zql/src/ivm/take.js +164 -147
- package/out/zql/src/ivm/take.js.map +1 -1
- package/out/zql/src/ivm/union-fan-in.d.ts +2 -2
- package/out/zql/src/ivm/union-fan-in.d.ts.map +1 -1
- package/out/zql/src/ivm/union-fan-in.js +7 -7
- package/out/zql/src/ivm/union-fan-in.js.map +1 -1
- package/out/zql/src/ivm/union-fan-out.d.ts +1 -1
- package/out/zql/src/ivm/union-fan-out.d.ts.map +1 -1
- package/out/zql/src/ivm/union-fan-out.js +3 -3
- package/out/zql/src/ivm/union-fan-out.js.map +1 -1
- package/out/zql/src/mutate/mutator-registry.d.ts.map +1 -1
- package/out/zql/src/mutate/mutator-registry.js +1 -0
- package/out/zql/src/mutate/mutator-registry.js.map +1 -1
- package/out/zql/src/mutate/mutator.d.ts +10 -0
- package/out/zql/src/mutate/mutator.d.ts.map +1 -1
- package/out/zql/src/mutate/mutator.js.map +1 -1
- package/out/zql/src/planner/planner-builder.d.ts +2 -1
- package/out/zql/src/planner/planner-builder.d.ts.map +1 -1
- package/out/zql/src/planner/planner-builder.js +5 -5
- package/out/zql/src/planner/planner-builder.js.map +1 -1
- package/out/zql/src/planner/planner-graph.d.ts +3 -1
- package/out/zql/src/planner/planner-graph.d.ts.map +1 -1
- package/out/zql/src/planner/planner-graph.js +5 -5
- package/out/zql/src/planner/planner-graph.js.map +1 -1
- package/out/zql/src/query/create-builder.d.ts.map +1 -1
- package/out/zql/src/query/create-builder.js +7 -36
- package/out/zql/src/query/create-builder.js.map +1 -1
- package/out/zql/src/query/measure-push-operator.d.ts +1 -1
- package/out/zql/src/query/measure-push-operator.d.ts.map +1 -1
- package/out/zql/src/query/measure-push-operator.js +2 -2
- package/out/zql/src/query/measure-push-operator.js.map +1 -1
- package/out/zqlite/src/internal/sql-inline.d.ts +13 -0
- package/out/zqlite/src/internal/sql-inline.d.ts.map +1 -0
- package/out/zqlite/src/internal/sql-inline.js +45 -0
- package/out/zqlite/src/internal/sql-inline.js.map +1 -0
- package/out/zqlite/src/sqlite-cost-model.d.ts.map +1 -1
- package/out/zqlite/src/sqlite-cost-model.js +2 -2
- package/out/zqlite/src/sqlite-cost-model.js.map +1 -1
- package/out/zqlite/src/table-source.d.ts +3 -2
- package/out/zqlite/src/table-source.d.ts.map +1 -1
- package/out/zqlite/src/table-source.js +5 -2
- package/out/zqlite/src/table-source.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"planner-builder.js","sources":["../../../../../zql/src/planner/planner-builder.ts"],"sourcesContent":["import {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport type {\n AST,\n Condition,\n Conjunction,\n CorrelatedSubqueryCondition,\n Disjunction,\n} from '../../../zero-protocol/src/ast.ts';\nimport {planIdSymbol} from '../../../zero-protocol/src/ast.ts';\nimport type {ConnectionCostModel} from './planner-connection.ts';\nimport type {PlannerConstraint} from './planner-constraint.ts';\nimport type {PlanDebugger} from './planner-debug.ts';\nimport {PlannerFanIn} from './planner-fan-in.ts';\nimport {PlannerFanOut} from './planner-fan-out.ts';\nimport {PlannerGraph} from './planner-graph.ts';\nimport {PlannerJoin} from './planner-join.ts';\nimport type {PlannerNode} from './planner-node.ts';\nimport {PlannerTerminus} from './planner-terminus.ts';\n\nfunction wireOutput(from: PlannerNode, to: PlannerNode): void {\n switch (from.kind) {\n case 'connection':\n case 'join':\n case 'fan-in':\n from.setOutput(to);\n break;\n case 'fan-out':\n from.addOutput(to);\n break;\n case 'terminus':\n assert(false, 'Terminus nodes cannot have outputs');\n }\n}\n\nexport type Plans = {\n plan: PlannerGraph;\n subPlans: {[key: string]: Plans};\n};\n\nexport function buildPlanGraph(\n ast: AST,\n model: ConnectionCostModel,\n isRoot: boolean,\n baseConstraints?: PlannerConstraint,\n): Plans {\n const graph = new PlannerGraph();\n let nextPlanId = 0;\n\n const source = graph.addSource(ast.table, model);\n const connection = source.connect(\n ast.orderBy ?? [],\n ast.where,\n isRoot,\n baseConstraints,\n ast.limit,\n );\n graph.connections.push(connection);\n\n let end: PlannerNode = connection;\n if (ast.where) {\n end = processCondition(\n ast.where,\n end,\n graph,\n model,\n ast.table,\n () => nextPlanId++,\n );\n }\n\n const terminus = new PlannerTerminus(end);\n wireOutput(end, terminus);\n graph.setTerminus(terminus);\n\n const subPlans: {[key: string]: Plans} = {};\n if (ast.related) {\n for (const csq of ast.related) {\n const alias = must(\n csq.subquery.alias,\n 'Related subquery must have alias',\n );\n const childConstraints = extractConstraint(\n csq.correlation.childField,\n csq.subquery.table,\n );\n subPlans[alias] = buildPlanGraph(\n csq.subquery,\n model,\n true,\n childConstraints,\n );\n }\n }\n\n return {plan: graph, subPlans};\n}\n\nfunction processCondition(\n condition: Condition,\n input: Exclude<PlannerNode, PlannerTerminus>,\n graph: PlannerGraph,\n model: ConnectionCostModel,\n parentTable: string,\n getPlanId: () => number,\n): Exclude<PlannerNode, PlannerTerminus> {\n switch (condition.type) {\n case 'simple':\n return input;\n case 'and':\n return processAnd(condition, input, graph, model, parentTable, getPlanId);\n case 'or':\n return processOr(condition, input, graph, model, parentTable, getPlanId);\n case 'correlatedSubquery':\n return processCorrelatedSubquery(\n condition,\n input,\n graph,\n model,\n parentTable,\n getPlanId,\n );\n }\n}\n\nfunction processAnd(\n condition: Conjunction,\n input: Exclude<PlannerNode, PlannerTerminus>,\n graph: PlannerGraph,\n model: ConnectionCostModel,\n parentTable: string,\n getPlanId: () => number,\n): Exclude<PlannerNode, PlannerTerminus> {\n let end = input;\n for (const subCondition of condition.conditions) {\n end = processCondition(\n subCondition,\n end,\n graph,\n model,\n parentTable,\n getPlanId,\n );\n }\n return end;\n}\n\nfunction processOr(\n condition: Disjunction,\n input: Exclude<PlannerNode, PlannerTerminus>,\n graph: PlannerGraph,\n model: ConnectionCostModel,\n parentTable: string,\n getPlanId: () => number,\n): Exclude<PlannerNode, PlannerTerminus> {\n const subqueryConditions = condition.conditions.filter(\n c => c.type === 'correlatedSubquery' || hasCorrelatedSubquery(c),\n );\n\n if (subqueryConditions.length === 0) {\n return input;\n }\n\n const fanOut = new PlannerFanOut(input);\n graph.fanOuts.push(fanOut);\n wireOutput(input, fanOut);\n\n const branches: Exclude<PlannerNode, PlannerTerminus>[] = [];\n for (const subCondition of subqueryConditions) {\n const branch = processCondition(\n subCondition,\n fanOut,\n graph,\n model,\n parentTable,\n getPlanId,\n );\n branches.push(branch);\n fanOut.addOutput(branch);\n }\n\n const fanIn = new PlannerFanIn(branches);\n graph.fanIns.push(fanIn);\n for (const branch of branches) {\n wireOutput(branch, fanIn);\n }\n\n return fanIn;\n}\n\nfunction processCorrelatedSubquery(\n condition: CorrelatedSubqueryCondition,\n input: Exclude<PlannerNode, PlannerTerminus>,\n graph: PlannerGraph,\n model: ConnectionCostModel,\n parentTable: string,\n getPlanId: () => number,\n): Exclude<PlannerNode, PlannerTerminus> {\n const {related} = condition;\n const childTable = related.subquery.table;\n\n const childSource = graph.hasSource(childTable)\n ? graph.getSource(childTable)\n : graph.addSource(childTable, model);\n\n const childConnection = childSource.connect(\n related.subquery.orderBy ?? [],\n related.subquery.where,\n false,\n undefined, // no base constraints for EXISTS/NOT EXISTS\n condition.op === 'EXISTS' ? 1 : undefined,\n );\n graph.connections.push(childConnection);\n\n let childEnd: PlannerNode = childConnection;\n if (related.subquery.where) {\n childEnd = processCondition(\n related.subquery.where,\n childEnd,\n graph,\n model,\n childTable,\n getPlanId,\n );\n }\n\n const parentConstraint = extractConstraint(\n related.correlation.parentField,\n parentTable,\n );\n const childConstraint = extractConstraint(\n related.correlation.childField,\n childTable,\n );\n\n const planId = getPlanId();\n condition[planIdSymbol] = planId;\n\n // Determine flippability and initial type based on flip flag and operator\n const isNotExists = condition.op === 'NOT EXISTS';\n const manualFlip = condition.flip;\n\n let flippable: boolean;\n let initialType: 'semi' | 'flipped';\n\n if (isNotExists) {\n // NOT EXISTS joins can never be flipped\n flippable = false;\n initialType = 'semi';\n } else if (manualFlip === true) {\n // User explicitly requested flip=true: start flipped, don't allow planner to change\n flippable = false;\n initialType = 'flipped';\n } else if (manualFlip === false) {\n // User explicitly requested flip=false: start semi, don't allow planner to change\n flippable = false;\n initialType = 'semi';\n } else {\n // flip is undefined: planner can decide\n flippable = true;\n initialType = 'semi';\n }\n\n const join = new PlannerJoin(\n input,\n childEnd,\n parentConstraint,\n childConstraint,\n flippable,\n planId,\n initialType,\n );\n graph.joins.push(join);\n\n wireOutput(input, join);\n wireOutput(childEnd, join);\n\n return join;\n}\n\nfunction hasCorrelatedSubquery(condition: Condition): boolean {\n if (condition.type === 'correlatedSubquery') {\n return true;\n }\n if (condition.type === 'and' || condition.type === 'or') {\n return condition.conditions.some(hasCorrelatedSubquery);\n }\n return false;\n}\n\nfunction extractConstraint(\n fields: readonly string[],\n _tableName: string,\n): PlannerConstraint {\n return Object.fromEntries(fields.map(field => [field, undefined]));\n}\n\nfunction planRecursively(plans: Plans, planDebugger?: PlanDebugger): void {\n for (const subPlan of Object.values(plans.subPlans)) {\n planRecursively(subPlan, planDebugger);\n }\n plans.plan.plan(planDebugger);\n}\n\nexport function planQuery(\n ast: AST,\n model: ConnectionCostModel,\n planDebugger?: PlanDebugger,\n): AST {\n const plans = buildPlanGraph(ast, model, true);\n planRecursively(plans, planDebugger);\n return applyPlansToAST(ast, plans);\n}\n\nfunction applyToCondition(\n condition: Condition,\n flippedIds: Set<number>,\n): Condition {\n if (condition.type === 'simple') {\n return condition;\n }\n\n if (condition.type === 'correlatedSubquery') {\n const planId = (condition as unknown as Record<symbol, number>)[\n planIdSymbol\n ];\n const shouldFlip = planId !== undefined && flippedIds.has(planId);\n\n return {\n ...condition,\n flip: shouldFlip,\n related: {\n ...condition.related,\n subquery: {\n ...condition.related.subquery,\n where: condition.related.subquery.where\n ? applyToCondition(condition.related.subquery.where, flippedIds)\n : undefined,\n },\n },\n };\n }\n\n return {\n ...condition,\n conditions: condition.conditions.map(c => applyToCondition(c, flippedIds)),\n };\n}\n\nexport function applyPlansToAST(ast: AST, plans: Plans): AST {\n const flippedIds = new Set<number>();\n for (const join of plans.plan.joins) {\n if (join.type === 'flipped' && join.planId !== undefined) {\n flippedIds.add(join.planId);\n }\n }\n\n return {\n ...ast,\n where: ast.where ? applyToCondition(ast.where, flippedIds) : undefined,\n related: ast.related?.map(csq => {\n const alias = must(\n csq.subquery.alias,\n 'Related subquery must have alias',\n );\n const subPlan = plans.subPlans[alias];\n return {\n ...csq,\n subquery: subPlan\n ? applyPlansToAST(csq.subquery, subPlan)\n : csq.subquery,\n };\n }),\n };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"planner-builder.js","sources":["../../../../../zql/src/planner/planner-builder.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport type {\n AST,\n Condition,\n Conjunction,\n CorrelatedSubqueryCondition,\n Disjunction,\n} from '../../../zero-protocol/src/ast.ts';\nimport {planIdSymbol} from '../../../zero-protocol/src/ast.ts';\nimport type {ConnectionCostModel} from './planner-connection.ts';\nimport type {PlannerConstraint} from './planner-constraint.ts';\nimport type {PlanDebugger} from './planner-debug.ts';\nimport {PlannerFanIn} from './planner-fan-in.ts';\nimport {PlannerFanOut} from './planner-fan-out.ts';\nimport {PlannerGraph} from './planner-graph.ts';\nimport {PlannerJoin} from './planner-join.ts';\nimport type {PlannerNode} from './planner-node.ts';\nimport {PlannerTerminus} from './planner-terminus.ts';\n\nfunction wireOutput(from: PlannerNode, to: PlannerNode): void {\n switch (from.kind) {\n case 'connection':\n case 'join':\n case 'fan-in':\n from.setOutput(to);\n break;\n case 'fan-out':\n from.addOutput(to);\n break;\n case 'terminus':\n assert(false, 'Terminus nodes cannot have outputs');\n }\n}\n\nexport type Plans = {\n plan: PlannerGraph;\n subPlans: {[key: string]: Plans};\n};\n\nexport function buildPlanGraph(\n ast: AST,\n model: ConnectionCostModel,\n isRoot: boolean,\n baseConstraints?: PlannerConstraint,\n): Plans {\n const graph = new PlannerGraph();\n let nextPlanId = 0;\n\n const source = graph.addSource(ast.table, model);\n const connection = source.connect(\n ast.orderBy ?? [],\n ast.where,\n isRoot,\n baseConstraints,\n ast.limit,\n );\n graph.connections.push(connection);\n\n let end: PlannerNode = connection;\n if (ast.where) {\n end = processCondition(\n ast.where,\n end,\n graph,\n model,\n ast.table,\n () => nextPlanId++,\n );\n }\n\n const terminus = new PlannerTerminus(end);\n wireOutput(end, terminus);\n graph.setTerminus(terminus);\n\n const subPlans: {[key: string]: Plans} = {};\n if (ast.related) {\n for (const csq of ast.related) {\n const alias = must(\n csq.subquery.alias,\n 'Related subquery must have alias',\n );\n const childConstraints = extractConstraint(\n csq.correlation.childField,\n csq.subquery.table,\n );\n subPlans[alias] = buildPlanGraph(\n csq.subquery,\n model,\n true,\n childConstraints,\n );\n }\n }\n\n return {plan: graph, subPlans};\n}\n\nfunction processCondition(\n condition: Condition,\n input: Exclude<PlannerNode, PlannerTerminus>,\n graph: PlannerGraph,\n model: ConnectionCostModel,\n parentTable: string,\n getPlanId: () => number,\n): Exclude<PlannerNode, PlannerTerminus> {\n switch (condition.type) {\n case 'simple':\n return input;\n case 'and':\n return processAnd(condition, input, graph, model, parentTable, getPlanId);\n case 'or':\n return processOr(condition, input, graph, model, parentTable, getPlanId);\n case 'correlatedSubquery':\n return processCorrelatedSubquery(\n condition,\n input,\n graph,\n model,\n parentTable,\n getPlanId,\n );\n }\n}\n\nfunction processAnd(\n condition: Conjunction,\n input: Exclude<PlannerNode, PlannerTerminus>,\n graph: PlannerGraph,\n model: ConnectionCostModel,\n parentTable: string,\n getPlanId: () => number,\n): Exclude<PlannerNode, PlannerTerminus> {\n let end = input;\n for (const subCondition of condition.conditions) {\n end = processCondition(\n subCondition,\n end,\n graph,\n model,\n parentTable,\n getPlanId,\n );\n }\n return end;\n}\n\nfunction processOr(\n condition: Disjunction,\n input: Exclude<PlannerNode, PlannerTerminus>,\n graph: PlannerGraph,\n model: ConnectionCostModel,\n parentTable: string,\n getPlanId: () => number,\n): Exclude<PlannerNode, PlannerTerminus> {\n const subqueryConditions = condition.conditions.filter(\n c => c.type === 'correlatedSubquery' || hasCorrelatedSubquery(c),\n );\n\n if (subqueryConditions.length === 0) {\n return input;\n }\n\n const fanOut = new PlannerFanOut(input);\n graph.fanOuts.push(fanOut);\n wireOutput(input, fanOut);\n\n const branches: Exclude<PlannerNode, PlannerTerminus>[] = [];\n for (const subCondition of subqueryConditions) {\n const branch = processCondition(\n subCondition,\n fanOut,\n graph,\n model,\n parentTable,\n getPlanId,\n );\n branches.push(branch);\n fanOut.addOutput(branch);\n }\n\n const fanIn = new PlannerFanIn(branches);\n graph.fanIns.push(fanIn);\n for (const branch of branches) {\n wireOutput(branch, fanIn);\n }\n\n return fanIn;\n}\n\nfunction processCorrelatedSubquery(\n condition: CorrelatedSubqueryCondition,\n input: Exclude<PlannerNode, PlannerTerminus>,\n graph: PlannerGraph,\n model: ConnectionCostModel,\n parentTable: string,\n getPlanId: () => number,\n): Exclude<PlannerNode, PlannerTerminus> {\n const {related} = condition;\n const childTable = related.subquery.table;\n\n const childSource = graph.hasSource(childTable)\n ? graph.getSource(childTable)\n : graph.addSource(childTable, model);\n\n const childConnection = childSource.connect(\n related.subquery.orderBy ?? [],\n related.subquery.where,\n false,\n undefined, // no base constraints for EXISTS/NOT EXISTS\n condition.op === 'EXISTS' ? 1 : undefined,\n );\n graph.connections.push(childConnection);\n\n let childEnd: PlannerNode = childConnection;\n if (related.subquery.where) {\n childEnd = processCondition(\n related.subquery.where,\n childEnd,\n graph,\n model,\n childTable,\n getPlanId,\n );\n }\n\n const parentConstraint = extractConstraint(\n related.correlation.parentField,\n parentTable,\n );\n const childConstraint = extractConstraint(\n related.correlation.childField,\n childTable,\n );\n\n const planId = getPlanId();\n condition[planIdSymbol] = planId;\n\n // Determine flippability and initial type based on flip flag and operator\n const isNotExists = condition.op === 'NOT EXISTS';\n const manualFlip = condition.flip;\n\n let flippable: boolean;\n let initialType: 'semi' | 'flipped';\n\n if (isNotExists) {\n // NOT EXISTS joins can never be flipped\n flippable = false;\n initialType = 'semi';\n } else if (manualFlip === true) {\n // User explicitly requested flip=true: start flipped, don't allow planner to change\n flippable = false;\n initialType = 'flipped';\n } else if (manualFlip === false) {\n // User explicitly requested flip=false: start semi, don't allow planner to change\n flippable = false;\n initialType = 'semi';\n } else {\n // flip is undefined: planner can decide\n flippable = true;\n initialType = 'semi';\n }\n\n const join = new PlannerJoin(\n input,\n childEnd,\n parentConstraint,\n childConstraint,\n flippable,\n planId,\n initialType,\n );\n graph.joins.push(join);\n\n wireOutput(input, join);\n wireOutput(childEnd, join);\n\n return join;\n}\n\nfunction hasCorrelatedSubquery(condition: Condition): boolean {\n if (condition.type === 'correlatedSubquery') {\n return true;\n }\n if (condition.type === 'and' || condition.type === 'or') {\n return condition.conditions.some(hasCorrelatedSubquery);\n }\n return false;\n}\n\nfunction extractConstraint(\n fields: readonly string[],\n _tableName: string,\n): PlannerConstraint {\n return Object.fromEntries(fields.map(field => [field, undefined]));\n}\n\nfunction planRecursively(\n plans: Plans,\n planDebugger?: PlanDebugger,\n lc?: LogContext,\n): void {\n for (const subPlan of Object.values(plans.subPlans)) {\n planRecursively(subPlan, planDebugger, lc);\n }\n plans.plan.plan(planDebugger, lc);\n}\n\nexport function planQuery(\n ast: AST,\n model: ConnectionCostModel,\n planDebugger?: PlanDebugger,\n lc?: LogContext,\n): AST {\n const plans = buildPlanGraph(ast, model, true);\n planRecursively(plans, planDebugger, lc);\n return applyPlansToAST(ast, plans);\n}\n\nfunction applyToCondition(\n condition: Condition,\n flippedIds: Set<number>,\n): Condition {\n if (condition.type === 'simple') {\n return condition;\n }\n\n if (condition.type === 'correlatedSubquery') {\n const planId = (condition as unknown as Record<symbol, number>)[\n planIdSymbol\n ];\n const shouldFlip = planId !== undefined && flippedIds.has(planId);\n\n return {\n ...condition,\n flip: shouldFlip,\n related: {\n ...condition.related,\n subquery: {\n ...condition.related.subquery,\n where: condition.related.subquery.where\n ? applyToCondition(condition.related.subquery.where, flippedIds)\n : undefined,\n },\n },\n };\n }\n\n return {\n ...condition,\n conditions: condition.conditions.map(c => applyToCondition(c, flippedIds)),\n };\n}\n\nexport function applyPlansToAST(ast: AST, plans: Plans): AST {\n const flippedIds = new Set<number>();\n for (const join of plans.plan.joins) {\n if (join.type === 'flipped' && join.planId !== undefined) {\n flippedIds.add(join.planId);\n }\n }\n\n return {\n ...ast,\n where: ast.where ? applyToCondition(ast.where, flippedIds) : undefined,\n related: ast.related?.map(csq => {\n const alias = must(\n csq.subquery.alias,\n 'Related subquery must have alias',\n );\n const subPlan = plans.subPlans[alias];\n return {\n ...csq,\n subquery: subPlan\n ? applyPlansToAST(csq.subquery, subPlan)\n : csq.subquery,\n };\n }),\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LogContext } from '@rocicorp/logger';
|
|
1
2
|
import type { PlanDebugger } from './planner-debug.ts';
|
|
2
3
|
import type { PlannerConnection } from './planner-connection.ts';
|
|
3
4
|
import type { PlannerConstraint } from './planner-constraint.ts';
|
|
@@ -96,7 +97,8 @@ export declare class PlannerGraph {
|
|
|
96
97
|
* FanOut/FanIn states (FO/UFO and FI/UFI) are automatically derived from join flip states.
|
|
97
98
|
*
|
|
98
99
|
* @param planDebugger - Optional debugger to receive structured events during planning
|
|
100
|
+
* @param lc - Optional logger for warnings
|
|
99
101
|
*/
|
|
100
|
-
plan(planDebugger?: PlanDebugger): void;
|
|
102
|
+
plan(planDebugger?: PlanDebugger, lc?: LogContext): void;
|
|
101
103
|
}
|
|
102
104
|
//# sourceMappingURL=planner-graph.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"planner-graph.d.ts","sourceRoot":"","sources":["../../../../../zql/src/planner/planner-graph.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"planner-graph.d.ts","sourceRoot":"","sources":["../../../../../zql/src/planner/planner-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAGjD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,qBAAqB,CAAC;AACtD,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC;AAGnD,OAAO,EAAC,aAAa,EAAE,KAAK,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAE3D;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,WAAW,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;KAAC,CAAC,CAAC;IAChD,KAAK,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;KAAC,CAAC,CAAC;IACzC,OAAO,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,IAAI,GAAG,KAAK,CAAA;KAAC,CAAC,CAAC;IACrC,MAAM,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,IAAI,GAAG,KAAK,CAAA;KAAC,CAAC,CAAC;IACpC,qBAAqB,EAAE,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,GAAG,SAAS,CAAC,CAAC,CAAC;CAC1E,CAAC;AAkBF,qBAAa,YAAY;;IAQvB,KAAK,EAAE,WAAW,EAAE,CAAM;IAC1B,OAAO,EAAE,aAAa,EAAE,CAAM;IAC9B,MAAM,EAAE,YAAY,EAAE,CAAM;IAC5B,WAAW,EAAE,iBAAiB,EAAE,CAAM;IAEtC;;;;;OAKG;IACH,kBAAkB;IAOlB;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,aAAa;IAUlE;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa;IAMtC;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIhC;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAI5C;;;;OAIG;IACH,oBAAoB,CAAC,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI;IAQvD;;;OAGG;IACH,YAAY,CAAC,YAAY,CAAC,EAAE,YAAY,GAAG,MAAM;IAKjD;;;;;;;;OAQG;IACH,uBAAuB,IAAI,SAAS;IAYpC;;;;;;;;OAQG;IACH,uBAAuB,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI;IAsF/C;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,YAAY,CAAC,EAAE,YAAY,EAAE,EAAE,CAAC,EAAE,UAAU,GAAG,IAAI;CA+HzD"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { assert } from "../../../shared/src/asserts.js";
|
|
2
2
|
import { must } from "../../../shared/src/must.js";
|
|
3
|
-
import { PlannerException } from "../error.js";
|
|
4
3
|
import { omitFanout } from "./planner-node.js";
|
|
5
4
|
import { PlannerSource } from "./planner-source.js";
|
|
6
5
|
const MAX_FLIPPABLE_JOINS = 9;
|
|
@@ -195,14 +194,15 @@ class PlannerGraph {
|
|
|
195
194
|
* FanOut/FanIn states (FO/UFO and FI/UFI) are automatically derived from join flip states.
|
|
196
195
|
*
|
|
197
196
|
* @param planDebugger - Optional debugger to receive structured events during planning
|
|
197
|
+
* @param lc - Optional logger for warnings
|
|
198
198
|
*/
|
|
199
|
-
plan(planDebugger) {
|
|
199
|
+
plan(planDebugger, lc) {
|
|
200
200
|
const flippableJoins = this.joins.filter((j) => j.isFlippable());
|
|
201
201
|
if (flippableJoins.length > MAX_FLIPPABLE_JOINS) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
`Query has ${flippableJoins.length} EXISTS checks in a single RELATED call (or in the top level query), which would require ${2 ** flippableJoins.length} plan evaluations. This may be very slow. Consider simplifying the query or increasing MAX_FLIPPABLE_JOINS (currently set to ${MAX_FLIPPABLE_JOINS}).`
|
|
202
|
+
lc?.warn?.(
|
|
203
|
+
`Query has ${flippableJoins.length} EXISTS checks which would require ${2 ** flippableJoins.length} plan evaluations. Skipping optimization.`
|
|
205
204
|
);
|
|
205
|
+
return;
|
|
206
206
|
}
|
|
207
207
|
const fofiCache = buildFOFICache(this);
|
|
208
208
|
const numPatterns = flippableJoins.length === 0 ? 0 : 2 ** flippableJoins.length;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"planner-graph.js","sources":["../../../../../zql/src/planner/planner-graph.ts"],"sourcesContent":["import {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {PlannerException} from '../error.ts';\nimport type {PlanDebugger} from './planner-debug.ts';\nimport type {PlannerConnection} from './planner-connection.ts';\nimport type {PlannerConstraint} from './planner-constraint.ts';\nimport type {PlannerFanIn} from './planner-fan-in.ts';\nimport type {PlannerFanOut} from './planner-fan-out.ts';\nimport type {PlannerJoin} from './planner-join.ts';\nimport {omitFanout} from './planner-node.ts';\nimport type {PlannerNode} from './planner-node.ts';\nimport {PlannerSource, type ConnectionCostModel} from './planner-source.ts';\nimport type {PlannerTerminus} from './planner-terminus.ts';\n\n/**\n * Captured state of a plan for comparison and restoration.\n */\nexport type PlanState = {\n connections: Array<{limit: number | undefined}>;\n joins: Array<{type: 'semi' | 'flipped'}>;\n fanOuts: Array<{type: 'FO' | 'UFO'}>;\n fanIns: Array<{type: 'FI' | 'UFI'}>;\n connectionConstraints: Array<Map<string, PlannerConstraint | undefined>>;\n};\n\n/**\n * Maximum number of flippable joins to attempt exhaustive enumeration.\n * With n flippable joins, we explore 2^n plans.\n * 10 joins = 1024 plans (~100-200ms), 12 joins = 4096 plans (~400ms - 1 second)\n */\nconst MAX_FLIPPABLE_JOINS = 9;\n\n/**\n * Cached information about FanOut→FanIn relationships.\n * Computed once during planning to avoid redundant BFS traversals.\n */\ntype FOFIInfo = {\n fi: PlannerFanIn | undefined;\n joinsBetween: PlannerJoin[];\n};\n\nexport class PlannerGraph {\n // Sources indexed by table name\n readonly #sources = new Map<string, PlannerSource>();\n\n // The final output node where constraint propagation starts\n #terminus: PlannerTerminus | undefined = undefined;\n\n // Collections of nodes with mutable planning state\n joins: PlannerJoin[] = [];\n fanOuts: PlannerFanOut[] = [];\n fanIns: PlannerFanIn[] = [];\n connections: PlannerConnection[] = [];\n\n /**\n * Reset all planning state back to initial values for another planning pass.\n * Resets only mutable planning state - graph structure is unchanged.\n *\n * This allows replanning the same query graph with different strategies.\n */\n resetPlanningState() {\n for (const j of this.joins) j.reset();\n for (const fo of this.fanOuts) fo.reset();\n for (const fi of this.fanIns) fi.reset();\n for (const c of this.connections) c.reset();\n }\n\n /**\n * Create and register a source (table) in the graph.\n */\n addSource(name: string, model: ConnectionCostModel): PlannerSource {\n assert(\n !this.#sources.has(name),\n `Source ${name} already exists in the graph`,\n );\n const source = new PlannerSource(name, model);\n this.#sources.set(name, source);\n return source;\n }\n\n /**\n * Get a source by table name.\n */\n getSource(name: string): PlannerSource {\n const source = this.#sources.get(name);\n assert(source !== undefined, `Source ${name} not found in the graph`);\n return source;\n }\n\n /**\n * Check if a source exists by table name.\n */\n hasSource(name: string): boolean {\n return this.#sources.has(name);\n }\n\n /**\n * Set the terminus (final output) node of the graph.\n * Constraint propagation starts from this node.\n */\n setTerminus(terminus: PlannerTerminus): void {\n this.#terminus = terminus;\n }\n\n /**\n * Initiate constraint propagation from the terminus node.\n * This sends constraints up through the graph to update\n * connection cost estimates.\n */\n propagateConstraints(planDebugger?: PlanDebugger): void {\n assert(\n this.#terminus !== undefined,\n 'Cannot propagate constraints without a terminus node',\n );\n this.#terminus.propagateConstraints(planDebugger);\n }\n\n /**\n * Calculate total cost of the current plan.\n * Total cost includes both startup cost (one-time, e.g., sorting) and running cost.\n */\n getTotalCost(planDebugger?: PlanDebugger): number {\n const estimate = must(this.#terminus).estimateCost(planDebugger);\n return estimate.cost + estimate.startupCost;\n }\n\n /**\n * Capture a lightweight snapshot of the current planning state.\n * Used for backtracking during multi-start greedy search.\n *\n * Captures mutable state including pinned flags, join types, and\n * constraint maps to avoid needing repropagation on restore.\n *\n * @returns A snapshot that can be restored via restorePlanningSnapshot()\n */\n capturePlanningSnapshot(): PlanState {\n return {\n connections: this.connections.map(c => ({\n limit: c.limit,\n })),\n joins: this.joins.map(j => ({type: j.type})),\n fanOuts: this.fanOuts.map(fo => ({type: fo.type})),\n fanIns: this.fanIns.map(fi => ({type: fi.type})),\n connectionConstraints: this.connections.map(c => c.captureConstraints()),\n };\n }\n\n /**\n * Restore planning state from a previously captured snapshot.\n * Used for backtracking when a planning attempt fails.\n *\n * Restores pinned flags, join types, and constraint maps, eliminating\n * the need for repropagation.\n *\n * @param state - Snapshot created by capturePlanningSnapshot()\n */\n restorePlanningSnapshot(state: PlanState): void {\n this.#validateSnapshotShape(state);\n this.#restoreConnections(state);\n this.#restoreJoins(state);\n this.#restoreFanNodes(state);\n }\n\n /**\n * Validate that snapshot shape matches current graph structure.\n */\n #validateSnapshotShape(state: PlanState): void {\n assert(\n this.connections.length === state.connections.length,\n 'Plan state mismatch: connections',\n );\n assert(\n this.joins.length === state.joins.length,\n 'Plan state mismatch: joins',\n );\n assert(\n this.fanOuts.length === state.fanOuts.length,\n 'Plan state mismatch: fanOuts',\n );\n assert(\n this.fanIns.length === state.fanIns.length,\n 'Plan state mismatch: fanIns',\n );\n assert(\n this.connections.length === state.connectionConstraints.length,\n 'Plan state mismatch: connectionConstraints',\n );\n }\n\n /**\n * Restore connection pinned flags, limits, and constraint maps.\n */\n #restoreConnections(state: PlanState): void {\n for (let i = 0; i < this.connections.length; i++) {\n this.connections[i].limit = state.connections[i].limit;\n this.connections[i].restoreConstraints(state.connectionConstraints[i]);\n }\n }\n\n /**\n * Restore join types and pinned flags.\n */\n #restoreJoins(state: PlanState): void {\n for (let i = 0; i < this.joins.length; i++) {\n const join = this.joins[i];\n const targetState = state.joins[i];\n\n // Reset to initial state first\n join.reset();\n\n // Apply target state\n if (targetState.type === 'flipped' && join.type !== 'flipped') {\n join.flip();\n }\n assert(\n targetState.type === join.type,\n 'join is not in the correct state after reset',\n );\n }\n }\n\n /**\n * Restore FanOut and FanIn types.\n */\n #restoreFanNodes(state: PlanState): void {\n for (let i = 0; i < this.fanOuts.length; i++) {\n const fo = this.fanOuts[i];\n const targetType = state.fanOuts[i].type;\n if (targetType === 'UFO' && fo.type === 'FO') {\n fo.convertToUFO();\n }\n }\n\n for (let i = 0; i < this.fanIns.length; i++) {\n const fi = this.fanIns[i];\n const targetType = state.fanIns[i].type;\n if (targetType === 'UFI' && fi.type === 'FI') {\n fi.convertToUFI();\n }\n }\n }\n\n /**\n * Main planning algorithm using exhaustive join flip enumeration.\n *\n * Enumerates all possible flip patterns for flippable joins (2^n for n flippable joins).\n * Each pattern represents a different query execution plan. We evaluate the cost of each\n * plan and select the one with the lowest cost.\n *\n * Connections are used only for cost estimation - the flip patterns determine the plan.\n * FanOut/FanIn states (FO/UFO and FI/UFI) are automatically derived from join flip states.\n *\n * @param planDebugger - Optional debugger to receive structured events during planning\n */\n plan(planDebugger?: PlanDebugger): void {\n // Get all flippable joins\n const flippableJoins = this.joins.filter(j => j.isFlippable());\n\n // Safety check: throw if too many flippable joins\n if (flippableJoins.length > MAX_FLIPPABLE_JOINS) {\n throw new PlannerException(\n 'max_flippable_joins',\n `Query has ${flippableJoins.length} EXISTS checks in a single RELATED call (or in the top level query), which would require ` +\n `${2 ** flippableJoins.length} plan evaluations. This may be very slow. ` +\n `Consider simplifying the query or increasing MAX_FLIPPABLE_JOINS (currently set to ${MAX_FLIPPABLE_JOINS}).`,\n );\n }\n\n // Build FO→FI cache once to avoid redundant BFS traversals in each iteration\n const fofiCache = buildFOFICache(this);\n\n const numPatterns =\n flippableJoins.length === 0 ? 0 : 2 ** flippableJoins.length;\n let bestCost = Infinity;\n let bestPlan: PlanState | undefined = undefined;\n let bestAttemptNumber = -1;\n\n // Enumerate all flip patterns\n for (let pattern = 0; pattern < numPatterns; pattern++) {\n // Reset to initial state\n this.resetPlanningState();\n\n if (planDebugger) {\n planDebugger.log({\n type: 'attempt-start',\n attemptNumber: pattern,\n totalAttempts: numPatterns,\n });\n }\n\n // Apply flip pattern (treat pattern as bitmask)\n // Bit i set to 1 means flip join i\n for (let i = 0; i < flippableJoins.length; i++) {\n if (pattern & (1 << i)) {\n flippableJoins[i].flip();\n }\n }\n\n // Derive FO/UFO and FI/UFI states from join flip states\n checkAndConvertFOFI(fofiCache);\n\n // Propagate unlimiting for flipped joins\n propagateUnlimitForFlippedJoins(this);\n\n // Propagate constraints through the graph\n this.propagateConstraints(planDebugger);\n\n if (planDebugger) {\n planDebugger.log({\n type: 'constraints-propagated',\n attemptNumber: pattern,\n connectionConstraints: this.connections.map(c => {\n const constraintCosts = c.getConstraintCostsForDebug();\n const constraintCostsWithoutFanout: Record<\n string,\n Omit<(typeof constraintCosts)[string], 'fanout'>\n > = {};\n for (const [key, cost] of Object.entries(constraintCosts)) {\n constraintCostsWithoutFanout[key] = omitFanout(cost);\n }\n return {\n connection: c.name,\n constraints: c.getConstraintsForDebug(),\n constraintCosts: constraintCostsWithoutFanout,\n };\n }),\n });\n }\n\n // Evaluate this plan\n const totalCost = this.getTotalCost(planDebugger);\n\n if (planDebugger) {\n planDebugger.log({\n type: 'plan-complete',\n attemptNumber: pattern,\n totalCost,\n flipPattern: pattern, // Bitmask of which joins are flipped\n planSnapshot: this.capturePlanningSnapshot(),\n joinStates: this.joins.map(j => {\n const info = j.getDebugInfo();\n return {\n join: info.name,\n type: info.type,\n };\n }),\n });\n }\n\n // Track best plan\n if (totalCost < bestCost) {\n bestCost = totalCost;\n bestPlan = this.capturePlanningSnapshot();\n bestAttemptNumber = pattern;\n }\n }\n\n // Restore best plan\n if (bestPlan) {\n this.restorePlanningSnapshot(bestPlan);\n // Propagate constraints to ensure all derived state is consistent\n this.propagateConstraints(planDebugger);\n\n if (planDebugger) {\n planDebugger.log({\n type: 'best-plan-selected',\n bestAttemptNumber,\n totalCost: bestCost,\n flipPattern: bestAttemptNumber, // The best attempt number is also the flip pattern\n joinStates: this.joins.map(j => ({\n join: j.getName(),\n type: j.type,\n })),\n });\n }\n } else {\n assert(\n numPatterns === 0,\n 'no plan was found but flippable joins did exist!',\n );\n }\n }\n}\n\n/**\n * Build cache of FO→FI relationships and joins between them.\n * Called once at the start of planning to avoid redundant BFS traversals.\n */\nfunction buildFOFICache(graph: PlannerGraph): Map<PlannerFanOut, FOFIInfo> {\n const cache = new Map<PlannerFanOut, FOFIInfo>();\n\n for (const fo of graph.fanOuts) {\n const info = findFIAndJoins(fo);\n cache.set(fo, info);\n }\n\n return cache;\n}\n\n/**\n * Check if any joins downstream of a FanOut (before reaching FanIn) are flipped.\n * If so, convert the FO to UFO and the FI to UFI.\n *\n * This must be called after join flipping and before propagateConstraints.\n */\nfunction checkAndConvertFOFI(fofiCache: Map<PlannerFanOut, FOFIInfo>): void {\n for (const [fo, info] of fofiCache) {\n const hasFlippedJoin = info.joinsBetween.some(j => j.type === 'flipped');\n if (info.fi && hasFlippedJoin) {\n fo.convertToUFO();\n info.fi.convertToUFI();\n }\n }\n}\n\n/**\n * Traverse from a FanOut through its outputs to find the corresponding FanIn\n * and collect all joins along the way.\n */\nfunction findFIAndJoins(fo: PlannerFanOut): FOFIInfo {\n const joinsBetween: PlannerJoin[] = [];\n let fi: PlannerFanIn | undefined = undefined;\n\n // BFS through FO outputs to find FI and collect joins\n const queue: PlannerNode[] = [...fo.outputs];\n const visited = new Set<PlannerNode>();\n\n while (queue.length > 0) {\n const node = must(queue.shift());\n if (visited.has(node)) continue;\n visited.add(node);\n\n switch (node.kind) {\n case 'join':\n joinsBetween.push(node);\n queue.push(node.output);\n break;\n case 'fan-out':\n // Nested FO - traverse its outputs\n queue.push(...node.outputs);\n break;\n case 'fan-in':\n // Found the FI - this is the boundary, don't traverse further\n fi = node;\n break;\n case 'connection':\n // Shouldn't happen in a well-formed graph\n break;\n case 'terminus':\n // Reached the end without finding FI\n break;\n }\n }\n\n return {fi, joinsBetween};\n}\n\n/**\n * Propagate unlimiting to all flipped joins in the graph.\n * When a join is flipped, its child becomes the outer loop and should no longer\n * be limited by EXISTS semantics.\n *\n * This must be called after join flipping and before propagateConstraints.\n */\nfunction propagateUnlimitForFlippedJoins(graph: PlannerGraph): void {\n for (const join of graph.joins) {\n if (join.type === 'flipped') {\n join.propagateUnlimit();\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"planner-graph.js","sources":["../../../../../zql/src/planner/planner-graph.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport type {PlanDebugger} from './planner-debug.ts';\nimport type {PlannerConnection} from './planner-connection.ts';\nimport type {PlannerConstraint} from './planner-constraint.ts';\nimport type {PlannerFanIn} from './planner-fan-in.ts';\nimport type {PlannerFanOut} from './planner-fan-out.ts';\nimport type {PlannerJoin} from './planner-join.ts';\nimport {omitFanout} from './planner-node.ts';\nimport type {PlannerNode} from './planner-node.ts';\nimport {PlannerSource, type ConnectionCostModel} from './planner-source.ts';\nimport type {PlannerTerminus} from './planner-terminus.ts';\n\n/**\n * Captured state of a plan for comparison and restoration.\n */\nexport type PlanState = {\n connections: Array<{limit: number | undefined}>;\n joins: Array<{type: 'semi' | 'flipped'}>;\n fanOuts: Array<{type: 'FO' | 'UFO'}>;\n fanIns: Array<{type: 'FI' | 'UFI'}>;\n connectionConstraints: Array<Map<string, PlannerConstraint | undefined>>;\n};\n\n/**\n * Maximum number of flippable joins to attempt exhaustive enumeration.\n * With n flippable joins, we explore 2^n plans.\n * 10 joins = 1024 plans (~100-200ms), 12 joins = 4096 plans (~400ms - 1 second)\n */\nconst MAX_FLIPPABLE_JOINS = 9;\n\n/**\n * Cached information about FanOut→FanIn relationships.\n * Computed once during planning to avoid redundant BFS traversals.\n */\ntype FOFIInfo = {\n fi: PlannerFanIn | undefined;\n joinsBetween: PlannerJoin[];\n};\n\nexport class PlannerGraph {\n // Sources indexed by table name\n readonly #sources = new Map<string, PlannerSource>();\n\n // The final output node where constraint propagation starts\n #terminus: PlannerTerminus | undefined = undefined;\n\n // Collections of nodes with mutable planning state\n joins: PlannerJoin[] = [];\n fanOuts: PlannerFanOut[] = [];\n fanIns: PlannerFanIn[] = [];\n connections: PlannerConnection[] = [];\n\n /**\n * Reset all planning state back to initial values for another planning pass.\n * Resets only mutable planning state - graph structure is unchanged.\n *\n * This allows replanning the same query graph with different strategies.\n */\n resetPlanningState() {\n for (const j of this.joins) j.reset();\n for (const fo of this.fanOuts) fo.reset();\n for (const fi of this.fanIns) fi.reset();\n for (const c of this.connections) c.reset();\n }\n\n /**\n * Create and register a source (table) in the graph.\n */\n addSource(name: string, model: ConnectionCostModel): PlannerSource {\n assert(\n !this.#sources.has(name),\n `Source ${name} already exists in the graph`,\n );\n const source = new PlannerSource(name, model);\n this.#sources.set(name, source);\n return source;\n }\n\n /**\n * Get a source by table name.\n */\n getSource(name: string): PlannerSource {\n const source = this.#sources.get(name);\n assert(source !== undefined, `Source ${name} not found in the graph`);\n return source;\n }\n\n /**\n * Check if a source exists by table name.\n */\n hasSource(name: string): boolean {\n return this.#sources.has(name);\n }\n\n /**\n * Set the terminus (final output) node of the graph.\n * Constraint propagation starts from this node.\n */\n setTerminus(terminus: PlannerTerminus): void {\n this.#terminus = terminus;\n }\n\n /**\n * Initiate constraint propagation from the terminus node.\n * This sends constraints up through the graph to update\n * connection cost estimates.\n */\n propagateConstraints(planDebugger?: PlanDebugger): void {\n assert(\n this.#terminus !== undefined,\n 'Cannot propagate constraints without a terminus node',\n );\n this.#terminus.propagateConstraints(planDebugger);\n }\n\n /**\n * Calculate total cost of the current plan.\n * Total cost includes both startup cost (one-time, e.g., sorting) and running cost.\n */\n getTotalCost(planDebugger?: PlanDebugger): number {\n const estimate = must(this.#terminus).estimateCost(planDebugger);\n return estimate.cost + estimate.startupCost;\n }\n\n /**\n * Capture a lightweight snapshot of the current planning state.\n * Used for backtracking during multi-start greedy search.\n *\n * Captures mutable state including pinned flags, join types, and\n * constraint maps to avoid needing repropagation on restore.\n *\n * @returns A snapshot that can be restored via restorePlanningSnapshot()\n */\n capturePlanningSnapshot(): PlanState {\n return {\n connections: this.connections.map(c => ({\n limit: c.limit,\n })),\n joins: this.joins.map(j => ({type: j.type})),\n fanOuts: this.fanOuts.map(fo => ({type: fo.type})),\n fanIns: this.fanIns.map(fi => ({type: fi.type})),\n connectionConstraints: this.connections.map(c => c.captureConstraints()),\n };\n }\n\n /**\n * Restore planning state from a previously captured snapshot.\n * Used for backtracking when a planning attempt fails.\n *\n * Restores pinned flags, join types, and constraint maps, eliminating\n * the need for repropagation.\n *\n * @param state - Snapshot created by capturePlanningSnapshot()\n */\n restorePlanningSnapshot(state: PlanState): void {\n this.#validateSnapshotShape(state);\n this.#restoreConnections(state);\n this.#restoreJoins(state);\n this.#restoreFanNodes(state);\n }\n\n /**\n * Validate that snapshot shape matches current graph structure.\n */\n #validateSnapshotShape(state: PlanState): void {\n assert(\n this.connections.length === state.connections.length,\n 'Plan state mismatch: connections',\n );\n assert(\n this.joins.length === state.joins.length,\n 'Plan state mismatch: joins',\n );\n assert(\n this.fanOuts.length === state.fanOuts.length,\n 'Plan state mismatch: fanOuts',\n );\n assert(\n this.fanIns.length === state.fanIns.length,\n 'Plan state mismatch: fanIns',\n );\n assert(\n this.connections.length === state.connectionConstraints.length,\n 'Plan state mismatch: connectionConstraints',\n );\n }\n\n /**\n * Restore connection pinned flags, limits, and constraint maps.\n */\n #restoreConnections(state: PlanState): void {\n for (let i = 0; i < this.connections.length; i++) {\n this.connections[i].limit = state.connections[i].limit;\n this.connections[i].restoreConstraints(state.connectionConstraints[i]);\n }\n }\n\n /**\n * Restore join types and pinned flags.\n */\n #restoreJoins(state: PlanState): void {\n for (let i = 0; i < this.joins.length; i++) {\n const join = this.joins[i];\n const targetState = state.joins[i];\n\n // Reset to initial state first\n join.reset();\n\n // Apply target state\n if (targetState.type === 'flipped' && join.type !== 'flipped') {\n join.flip();\n }\n assert(\n targetState.type === join.type,\n 'join is not in the correct state after reset',\n );\n }\n }\n\n /**\n * Restore FanOut and FanIn types.\n */\n #restoreFanNodes(state: PlanState): void {\n for (let i = 0; i < this.fanOuts.length; i++) {\n const fo = this.fanOuts[i];\n const targetType = state.fanOuts[i].type;\n if (targetType === 'UFO' && fo.type === 'FO') {\n fo.convertToUFO();\n }\n }\n\n for (let i = 0; i < this.fanIns.length; i++) {\n const fi = this.fanIns[i];\n const targetType = state.fanIns[i].type;\n if (targetType === 'UFI' && fi.type === 'FI') {\n fi.convertToUFI();\n }\n }\n }\n\n /**\n * Main planning algorithm using exhaustive join flip enumeration.\n *\n * Enumerates all possible flip patterns for flippable joins (2^n for n flippable joins).\n * Each pattern represents a different query execution plan. We evaluate the cost of each\n * plan and select the one with the lowest cost.\n *\n * Connections are used only for cost estimation - the flip patterns determine the plan.\n * FanOut/FanIn states (FO/UFO and FI/UFI) are automatically derived from join flip states.\n *\n * @param planDebugger - Optional debugger to receive structured events during planning\n * @param lc - Optional logger for warnings\n */\n plan(planDebugger?: PlanDebugger, lc?: LogContext): void {\n // Get all flippable joins\n const flippableJoins = this.joins.filter(j => j.isFlippable());\n\n // Too many flippable joins - skip optimization and run as-is\n if (flippableJoins.length > MAX_FLIPPABLE_JOINS) {\n lc?.warn?.(\n `Query has ${flippableJoins.length} EXISTS checks which would require ` +\n `${2 ** flippableJoins.length} plan evaluations. Skipping optimization.`,\n );\n return;\n }\n\n // Build FO→FI cache once to avoid redundant BFS traversals in each iteration\n const fofiCache = buildFOFICache(this);\n\n const numPatterns =\n flippableJoins.length === 0 ? 0 : 2 ** flippableJoins.length;\n let bestCost = Infinity;\n let bestPlan: PlanState | undefined = undefined;\n let bestAttemptNumber = -1;\n\n // Enumerate all flip patterns\n for (let pattern = 0; pattern < numPatterns; pattern++) {\n // Reset to initial state\n this.resetPlanningState();\n\n if (planDebugger) {\n planDebugger.log({\n type: 'attempt-start',\n attemptNumber: pattern,\n totalAttempts: numPatterns,\n });\n }\n\n // Apply flip pattern (treat pattern as bitmask)\n // Bit i set to 1 means flip join i\n for (let i = 0; i < flippableJoins.length; i++) {\n if (pattern & (1 << i)) {\n flippableJoins[i].flip();\n }\n }\n\n // Derive FO/UFO and FI/UFI states from join flip states\n checkAndConvertFOFI(fofiCache);\n\n // Propagate unlimiting for flipped joins\n propagateUnlimitForFlippedJoins(this);\n\n // Propagate constraints through the graph\n this.propagateConstraints(planDebugger);\n\n if (planDebugger) {\n planDebugger.log({\n type: 'constraints-propagated',\n attemptNumber: pattern,\n connectionConstraints: this.connections.map(c => {\n const constraintCosts = c.getConstraintCostsForDebug();\n const constraintCostsWithoutFanout: Record<\n string,\n Omit<(typeof constraintCosts)[string], 'fanout'>\n > = {};\n for (const [key, cost] of Object.entries(constraintCosts)) {\n constraintCostsWithoutFanout[key] = omitFanout(cost);\n }\n return {\n connection: c.name,\n constraints: c.getConstraintsForDebug(),\n constraintCosts: constraintCostsWithoutFanout,\n };\n }),\n });\n }\n\n // Evaluate this plan\n const totalCost = this.getTotalCost(planDebugger);\n\n if (planDebugger) {\n planDebugger.log({\n type: 'plan-complete',\n attemptNumber: pattern,\n totalCost,\n flipPattern: pattern, // Bitmask of which joins are flipped\n planSnapshot: this.capturePlanningSnapshot(),\n joinStates: this.joins.map(j => {\n const info = j.getDebugInfo();\n return {\n join: info.name,\n type: info.type,\n };\n }),\n });\n }\n\n // Track best plan\n if (totalCost < bestCost) {\n bestCost = totalCost;\n bestPlan = this.capturePlanningSnapshot();\n bestAttemptNumber = pattern;\n }\n }\n\n // Restore best plan\n if (bestPlan) {\n this.restorePlanningSnapshot(bestPlan);\n // Propagate constraints to ensure all derived state is consistent\n this.propagateConstraints(planDebugger);\n\n if (planDebugger) {\n planDebugger.log({\n type: 'best-plan-selected',\n bestAttemptNumber,\n totalCost: bestCost,\n flipPattern: bestAttemptNumber, // The best attempt number is also the flip pattern\n joinStates: this.joins.map(j => ({\n join: j.getName(),\n type: j.type,\n })),\n });\n }\n } else {\n assert(\n numPatterns === 0,\n 'no plan was found but flippable joins did exist!',\n );\n }\n }\n}\n\n/**\n * Build cache of FO→FI relationships and joins between them.\n * Called once at the start of planning to avoid redundant BFS traversals.\n */\nfunction buildFOFICache(graph: PlannerGraph): Map<PlannerFanOut, FOFIInfo> {\n const cache = new Map<PlannerFanOut, FOFIInfo>();\n\n for (const fo of graph.fanOuts) {\n const info = findFIAndJoins(fo);\n cache.set(fo, info);\n }\n\n return cache;\n}\n\n/**\n * Check if any joins downstream of a FanOut (before reaching FanIn) are flipped.\n * If so, convert the FO to UFO and the FI to UFI.\n *\n * This must be called after join flipping and before propagateConstraints.\n */\nfunction checkAndConvertFOFI(fofiCache: Map<PlannerFanOut, FOFIInfo>): void {\n for (const [fo, info] of fofiCache) {\n const hasFlippedJoin = info.joinsBetween.some(j => j.type === 'flipped');\n if (info.fi && hasFlippedJoin) {\n fo.convertToUFO();\n info.fi.convertToUFI();\n }\n }\n}\n\n/**\n * Traverse from a FanOut through its outputs to find the corresponding FanIn\n * and collect all joins along the way.\n */\nfunction findFIAndJoins(fo: PlannerFanOut): FOFIInfo {\n const joinsBetween: PlannerJoin[] = [];\n let fi: PlannerFanIn | undefined = undefined;\n\n // BFS through FO outputs to find FI and collect joins\n const queue: PlannerNode[] = [...fo.outputs];\n const visited = new Set<PlannerNode>();\n\n while (queue.length > 0) {\n const node = must(queue.shift());\n if (visited.has(node)) continue;\n visited.add(node);\n\n switch (node.kind) {\n case 'join':\n joinsBetween.push(node);\n queue.push(node.output);\n break;\n case 'fan-out':\n // Nested FO - traverse its outputs\n queue.push(...node.outputs);\n break;\n case 'fan-in':\n // Found the FI - this is the boundary, don't traverse further\n fi = node;\n break;\n case 'connection':\n // Shouldn't happen in a well-formed graph\n break;\n case 'terminus':\n // Reached the end without finding FI\n break;\n }\n }\n\n return {fi, joinsBetween};\n}\n\n/**\n * Propagate unlimiting to all flipped joins in the graph.\n * When a join is flipped, its child becomes the outer loop and should no longer\n * be limited by EXISTS semantics.\n *\n * This must be called after join flipping and before propagateConstraints.\n */\nfunction propagateUnlimitForFlippedJoins(graph: PlannerGraph): void {\n for (const join of graph.joins) {\n if (join.type === 'flipped') {\n join.propagateUnlimit();\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-builder.d.ts","sourceRoot":"","sources":["../../../../../zql/src/query/create-builder.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"create-builder.d.ts","sourceRoot":"","sources":["../../../../../zql/src/query/create-builder.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,mCAAmC,CAAC;AAC9D,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAIvD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC;AAEnD;;GAEG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAIzE;AAED,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,MAAM,EACpD,QAAQ,EAAE,aAAa,EACvB,MAAM,EAAE,CAAC,GACR,WAAW,CAAC,CAAC,CAAC,CAIhB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recordProxy } from "../../../shared/src/record-proxy.js";
|
|
1
2
|
import { newQuery } from "./query-impl.js";
|
|
2
3
|
import { newRunnableQuery } from "./runnable-query-impl.js";
|
|
3
4
|
function createBuilder(schema) {
|
|
@@ -13,43 +14,13 @@ function createRunnableBuilder(delegate, schema) {
|
|
|
13
14
|
);
|
|
14
15
|
}
|
|
15
16
|
function createBuilderWithQueryFactory(schema, queryFactory) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return cached;
|
|
17
|
+
return recordProxy(
|
|
18
|
+
schema.tables,
|
|
19
|
+
(_tableSchema, prop) => queryFactory(prop),
|
|
20
|
+
(prop) => {
|
|
21
|
+
throw new Error(`Table ${prop} does not exist in schema`);
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
return void 0;
|
|
25
|
-
}
|
|
26
|
-
const q = queryFactory(prop);
|
|
27
|
-
cache.set(prop, q);
|
|
28
|
-
return q;
|
|
29
|
-
}
|
|
30
|
-
return new Proxy(tables, {
|
|
31
|
-
get: (_target, prop) => {
|
|
32
|
-
if (typeof prop === "symbol") {
|
|
33
|
-
return void 0;
|
|
34
|
-
}
|
|
35
|
-
const q = getQuery(prop);
|
|
36
|
-
if (!q) {
|
|
37
|
-
throw new Error(`Table ${String(prop)} does not exist in schema`);
|
|
38
|
-
}
|
|
39
|
-
return q;
|
|
40
|
-
},
|
|
41
|
-
getOwnPropertyDescriptor: (_target, prop) => {
|
|
42
|
-
if (typeof prop === "symbol") {
|
|
43
|
-
return void 0;
|
|
44
|
-
}
|
|
45
|
-
const value = getQuery(prop);
|
|
46
|
-
if (!value) {
|
|
47
|
-
return void 0;
|
|
48
|
-
}
|
|
49
|
-
const desc = Reflect.getOwnPropertyDescriptor(tables, prop);
|
|
50
|
-
return { ...desc, value };
|
|
51
|
-
}
|
|
52
|
-
});
|
|
23
|
+
);
|
|
53
24
|
}
|
|
54
25
|
export {
|
|
55
26
|
createBuilder,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-builder.js","sources":["../../../../../zql/src/query/create-builder.ts"],"sourcesContent":["import type {Schema} from '../../../zero-types/src/schema.ts';\nimport type {QueryDelegate} from './query-delegate.ts';\nimport {newQuery} from './query-impl.ts';\nimport type {Query} from './query.ts';\nimport {newRunnableQuery} from './runnable-query-impl.ts';\nimport type {SchemaQuery} from './schema-query.ts';\n\n/**\n * Returns a set of query builders for the given schema.\n */\nexport function createBuilder<S extends Schema>(schema: S): SchemaQuery<S> {\n return createBuilderWithQueryFactory(schema, table =>\n newQuery(schema, table),\n );\n}\n\nexport function createRunnableBuilder<S extends Schema>(\n delegate: QueryDelegate,\n schema: S,\n): SchemaQuery<S> {\n return createBuilderWithQueryFactory(schema, table =>\n newRunnableQuery(delegate, schema, table),\n );\n}\n\nfunction createBuilderWithQueryFactory<S extends Schema>(\n schema: S,\n queryFactory: (table: keyof S['tables'] & string) => Query<string, S>,\n): SchemaQuery<S> {\n
|
|
1
|
+
{"version":3,"file":"create-builder.js","sources":["../../../../../zql/src/query/create-builder.ts"],"sourcesContent":["import {recordProxy} from '../../../shared/src/record-proxy.ts';\nimport type {Schema} from '../../../zero-types/src/schema.ts';\nimport type {QueryDelegate} from './query-delegate.ts';\nimport {newQuery} from './query-impl.ts';\nimport type {Query} from './query.ts';\nimport {newRunnableQuery} from './runnable-query-impl.ts';\nimport type {SchemaQuery} from './schema-query.ts';\n\n/**\n * Returns a set of query builders for the given schema.\n */\nexport function createBuilder<S extends Schema>(schema: S): SchemaQuery<S> {\n return createBuilderWithQueryFactory(schema, table =>\n newQuery(schema, table),\n );\n}\n\nexport function createRunnableBuilder<S extends Schema>(\n delegate: QueryDelegate,\n schema: S,\n): SchemaQuery<S> {\n return createBuilderWithQueryFactory(schema, table =>\n newRunnableQuery(delegate, schema, table),\n );\n}\n\nfunction createBuilderWithQueryFactory<S extends Schema>(\n schema: S,\n queryFactory: (table: keyof S['tables'] & string) => Query<string, S>,\n): SchemaQuery<S> {\n return recordProxy(\n schema.tables,\n (_tableSchema, prop) => queryFactory(prop),\n prop => {\n throw new Error(`Table ${prop} does not exist in schema`);\n },\n ) as unknown as SchemaQuery<S>;\n}\n"],"names":[],"mappings":";;;AAWO,SAAS,cAAgC,QAA2B;AACzE,SAAO;AAAA,IAA8B;AAAA,IAAQ,CAAA,UAC3C,SAAS,QAAQ,KAAK;AAAA,EAAA;AAE1B;AAEO,SAAS,sBACd,UACA,QACgB;AAChB,SAAO;AAAA,IAA8B;AAAA,IAAQ,CAAA,UAC3C,iBAAiB,UAAU,QAAQ,KAAK;AAAA,EAAA;AAE5C;AAEA,SAAS,8BACP,QACA,cACgB;AAChB,SAAO;AAAA,IACL,OAAO;AAAA,IACP,CAAC,cAAc,SAAS,aAAa,IAAI;AAAA,IACzC,CAAA,SAAQ;AACN,YAAM,IAAI,MAAM,SAAS,IAAI,2BAA2B;AAAA,IAC1D;AAAA,EAAA;AAEJ;"}
|
|
@@ -12,7 +12,7 @@ export declare class MeasurePushOperator implements Operator {
|
|
|
12
12
|
fetch(req: FetchRequest): Stream<Node | 'yield'>;
|
|
13
13
|
getSchema(): SourceSchema;
|
|
14
14
|
destroy(): void;
|
|
15
|
-
push(change: Change):
|
|
15
|
+
push(change: Change): Stream<'yield'>;
|
|
16
16
|
}
|
|
17
17
|
export {};
|
|
18
18
|
//# sourceMappingURL=measure-push-operator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"measure-push-operator.d.ts","sourceRoot":"","sources":["../../../../../zql/src/query/measure-push-operator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,KAAK,EACV,KAAK,QAAQ,EACb,KAAK,MAAM,EACZ,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAE3D,KAAK,UAAU,GAAG,qBAAqB,GAAG,qBAAqB,CAAC;AAEhE,qBAAa,mBAAoB,YAAW,QAAQ;;gBAShD,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,eAAe,EAChC,UAAU,EAAE,UAAU;IASxB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI/B,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC,IAAI,GAAG,OAAO,CAAC;IAIhD,SAAS,IAAI,YAAY;IAIzB,OAAO,IAAI,IAAI;
|
|
1
|
+
{"version":3,"file":"measure-push-operator.d.ts","sourceRoot":"","sources":["../../../../../zql/src/query/measure-push-operator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,KAAK,EACV,KAAK,QAAQ,EACb,KAAK,MAAM,EACZ,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAE3D,KAAK,UAAU,GAAG,qBAAqB,GAAG,qBAAqB,CAAC;AAEhE,qBAAa,mBAAoB,YAAW,QAAQ;;gBAShD,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,eAAe,EAChC,UAAU,EAAE,UAAU;IASxB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI/B,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC,IAAI,GAAG,OAAO,CAAC;IAIhD,SAAS,IAAI,YAAY;IAIzB,OAAO,IAAI,IAAI;IAId,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC;CASvC"}
|
|
@@ -24,9 +24,9 @@ class MeasurePushOperator {
|
|
|
24
24
|
destroy() {
|
|
25
25
|
this.#input.destroy();
|
|
26
26
|
}
|
|
27
|
-
push(change) {
|
|
27
|
+
*push(change) {
|
|
28
28
|
const startTime = performance.now();
|
|
29
|
-
this.#output.push(change, this);
|
|
29
|
+
yield* this.#output.push(change, this);
|
|
30
30
|
this.#metricsDelegate.addMetric(
|
|
31
31
|
this.#metricName,
|
|
32
32
|
performance.now() - startTime,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"measure-push-operator.js","sources":["../../../../../zql/src/query/measure-push-operator.ts"],"sourcesContent":["import type {Change} from '../ivm/change.ts';\nimport type {Node} from '../ivm/data.ts';\nimport {\n throwOutput,\n type FetchRequest,\n type Input,\n type Operator,\n type Output,\n} from '../ivm/operator.ts';\nimport type {SourceSchema} from '../ivm/schema.ts';\nimport type {Stream} from '../ivm/stream.ts';\nimport type {MetricsDelegate} from './metrics-delegate.ts';\n\ntype MetricName = 'query-update-client' | 'query-update-server';\n\nexport class MeasurePushOperator implements Operator {\n readonly #input: Input;\n readonly #queryID: string;\n readonly #metricsDelegate: MetricsDelegate;\n\n #output: Output = throwOutput;\n readonly #metricName: MetricName;\n\n constructor(\n input: Input,\n queryID: string,\n metricsDelegate: MetricsDelegate,\n metricName: MetricName,\n ) {\n this.#input = input;\n this.#queryID = queryID;\n this.#metricsDelegate = metricsDelegate;\n this.#metricName = metricName;\n input.setOutput(this);\n }\n\n setOutput(output: Output): void {\n this.#output = output;\n }\n\n fetch(req: FetchRequest): Stream<Node | 'yield'> {\n return this.#input.fetch(req);\n }\n\n getSchema(): SourceSchema {\n return this.#input.getSchema();\n }\n\n destroy(): void {\n this.#input.destroy();\n }\n\n push(change: Change):
|
|
1
|
+
{"version":3,"file":"measure-push-operator.js","sources":["../../../../../zql/src/query/measure-push-operator.ts"],"sourcesContent":["import type {Change} from '../ivm/change.ts';\nimport type {Node} from '../ivm/data.ts';\nimport {\n throwOutput,\n type FetchRequest,\n type Input,\n type Operator,\n type Output,\n} from '../ivm/operator.ts';\nimport type {SourceSchema} from '../ivm/schema.ts';\nimport type {Stream} from '../ivm/stream.ts';\nimport type {MetricsDelegate} from './metrics-delegate.ts';\n\ntype MetricName = 'query-update-client' | 'query-update-server';\n\nexport class MeasurePushOperator implements Operator {\n readonly #input: Input;\n readonly #queryID: string;\n readonly #metricsDelegate: MetricsDelegate;\n\n #output: Output = throwOutput;\n readonly #metricName: MetricName;\n\n constructor(\n input: Input,\n queryID: string,\n metricsDelegate: MetricsDelegate,\n metricName: MetricName,\n ) {\n this.#input = input;\n this.#queryID = queryID;\n this.#metricsDelegate = metricsDelegate;\n this.#metricName = metricName;\n input.setOutput(this);\n }\n\n setOutput(output: Output): void {\n this.#output = output;\n }\n\n fetch(req: FetchRequest): Stream<Node | 'yield'> {\n return this.#input.fetch(req);\n }\n\n getSchema(): SourceSchema {\n return this.#input.getSchema();\n }\n\n destroy(): void {\n this.#input.destroy();\n }\n\n *push(change: Change): Stream<'yield'> {\n const startTime = performance.now();\n yield* this.#output.push(change, this);\n this.#metricsDelegate.addMetric(\n this.#metricName,\n performance.now() - startTime,\n this.#queryID,\n );\n }\n}\n"],"names":[],"mappings":";AAeO,MAAM,oBAAwC;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EAET,UAAkB;AAAA,EACT;AAAA,EAET,YACE,OACA,SACA,iBACA,YACA;AACA,SAAK,SAAS;AACd,SAAK,WAAW;AAChB,SAAK,mBAAmB;AACxB,SAAK,cAAc;AACnB,UAAM,UAAU,IAAI;AAAA,EACtB;AAAA,EAEA,UAAU,QAAsB;AAC9B,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAM,KAA2C;AAC/C,WAAO,KAAK,OAAO,MAAM,GAAG;AAAA,EAC9B;AAAA,EAEA,YAA0B;AACxB,WAAO,KAAK,OAAO,UAAA;AAAA,EACrB;AAAA,EAEA,UAAgB;AACd,SAAK,OAAO,QAAA;AAAA,EACd;AAAA,EAEA,CAAC,KAAK,QAAiC;AACrC,UAAM,YAAY,YAAY,IAAA;AAC9B,WAAO,KAAK,QAAQ,KAAK,QAAQ,IAAI;AACrC,SAAK,iBAAiB;AAAA,MACpB,KAAK;AAAA,MACL,YAAY,QAAQ;AAAA,MACpB,KAAK;AAAA,IAAA;AAAA,EAET;AACF;"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SQLQuery } from '@databases/sql';
|
|
2
|
+
/**
|
|
3
|
+
* Compiles a SQL query with values inlined directly into the SQL string.
|
|
4
|
+
*
|
|
5
|
+
* WARNING: This should ONLY be used for cost estimation in the query planner.
|
|
6
|
+
* Never use this for user-facing queries - always use the standard `compile()`
|
|
7
|
+
* function from sql.ts which uses parameterized queries.
|
|
8
|
+
*
|
|
9
|
+
* @param sql The SQL query to compile
|
|
10
|
+
* @returns SQL string with values inlined
|
|
11
|
+
*/
|
|
12
|
+
export declare function compileInline(sql: SQLQuery): string;
|
|
13
|
+
//# sourceMappingURL=sql-inline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-inline.d.ts","sourceRoot":"","sources":["../../../../../zqlite/src/internal/sql-inline.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,gBAAgB,CAAC;AAoE7C;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAEnD"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { escapeSQLiteIdentifier } from "@databases/escape-identifier";
|
|
2
|
+
function escapeSQLiteString(str) {
|
|
3
|
+
return `'${str.replace(/'/g, "''")}'`;
|
|
4
|
+
}
|
|
5
|
+
function inlineValue(value) {
|
|
6
|
+
if (value === null) {
|
|
7
|
+
return "NULL";
|
|
8
|
+
}
|
|
9
|
+
if (typeof value === "string") {
|
|
10
|
+
return escapeSQLiteString(value);
|
|
11
|
+
}
|
|
12
|
+
if (typeof value === "number") {
|
|
13
|
+
return String(value);
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === "boolean") {
|
|
16
|
+
return value ? "1" : "0";
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
return escapeSQLiteString(JSON.stringify(value));
|
|
20
|
+
}
|
|
21
|
+
return escapeSQLiteString(JSON.stringify(value));
|
|
22
|
+
}
|
|
23
|
+
const sqliteInlineFormat = {
|
|
24
|
+
escapeIdentifier: (str) => escapeSQLiteIdentifier(str),
|
|
25
|
+
formatValue: (value) => (
|
|
26
|
+
// undefined is our signal to use a placeholder
|
|
27
|
+
// IMPORTANT. Changing this will break the planner as it will assume `NULL`
|
|
28
|
+
// for constraints!
|
|
29
|
+
value === void 0 ? {
|
|
30
|
+
placeholder: "?",
|
|
31
|
+
value
|
|
32
|
+
} : {
|
|
33
|
+
placeholder: inlineValue(value),
|
|
34
|
+
value: void 0
|
|
35
|
+
// No binding needed since value is inlined
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
};
|
|
39
|
+
function compileInline(sql) {
|
|
40
|
+
return sql.format(sqliteInlineFormat).text;
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
compileInline
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=sql-inline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-inline.js","sources":["../../../../../zqlite/src/internal/sql-inline.ts"],"sourcesContent":["import type {FormatConfig} from '@databases/sql';\nimport type {SQLQuery} from '@databases/sql';\nimport {escapeSQLiteIdentifier} from '@databases/escape-identifier';\n\n/**\n * Escapes a SQLite string value by doubling single quotes.\n * SQLite uses single quotes for string literals and doubles them for escaping.\n */\nfunction escapeSQLiteString(str: string): string {\n return `'${str.replace(/'/g, \"''\")}'`;\n}\n\n/**\n * Formats a value for inline inclusion in SQL (not as a placeholder).\n *\n * WARNING: This should ONLY be used for cost estimation in the query planner,\n * not for user-facing queries. All production queries must use parameterized\n * statements to prevent SQL injection.\n *\n * @param value The value to inline into SQL\n * @returns SQL literal representation of the value\n */\nfunction inlineValue(value: unknown): string {\n if (value === null) {\n return 'NULL';\n }\n if (typeof value === 'string') {\n return escapeSQLiteString(value);\n }\n if (typeof value === 'number') {\n return String(value);\n }\n if (typeof value === 'boolean') {\n // SQLite uses 1 and 0 for booleans\n return value ? '1' : '0';\n }\n if (Array.isArray(value)) {\n // For arrays, use JSON representation (same as query-builder.ts does for IN clauses)\n return escapeSQLiteString(JSON.stringify(value));\n }\n // For objects/other JSON types\n return escapeSQLiteString(JSON.stringify(value));\n}\n\n/**\n * Format configuration that inlines values directly into SQL instead of using placeholders.\n *\n * This is used ONLY for cost estimation in the SQLite cost model, where we want SQLite's\n * query planner to see actual values to make better decisions about index usage and query plans.\n *\n * Production code must use the standard parameterized format from sql.ts.\n */\nconst sqliteInlineFormat: FormatConfig = {\n escapeIdentifier: str => escapeSQLiteIdentifier(str),\n formatValue: value =>\n // undefined is our signal to use a placeholder\n // IMPORTANT. Changing this will break the planner as it will assume `NULL`\n // for constraints!\n value === undefined\n ? {\n placeholder: '?',\n value,\n }\n : {\n placeholder: inlineValue(value),\n value: undefined, // No binding needed since value is inlined\n },\n};\n\n/**\n * Compiles a SQL query with values inlined directly into the SQL string.\n *\n * WARNING: This should ONLY be used for cost estimation in the query planner.\n * Never use this for user-facing queries - always use the standard `compile()`\n * function from sql.ts which uses parameterized queries.\n *\n * @param sql The SQL query to compile\n * @returns SQL string with values inlined\n */\nexport function compileInline(sql: SQLQuery): string {\n return sql.format(sqliteInlineFormat).text;\n}\n"],"names":[],"mappings":";AAQA,SAAS,mBAAmB,KAAqB;AAC/C,SAAO,IAAI,IAAI,QAAQ,MAAM,IAAI,CAAC;AACpC;AAYA,SAAS,YAAY,OAAwB;AAC3C,MAAI,UAAU,MAAM;AAClB,WAAO;AAAA,EACT;AACA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,mBAAmB,KAAK;AAAA,EACjC;AACA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,OAAO,KAAK;AAAA,EACrB;AACA,MAAI,OAAO,UAAU,WAAW;AAE9B,WAAO,QAAQ,MAAM;AAAA,EACvB;AACA,MAAI,MAAM,QAAQ,KAAK,GAAG;AAExB,WAAO,mBAAmB,KAAK,UAAU,KAAK,CAAC;AAAA,EACjD;AAEA,SAAO,mBAAmB,KAAK,UAAU,KAAK,CAAC;AACjD;AAUA,MAAM,qBAAmC;AAAA,EACvC,kBAAkB,CAAA,QAAO,uBAAuB,GAAG;AAAA,EACnD,aAAa,CAAA;AAAA;AAAA;AAAA;AAAA,IAIX,UAAU,SACN;AAAA,MACE,aAAa;AAAA,MACb;AAAA,IAAA,IAEF;AAAA,MACE,aAAa,YAAY,KAAK;AAAA,MAC9B,OAAO;AAAA;AAAA,IAAA;AAAA;AAEjB;AAYO,SAAS,cAAc,KAAuB;AACnD,SAAO,IAAI,OAAO,kBAAkB,EAAE;AACxC;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-cost-model.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/sqlite-cost-model.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,mBAAmB,EAEpB,MAAM,6CAA6C,CAAC;AAIrD,OAAO,KAAK,EAAC,QAAQ,EAAY,MAAM,SAAS,CAAC;AAIjD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,sCAAsC,CAAC;AAiBtE;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,QAAQ,EACZ,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;CAAC,CAAC,GAC9D,mBAAmB,
|
|
1
|
+
{"version":3,"file":"sqlite-cost-model.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/sqlite-cost-model.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,mBAAmB,EAEpB,MAAM,6CAA6C,CAAC;AAIrD,OAAO,KAAK,EAAC,QAAQ,EAAY,MAAM,SAAS,CAAC;AAIjD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,sCAAsC,CAAC;AAiBtE;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,QAAQ,EACZ,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;CAAC,CAAC,GAC9D,mBAAmB,CAqDrB;AAoHD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK9C"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import SQLite3Database from "@rocicorp/zero-sqlite3";
|
|
2
2
|
import { buildSelectQuery } from "./query-builder.js";
|
|
3
|
-
import {
|
|
3
|
+
import { compileInline } from "./internal/sql-inline.js";
|
|
4
4
|
import { assert } from "../../shared/src/asserts.js";
|
|
5
5
|
import { must } from "../../shared/src/must.js";
|
|
6
6
|
import { SQLiteStatFanout } from "./sqlite-stat-fanout.js";
|
|
@@ -20,7 +20,7 @@ function createSQLiteCostModel(db, tableSpecs) {
|
|
|
20
20
|
void 0
|
|
21
21
|
// start is undefined here
|
|
22
22
|
);
|
|
23
|
-
const sql =
|
|
23
|
+
const sql = compileInline(query);
|
|
24
24
|
const stmt = db.prepare(sql);
|
|
25
25
|
const loops = getScanstatusLoops(stmt);
|
|
26
26
|
assert(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-cost-model.js","sources":["../../../../zqlite/src/sqlite-cost-model.ts"],"sourcesContent":["import type {Condition, Ordering} from '../../zero-protocol/src/ast.ts';\nimport type {\n ConnectionCostModel,\n CostModelCost,\n} from '../../zql/src/planner/planner-connection.ts';\nimport type {PlannerConstraint} from '../../zql/src/planner/planner-constraint.ts';\nimport SQLite3Database from '@rocicorp/zero-sqlite3';\nimport {buildSelectQuery, type NoSubqueryCondition} from './query-builder.ts';\nimport type {Database, Statement} from './db.ts';\nimport {
|
|
1
|
+
{"version":3,"file":"sqlite-cost-model.js","sources":["../../../../zqlite/src/sqlite-cost-model.ts"],"sourcesContent":["import type {Condition, Ordering} from '../../zero-protocol/src/ast.ts';\nimport type {\n ConnectionCostModel,\n CostModelCost,\n} from '../../zql/src/planner/planner-connection.ts';\nimport type {PlannerConstraint} from '../../zql/src/planner/planner-constraint.ts';\nimport SQLite3Database from '@rocicorp/zero-sqlite3';\nimport {buildSelectQuery, type NoSubqueryCondition} from './query-builder.ts';\nimport type {Database, Statement} from './db.ts';\nimport {compileInline} from './internal/sql-inline.ts';\nimport {assert} from '../../shared/src/asserts.ts';\nimport {must} from '../../shared/src/must.ts';\nimport type {SchemaValue} from '../../zero-types/src/schema-value.ts';\nimport {SQLiteStatFanout} from './sqlite-stat-fanout.ts';\n\n/**\n * Loop information returned by SQLite's scanstatus API.\n */\ninterface ScanstatusLoop {\n /** Unique identifier for this loop */\n selectId: number;\n /** Parent loop ID, or 0 for root loops */\n parentId: number;\n /** Estimated rows emitted per turn of parent loop */\n est: number;\n /** EXPLAIN text for this loop to determine: b-tree vs list subquery */\n explain: string;\n}\n\n/**\n * Creates a SQLite-based cost model for query planning.\n * Uses SQLite's scanstatus API to estimate query costs based on the actual\n * SQLite query planner's analysis.\n *\n * @param db Database instance for preparing statements\n * @param tableSpecs Map of table names to their table specs with ZQL schemas\n * @returns ConnectionCostModel function for use with the planner\n */\nexport function createSQLiteCostModel(\n db: Database,\n tableSpecs: Map<string, {zqlSpec: Record<string, SchemaValue>}>,\n): ConnectionCostModel {\n const fanoutEstimator = new SQLiteStatFanout(db);\n return (\n tableName: string,\n sort: Ordering,\n filters: Condition | undefined,\n constraint: PlannerConstraint | undefined,\n ): CostModelCost => {\n // Transform filters to remove correlated subqueries\n // The cost model can't handle correlated subqueries, so we estimate cost\n // without them. This is conservative - actual cost may be higher.\n const noSubqueryFilters = filters\n ? removeCorrelatedSubqueries(filters)\n : undefined;\n\n // Build the SQL query using the same logic as actual queries\n const {zqlSpec} = must(tableSpecs.get(tableName));\n\n const query = buildSelectQuery(\n tableName,\n zqlSpec,\n constraint,\n noSubqueryFilters,\n sort,\n undefined, // reverse is undefined here\n undefined, // start is undefined here\n );\n\n // Use compileInline to inline actual values into the SQL for cost estimation.\n // This allows SQLite's query planner to see real values and make better decisions\n // about index usage and query plans. This is safe here because it's only used for\n // cost estimation, not for executing user-facing queries (which use parameterized\n // queries via the standard compile() function).\n const sql = compileInline(query);\n\n // Prepare statement to get scanstatus information\n const stmt = db.prepare(sql);\n\n // Get scanstatus loops from the prepared statement\n const loops = getScanstatusLoops(stmt);\n\n // Scanstatus should always be available - if we get no loops, something is wrong\n assert(\n loops.length > 0,\n `Expected scanstatus to return at least one loop for query: ${sql}`,\n );\n\n const ret = estimateCost(loops, (columns: string[]) =>\n fanoutEstimator.getFanout(tableName, columns),\n );\n\n return ret;\n };\n}\n\n/**\n * Removes correlated subqueries from conditions.\n * The cost model estimates cost without correlated subqueries since\n * they can't be included in the scanstatus query.\n */\nfunction removeCorrelatedSubqueries(\n condition: Condition,\n): NoSubqueryCondition | undefined {\n switch (condition.type) {\n case 'correlatedSubquery':\n // Remove correlated subqueries - we can't estimate their cost via scanstatus\n return undefined;\n case 'simple':\n return condition;\n case 'and': {\n const filtered = condition.conditions\n .map(c => removeCorrelatedSubqueries(c))\n .filter((c): c is NoSubqueryCondition => c !== undefined);\n if (filtered.length === 0) return undefined;\n if (filtered.length === 1) return filtered[0];\n return {type: 'and', conditions: filtered};\n }\n case 'or': {\n const filtered = condition.conditions\n .map(c => removeCorrelatedSubqueries(c))\n .filter((c): c is NoSubqueryCondition => c !== undefined);\n if (filtered.length === 0) return undefined;\n if (filtered.length === 1) return filtered[0];\n return {type: 'or', conditions: filtered};\n }\n }\n}\n\n/**\n * Gets scanstatus loop information from a prepared statement.\n * Iterates through all query elements and extracts loop statistics.\n *\n * Uses SQLITE_SCANSTAT_COMPLEX flag (1) to get all loops including sorting operations.\n *\n * @param stmt Prepared statement to get scanstatus from\n * @returns Array of loop information, or empty array if scanstatus unavailable\n */\nfunction getScanstatusLoops(stmt: Statement): ScanstatusLoop[] {\n const loops: ScanstatusLoop[] = [];\n\n // Iterate through query elements by incrementing idx until we get undefined\n // which indicates we've reached the end\n for (let idx = 0; ; idx++) {\n const selectId = stmt.scanStatus(\n idx,\n SQLite3Database.SQLITE_SCANSTAT_SELECTID,\n 1,\n );\n\n if (selectId === undefined) {\n break;\n }\n\n loops.push({\n selectId: must(selectId),\n parentId: must(\n stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_PARENTID, 1),\n ),\n explain: must(\n stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_EXPLAIN, 1),\n ),\n est: must(stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_EST, 1)),\n });\n }\n\n return loops.sort((a, b) => a.selectId - b.selectId);\n}\n\n/**\n * Estimates the cost of a query based on scanstats from sqlite3_stmt_scanstatus_v2\n */\nfunction estimateCost(\n scanstats: ScanstatusLoop[],\n fanout: CostModelCost['fanout'],\n): CostModelCost {\n // Sort by selectId to process in execution order\n const sorted = [...scanstats].sort((a, b) => a.selectId - b.selectId);\n\n let totalRows = 0;\n let totalCost = 0;\n\n // Identify if there are multiple top-level (parentId=0) operations\n // If so, the first is typically the scan, and subsequent ones are sorts\n const topLevelOps = sorted.filter(s => s.parentId === 0);\n\n // We only consider top level ops since ZQL queries are single-table when hitting SQLite.\n // We do have a nested op in the case of `WHERE x IN (:arg)` but it is negligible\n // assuming :arg is small.\n let firstLoop = true;\n for (const op of topLevelOps) {\n if (firstLoop) {\n // First top-level op is the main scan\n // and determines the total number of rows output.\n totalRows = op.est;\n firstLoop = false;\n } else {\n if (op.explain.includes('ORDER BY')) {\n totalCost += btreeCost(totalRows);\n }\n }\n }\n\n return {\n rows: totalRows,\n startupCost: totalCost,\n fanout,\n };\n}\n\nexport function btreeCost(rows: number): number {\n // B-Tree construction is ~O(n log n) so we estimate the cost as such.\n // We divide the cost by 10 because sorting in SQLite is ~10x faster\n // than bringing the data into JS and sorting there.\n return (rows * Math.log2(rows)) / 10;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -6,6 +6,7 @@ import type { PrimaryKey } from '../../zero-protocol/src/primary-key.ts';
|
|
|
6
6
|
import type { SchemaValue, ValueType } from '../../zero-schema/src/table-schema.ts';
|
|
7
7
|
import type { DebugDelegate } from '../../zql/src/builder/debug-delegate.ts';
|
|
8
8
|
import { type Source, type SourceChange, type SourceInput } from '../../zql/src/ivm/source.ts';
|
|
9
|
+
import type { Stream } from '../../zql/src/ivm/stream.ts';
|
|
9
10
|
import type { Database } from './db.ts';
|
|
10
11
|
/**
|
|
11
12
|
* A source that is backed by a SQLite table.
|
|
@@ -42,8 +43,8 @@ export declare class TableSource implements Source {
|
|
|
42
43
|
setDB(db: Database): void;
|
|
43
44
|
connect(sort: Ordering, filters?: Condition, splitEditKeys?: Set<string>, debug?: DebugDelegate): SourceInput;
|
|
44
45
|
toSQLiteRow(row: Row): Row;
|
|
45
|
-
push(change: SourceChange):
|
|
46
|
-
genPush(change: SourceChange): Generator<undefined, void, unknown>;
|
|
46
|
+
push(change: SourceChange): Stream<'yield'>;
|
|
47
|
+
genPush(change: SourceChange): Generator<"yield" | undefined, void, unknown>;
|
|
47
48
|
/**
|
|
48
49
|
* Retrieves a row from the backing DB by a unique key, or `undefined` if such a
|
|
49
50
|
* row does not exist. This is not used in the IVM pipeline but is useful
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table-source.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/table-source.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,+BAA+B,CAAC;AAK7D,OAAO,KAAK,EAAC,SAAS,EAAE,QAAQ,EAAC,MAAM,gCAAgC,CAAC;AACxE,OAAO,KAAK,EAAC,GAAG,EAAQ,MAAM,iCAAiC,CAAC;AAChE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,wCAAwC,CAAC;AACvE,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACV,MAAM,uCAAuC,CAAC;AAC/C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,yCAAyC,CAAC;AAe3E,OAAO,EACL,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"table-source.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/table-source.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,+BAA+B,CAAC;AAK7D,OAAO,KAAK,EAAC,SAAS,EAAE,QAAQ,EAAC,MAAM,gCAAgC,CAAC;AACxE,OAAO,KAAK,EAAC,GAAG,EAAQ,MAAM,iCAAiC,CAAC;AAChE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,wCAAwC,CAAC;AACvE,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACV,MAAM,uCAAuC,CAAC;AAC/C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,yCAAyC,CAAC;AAe3E,OAAO,EACL,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,6BAA6B,CAAC;AACxD,OAAO,KAAK,EAAC,QAAQ,EAAY,MAAM,SAAS,CAAC;AAqBjD;;;;;;;;;;;;;GAaG;AACH,qBAAa,WAAY,YAAW,MAAM;;IAexC;;;;;OAKG;gBAED,UAAU,EAAE,UAAU,EACtB,SAAS,EAAE,SAAS,EACpB,EAAE,EAAE,QAAQ,EACZ,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACpC,UAAU,EAAE,UAAU,EACtB,WAAW,gBAAc;IAiB3B,IAAI,WAAW;;;;MAMd;IAED;;;OAGG;IACH,KAAK,CAAC,EAAE,EAAE,QAAQ;IA4FlB,OAAO,CACL,IAAI,EAAE,QAAQ,EACd,OAAO,CAAC,EAAE,SAAS,EACnB,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAC3B,KAAK,CAAC,EAAE,aAAa;IAuCvB,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG;IAqGzB,IAAI,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAAC,OAAO,CAAC;IAQ3C,OAAO,CAAC,MAAM,EAAE,YAAY;IA4F7B;;;;;;OAMG;IACH,MAAM,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,SAAS;CA6BrC;AA6BD,wBAAgB,aAAa,CAC3B,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,GAAG,EAAE,GAAG,EACR,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GACvC,SAAS,OAAO,EAAE,CAEpB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,wCAa/C;AAED,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACvC,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,MAAM,GAChB,GAAG,CAaL;AAwCD,qBAAa,qBAAsB,SAAQ,KAAK;CAAG"}
|
|
@@ -257,8 +257,11 @@ class TableSource {
|
|
|
257
257
|
rowIterator.return?.();
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
|
-
push(change) {
|
|
261
|
-
for (const
|
|
260
|
+
*push(change) {
|
|
261
|
+
for (const result of this.genPush(change)) {
|
|
262
|
+
if (result === "yield") {
|
|
263
|
+
yield result;
|
|
264
|
+
}
|
|
262
265
|
}
|
|
263
266
|
}
|
|
264
267
|
*genPush(change) {
|