Reject unsupported query operators during execution planning
This commit is contained in:
@@ -14,6 +14,7 @@ import { canonicalJson, contentDigest } from "../capability-model/evolution.js";
|
||||
import { resolveForwardedBinding } from "../capability-model/forwarding.js";
|
||||
import { linkQueries, type LinkedQuery } from "../query/link.js";
|
||||
import { type BindingSchema } from "../bindings/index.js";
|
||||
import { unsupportedQueryOperator } from "./query-operators.js";
|
||||
|
||||
export class ExecutionPlanError extends Error {
|
||||
constructor(
|
||||
@@ -305,6 +306,35 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
|
||||
admittedOperations: [],
|
||||
}));
|
||||
for (const query of plannedQueries) {
|
||||
for (const field of query.query.fields) {
|
||||
const operation = operationMap.get(publicOperationKey(field.atomId, field.interfaceRevisionId, field.getter))!;
|
||||
const effect = query.query.checked.effects.find(
|
||||
(effect) => effect.interfaceRevisionId === field.interfaceRevisionId && effect.memberId === field.memberId,
|
||||
);
|
||||
if (
|
||||
operation.execution.kind === "remote" &&
|
||||
(query.query.checked.declaration.placement?.kind === "replica" || effect?.uses.some((use) => use !== "select"))
|
||||
)
|
||||
throw new ExecutionPlanError(
|
||||
"QUERY_REMOTE_MEMBERSHIP_UNSUPPORTED",
|
||||
query.id,
|
||||
"Remote getters may enrich selected values after native snapshot closure; they cannot determine membership, order or canonical-input admission. Expose an accepted canonical value or split the service result into a separate query.",
|
||||
);
|
||||
const member = interfaces.get(field.interfaceRevisionId)?.members.find((member) => member.id === field.memberId);
|
||||
if (member?.kind === "relationship" && operation.binding.kind !== "edge")
|
||||
throw new ExecutionPlanError(
|
||||
"QUERY_NON_NATIVE_RELATION",
|
||||
query.id,
|
||||
"This query runtime traverses native graph edges. Expose an edge relationship, or return a computed service value through a value getter.",
|
||||
);
|
||||
}
|
||||
const unsupported = unsupportedQueryOperator(query.query.checked);
|
||||
if (unsupported)
|
||||
throw new ExecutionPlanError(
|
||||
"QUERY_OPERATOR_UNSUPPORTED",
|
||||
query.id,
|
||||
`This execution world does not support ${unsupported}. Use a bounded portable getter over admitted inputs, or a server service returning the computed value. Query placement will not change automatically.`,
|
||||
);
|
||||
const placement = query.query.checked.declaration.placement;
|
||||
if (placement?.kind !== "local") continue;
|
||||
const source = plannedQueries.find(
|
||||
@@ -324,6 +354,10 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
|
||||
const paths = new Map<string, boolean>();
|
||||
const visit = (selections: LinkedQuery["checked"]["selection"], parents: string[]) => {
|
||||
for (const selection of selections) {
|
||||
for (const stage of selection.relational?.stages ?? []) {
|
||||
if (stage.operation.kind === "source")
|
||||
paths.set(JSON.stringify([...parents, stage.operation.memberId]), false);
|
||||
}
|
||||
const member = selection.interfaceRevisionId
|
||||
? interfaces.get(selection.interfaceRevisionId)?.members.find((m) => m.id === selection.memberId)
|
||||
: undefined;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { CheckedQuery, QuerySelection } from "../query/types.js";
|
||||
import type { QueryPredicate, QueryPathStep } from "../query/relational.js";
|
||||
|
||||
/** Supported by the fresh coordinator/replica interpreters. The language parser
|
||||
* can describe more operators; that does not make them executable in this world. */
|
||||
export function unsupportedQueryOperator(query: CheckedQuery): string | undefined {
|
||||
const path = (steps: QueryPathStep[]) =>
|
||||
steps.some((step) => step.kind !== "relation") ? "expanded source/target paths" : undefined;
|
||||
const predicate = (value: QueryPredicate): string | undefined => {
|
||||
if (value.kind === "reduce") return "aggregate predicates";
|
||||
if (value.kind === "and" || value.kind === "or") return value.children.map(predicate).find(Boolean);
|
||||
if (value.kind === "not") return predicate(value.child);
|
||||
if (value.kind === "compare")
|
||||
return (
|
||||
path(value.expression.path) ??
|
||||
(value.expression.leaf.kind === "mapKey" ? "keyed relationship operands" : undefined)
|
||||
);
|
||||
if (value.kind === "relation") return path(value.path) ?? (value.predicate && predicate(value.predicate));
|
||||
return undefined;
|
||||
};
|
||||
const conditional = (selections: readonly QuerySelection[]): boolean =>
|
||||
selections.some((selection) => selection.conditions.length > 0 || conditional(selection.selection));
|
||||
const walk = (selections: readonly QuerySelection[]): string | undefined => {
|
||||
for (const selection of selections) {
|
||||
const after = selection.arguments.after;
|
||||
if (after && !(after.kind === "literal" && after.value === null)) return "cursor continuation";
|
||||
if (selection.predicate) {
|
||||
const reason = predicate(selection.predicate);
|
||||
if (reason) return reason;
|
||||
}
|
||||
if (selection.relational) {
|
||||
if (query.declaration.placement?.kind === "replica") return "aggregate selections in canonical-input scopes";
|
||||
const { stages, terminals } = selection.relational;
|
||||
if (stages.length !== 1 || stages[0]!.operation.kind !== "source")
|
||||
return "relational expansion, filtering or distinct stages";
|
||||
if (terminals.length !== 1) return "multiple aggregate terminals";
|
||||
const terminal = terminals[0]!;
|
||||
if (
|
||||
terminal.kind !== "aggregate" ||
|
||||
terminal.stage !== stages[0]!.id ||
|
||||
terminal.keys.length ||
|
||||
terminal.order.length ||
|
||||
terminal.residual ||
|
||||
terminal.where ||
|
||||
terminal.having ||
|
||||
terminal.first ||
|
||||
terminal.all ||
|
||||
terminal.after ||
|
||||
terminal.reductions.some(
|
||||
(reduction) => reduction.reduction.operator !== "count" || reduction.reduction.operand,
|
||||
)
|
||||
)
|
||||
return "grouping or reductions other than a direct relationship count";
|
||||
if (conditional(selection.selection)) return "conditional aggregate projections";
|
||||
}
|
||||
const reason = walk(selection.selection);
|
||||
if (reason) return reason;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
return walk(query.selection);
|
||||
}
|
||||
Reference in New Issue
Block a user