Complete prepared presentation build and runtime integration
This commit is contained in:
+49
-31
@@ -6,6 +6,7 @@ import { QueryCompileError, type CheckedQuery } from "./types.js";
|
||||
/** Bindings refer to checked values, never executable callbacks. A path cannot
|
||||
* silently cross a nullable value or expand an unbounded collection. */
|
||||
export type QueryBinding =
|
||||
| { flatMap: { source: QueryBinding; path: string[] } }
|
||||
| { literal: string | boolean }
|
||||
| { equal: [QueryBinding, QueryBinding] }
|
||||
| { from: "input"; path: string[] }
|
||||
@@ -54,37 +55,8 @@ export function compileQueryComposition(
|
||||
const outputs = new Map<string, ValueType>();
|
||||
const dependencies = new Map<string, string>();
|
||||
let count = 0;
|
||||
function binding(expression: QueryBinding, item?: ValueType): ValueType {
|
||||
if ("literal" in expression)
|
||||
return { kind: "scalar", name: typeof expression.literal === "string" ? "string" : "bool" };
|
||||
if ("equal" in expression) {
|
||||
const left = binding(expression.equal[0], item);
|
||||
const right = binding(expression.equal[1], item);
|
||||
if (left.kind !== "scalar" || !equal(left, right)) fail("equal requires matching scalar operands");
|
||||
return { kind: "scalar", name: "bool" };
|
||||
}
|
||||
if ("record" in expression)
|
||||
return {
|
||||
kind: "record",
|
||||
fields: Object.fromEntries(
|
||||
Object.entries(expression.record).map(([key, value]) => [key, binding(value, item)]),
|
||||
),
|
||||
};
|
||||
let type =
|
||||
expression.from === "input" ? source.input : expression.from === "item" ? item : outputs.get(expression.name);
|
||||
if (!type)
|
||||
return fail(
|
||||
expression.from === "result"
|
||||
? `result ${expression.name} must name an earlier step (forward references and cycles are unsupported)`
|
||||
: "item is only available within a bounded each step",
|
||||
);
|
||||
for (const key of expression.path) {
|
||||
if (type.kind !== "record" || !Object.hasOwn(type.fields, key))
|
||||
return fail(`cannot select ${key} from ${type.kind}; nulls and collections need an explicit boundary`);
|
||||
type = type.fields[key]!;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
const binding = (expression: QueryBinding, item?: ValueType) =>
|
||||
queryBindingType(expression, source.input, outputs, item);
|
||||
const equal = (a: ValueType, b: ValueType) => canonicalJson(a) === canonicalJson(b);
|
||||
for (const step of source.steps) {
|
||||
if (!step.name || outputs.has(step.name)) fail(`duplicate or empty step name ${step.name}`);
|
||||
@@ -137,3 +109,49 @@ export function compileQueryComposition(
|
||||
};
|
||||
return { ...result, definitionDigest: createHash("sha256").update(canonicalJson(result)).digest("hex") };
|
||||
}
|
||||
|
||||
export function queryBindingType(
|
||||
expression: QueryBinding,
|
||||
input: ValueType,
|
||||
outputs: ReadonlyMap<string, ValueType>,
|
||||
item?: ValueType,
|
||||
): ValueType {
|
||||
const fail = (message: string): never => {
|
||||
throw new QueryCompileError("QUERY_COMPOSITION", message);
|
||||
};
|
||||
const binding = (expression: QueryBinding, item?: ValueType) => queryBindingType(expression, input, outputs, item);
|
||||
const equal = (a: ValueType, b: ValueType) => canonicalJson(a) === canonicalJson(b);
|
||||
if ("flatMap" in expression) {
|
||||
const rows = binding(expression.flatMap.source, item);
|
||||
if (rows.kind !== "list") return fail("flatMap requires a list");
|
||||
const children = binding({ from: "item", path: expression.flatMap.path }, rows.value);
|
||||
if (children.kind !== "list") return fail("flatMap path requires a list");
|
||||
return children;
|
||||
}
|
||||
if ("literal" in expression)
|
||||
return { kind: "scalar", name: typeof expression.literal === "string" ? "string" : "bool" };
|
||||
if ("equal" in expression) {
|
||||
const left = binding(expression.equal[0], item);
|
||||
const right = binding(expression.equal[1], item);
|
||||
if (left.kind !== "scalar" || !equal(left, right)) fail("equal requires matching scalar operands");
|
||||
return { kind: "scalar", name: "bool" };
|
||||
}
|
||||
if ("record" in expression)
|
||||
return {
|
||||
kind: "record",
|
||||
fields: Object.fromEntries(Object.entries(expression.record).map(([key, value]) => [key, binding(value, item)])),
|
||||
};
|
||||
let type = expression.from === "input" ? input : expression.from === "item" ? item : outputs.get(expression.name);
|
||||
if (!type)
|
||||
return fail(
|
||||
expression.from === "result"
|
||||
? `result ${expression.name} must name an earlier step (forward references and cycles are unsupported)`
|
||||
: "item is only available within a bounded each step",
|
||||
);
|
||||
for (const key of expression.path) {
|
||||
if (type.kind !== "record" || !Object.hasOwn(type.fields, key))
|
||||
return fail(`cannot select ${key} from ${type.kind}; nulls and collections need an explicit boundary`);
|
||||
type = type.fields[key]!;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user