Implement checked aggregation plans, native SQL, and bounded RPC capture
This commit is contained in:
+71
-52
@@ -34,6 +34,8 @@ import {
|
||||
type InterfaceRevisionId,
|
||||
} from "../capability-model/types.js";
|
||||
import { querySchema } from "./schema.js";
|
||||
import { objectRow } from "./relational-schema.js";
|
||||
import { relationalCompiler } from "./relational-compile.js";
|
||||
import {
|
||||
QueryCompileError,
|
||||
type QueryDeclaration,
|
||||
@@ -58,26 +60,6 @@ const fail = (code: string, message: string, node?: ASTNode): never => {
|
||||
: undefined,
|
||||
);
|
||||
};
|
||||
const shapeScalar = (name: string): ValueType => {
|
||||
switch (name) {
|
||||
case "Boolean":
|
||||
return valueType.bool;
|
||||
case "Int":
|
||||
return valueType.int32;
|
||||
case "Int64":
|
||||
return valueType.int64;
|
||||
case "UInt32":
|
||||
return valueType.uint32;
|
||||
case "UInt64":
|
||||
return valueType.uint64;
|
||||
case "Float":
|
||||
return valueType.double;
|
||||
case "Bytes":
|
||||
return valueType.bytes;
|
||||
default:
|
||||
return valueType.string;
|
||||
}
|
||||
};
|
||||
|
||||
/** Only immutable repository sources call this. Runtime requests never compile documents. */
|
||||
export async function compileQuery(
|
||||
@@ -104,7 +86,7 @@ export async function compileQuery(
|
||||
definitions.push(...document.definitions);
|
||||
}
|
||||
const document: DocumentNode = { kind: "Document" as DocumentNode["kind"], definitions };
|
||||
const generated = querySchema(declaration, interfaces);
|
||||
const generated = querySchema(declaration, interfaces, document);
|
||||
const operations = definitions.filter((entry) => entry.kind === "OperationDefinition");
|
||||
if (
|
||||
operations.length !== 1 ||
|
||||
@@ -114,6 +96,8 @@ export async function compileQuery(
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", "Exactly one named query matching the declaration is required");
|
||||
visit(document, {
|
||||
Field(node) {
|
||||
if (node.name.value === "_unavailable")
|
||||
fail("QUERY_AGGREGATE_TYPE", "No supported field exists at this operator path", node);
|
||||
if (node.name.value.startsWith("__")) fail("QUERY_UNSUPPORTED_FEATURE", "Introspection is not supported", node);
|
||||
},
|
||||
Directive(node) {
|
||||
@@ -130,6 +114,11 @@ export async function compileQuery(
|
||||
fail("QUERY_VALIDATION", error.message, error.nodes?.[0]);
|
||||
}
|
||||
const effects = new Map<string, QueryFieldEffect>();
|
||||
const fragments = new Map(
|
||||
definitions
|
||||
.filter((entry): entry is FragmentDefinitionNode => entry.kind === "FragmentDefinition")
|
||||
.map((entry) => [entry.name.value, entry]),
|
||||
);
|
||||
const mark = (id: InterfaceRevisionId, name: string, use: QueryUse, node: ASTNode) => {
|
||||
const contract = generated.contracts.get(id)!;
|
||||
const member = contract.members.find((entry) => entry.displayName === name);
|
||||
@@ -167,7 +156,13 @@ export async function compileQuery(
|
||||
node,
|
||||
);
|
||||
};
|
||||
const relational = relationalCompiler(generated, declaration, mark, fragments);
|
||||
const compiledPredicates = new WeakMap<ASTNode, import("./relational.js").QueryPredicate>();
|
||||
const inputEffects = (id: InterfaceRevisionId, value: ValueNode, use: "predicate" | "order") => {
|
||||
if (use === "predicate") {
|
||||
relational.predicate(objectRow(id), value);
|
||||
return;
|
||||
}
|
||||
if (value.kind === "Variable") {
|
||||
// A dynamic filter may name any field in its declared input type.
|
||||
for (const member of generated.contracts.get(id)!.members)
|
||||
@@ -175,12 +170,10 @@ export async function compileQuery(
|
||||
} else if (value.kind === "ListValue") value.values.forEach((child) => inputEffects(id, child, use));
|
||||
else if (value.kind === "ObjectValue")
|
||||
for (const field of value.fields) {
|
||||
if (use === "predicate" && ["and", "or", "not"].includes(field.name.value)) inputEffects(id, field.value, use);
|
||||
else mark(id, field.name.value, use, field);
|
||||
mark(id, field.name.value, use, field);
|
||||
}
|
||||
};
|
||||
const info = new TypeInfo(generated.schema);
|
||||
let hasContinuation = false;
|
||||
visit(
|
||||
document,
|
||||
visitWithTypeInfo(info, {
|
||||
@@ -200,9 +193,9 @@ export async function compileQuery(
|
||||
if (bounds[0]!.name.value === "all" && node.arguments?.some((argument) => argument.name.value === "after"))
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", "Bounded-all does not accept a continuation", node);
|
||||
for (const argument of node.arguments ?? []) {
|
||||
if (argument.name.value === "where") inputEffects(target, argument.value, "predicate");
|
||||
if (argument.name.value === "where")
|
||||
compiledPredicates.set(node, relational.predicate(objectRow(target), argument.value));
|
||||
if (argument.name.value === "orderBy") inputEffects(target, argument.value, "order");
|
||||
if (argument.name.value === "after") hasContinuation = true;
|
||||
if (
|
||||
["first", "all"].includes(argument.name.value) &&
|
||||
argument.value.kind !== "Variable" &&
|
||||
@@ -216,6 +209,33 @@ export async function compileQuery(
|
||||
argument,
|
||||
);
|
||||
}
|
||||
if (node.arguments?.some((a) => a.name.value === "after")) {
|
||||
const rpcOrder = (value: ValueNode): boolean =>
|
||||
value.kind === "Variable"
|
||||
? generated.contracts
|
||||
.get(target)!
|
||||
.members.some((m) => m.kind === "value" && m.queryRead?.execution === "rpc-permitted")
|
||||
: value.kind === "ListValue"
|
||||
? value.values.some(rpcOrder)
|
||||
: value.kind === "ObjectValue" &&
|
||||
value.fields.some((f) =>
|
||||
generated.contracts
|
||||
.get(target)!
|
||||
.members.some(
|
||||
(m) =>
|
||||
m.displayName === f.name.value &&
|
||||
m.kind === "value" &&
|
||||
m.queryRead?.execution === "rpc-permitted",
|
||||
),
|
||||
);
|
||||
const order = node.arguments.find((a) => a.name.value === "orderBy");
|
||||
if (relational.needsRpc(compiledPredicates.get(node)) || (order && rpcOrder(order.value)))
|
||||
fail(
|
||||
"QUERY_UNSUPPORTED_FEATURE",
|
||||
"RPC predicates/order require a bounded first/all window without continuation",
|
||||
node,
|
||||
);
|
||||
}
|
||||
},
|
||||
FragmentSpread(node) {
|
||||
const fragment = definitions.find(
|
||||
@@ -226,18 +246,6 @@ export async function compileQuery(
|
||||
},
|
||||
}),
|
||||
);
|
||||
if (
|
||||
hasContinuation &&
|
||||
[...effects.values()].some(
|
||||
(effect) => effect.execution === "rpc-permitted" && effect.uses.some((use) => use !== "select"),
|
||||
)
|
||||
)
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", "RPC predicates/order support bounded first windows, not continuation");
|
||||
const fragments = new Map(
|
||||
definitions
|
||||
.filter((entry): entry is FragmentDefinitionNode => entry.kind === "FragmentDefinition")
|
||||
.map((entry) => [entry.name.value, entry]),
|
||||
);
|
||||
let expandedFields = 0;
|
||||
// GraphQL merges repeated response keys. In particular, fragments may each
|
||||
// contribute different children of one relationship; last-write-wins would
|
||||
@@ -280,16 +288,6 @@ export async function compileQuery(
|
||||
depth + 1,
|
||||
conditional || !!selection.directives?.length,
|
||||
);
|
||||
if (selection.name.value === "_qx") {
|
||||
const contract = generated.byName.get(type.name)!;
|
||||
const metadataFields: Record<string, ValueType> = {};
|
||||
for (const metadata of selection.selectionSet?.selections ?? []) {
|
||||
if (metadata.kind !== "Field" || metadata.name.value !== "ref")
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", "Select _qx.ref directly", metadata);
|
||||
else metadataFields[metadata.alias?.value ?? "ref"] = valueType.interfaceRef(contract.revisionId);
|
||||
}
|
||||
fields[key] = { kind: "record", fields: metadataFields };
|
||||
}
|
||||
if ((conditional || selection.directives?.length) && fields[key]!.kind !== "optional")
|
||||
fields[key] = valueType.optional(fields[key]!);
|
||||
if (previous) fields[key] = mergeOutput(previous, fields[key]!);
|
||||
@@ -298,7 +296,10 @@ export async function compileQuery(
|
||||
if (selections) add(selections, conditional);
|
||||
return { kind: "record", fields };
|
||||
}
|
||||
return shapeScalar(getNamedType(type)!.name);
|
||||
return (
|
||||
generated.scalarTypes.get(getNamedType(type)!.name) ??
|
||||
fail("QUERY_TYPE", `Unsupported query scalar ${getNamedType(type)!.name}`)
|
||||
);
|
||||
};
|
||||
const variables: Record<string, ValueType> = {};
|
||||
const inputShape = (type: GraphQLType, seen = new Set<string>()): ValueType => {
|
||||
@@ -324,7 +325,9 @@ export async function compileQuery(
|
||||
),
|
||||
};
|
||||
}
|
||||
if (isScalarType(type) || isEnumType(type)) return shapeScalar(type.name);
|
||||
if (isEnumType(type)) return valueType.string;
|
||||
if (isScalarType(type))
|
||||
return generated.scalarTypes.get(type.name) ?? fail("QUERY_TYPE", `Unsupported query scalar ${type.name}`);
|
||||
return fail("QUERY_VARIABLE_TYPE", "Unsupported variable type");
|
||||
};
|
||||
for (const variable of operations[0]!.variableDefinitions ?? [])
|
||||
@@ -370,6 +373,19 @@ export async function compileQuery(
|
||||
const contract = generated.byName.get(parent.name);
|
||||
const member = contract?.members.find((entry) => entry.displayName === node.name.value);
|
||||
const type = getNamedType(parent.getFields()[node.name.value]!.type);
|
||||
const role = generated.relational.roles.get(parent.name);
|
||||
const relationship =
|
||||
role?.kind === "relations"
|
||||
? generated.relational.relations(role.row).find((m) => m.displayName === node.name.value)
|
||||
: undefined;
|
||||
const plan =
|
||||
relationship && role?.row.kind === "object" && node.selectionSet
|
||||
? relational.plan(role.row.interfaceRevisionId, relationship, node.selectionSet, (set, name) => {
|
||||
const t = generated.schema.getType(name);
|
||||
if (!t || !isObjectType(t)) return fail("QUERY_TYPE", `Missing generated output type ${name}`, node);
|
||||
return selection(set, t);
|
||||
})
|
||||
: undefined;
|
||||
return [
|
||||
{
|
||||
name: node.name.value,
|
||||
@@ -381,15 +397,18 @@ export async function compileQuery(
|
||||
(node.arguments ?? []).map((entry) => [entry.name.value, argument(entry.value)]),
|
||||
),
|
||||
selection: node.selectionSet && isObjectType(type) ? selection(node.selectionSet, type) : [],
|
||||
...(plan ? { relational: plan } : {}),
|
||||
...(compiledPredicates.has(node) ? { predicate: compiledPredicates.get(node)! } : {}),
|
||||
},
|
||||
];
|
||||
});
|
||||
const normalized = print(document),
|
||||
schema = printSchema(generated.schema);
|
||||
const selections = selection(operations[0]!.selectionSet, generated.schema.getQueryType()!);
|
||||
const definitionDigest = createHash("sha256")
|
||||
.update(
|
||||
canonicalJson({
|
||||
semantics: 1,
|
||||
semantics: 2,
|
||||
declaration,
|
||||
normalized,
|
||||
interfaces: [...generated.byName.values()].sort((a, b) =>
|
||||
@@ -406,7 +425,7 @@ export async function compileQuery(
|
||||
variables: { kind: "record", fields: variables },
|
||||
output: result,
|
||||
effects: [...effects.values()],
|
||||
selection: selection(operations[0]!.selectionSet, generated.schema.getQueryType()!),
|
||||
selection: selections,
|
||||
variableDefaults: Object.fromEntries(
|
||||
(operations[0]!.variableDefinitions ?? [])
|
||||
.filter((entry) => entry.defaultValue)
|
||||
|
||||
Reference in New Issue
Block a user