Reject unsupported query operators during execution planning

This commit is contained in:
Timothy J. Aveni
2026-09-20 23:58:07 -07:00
parent 47a3fe9eae
commit 08e322e59f
4 changed files with 132 additions and 0 deletions
+34
View File
@@ -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;