150 lines
5.3 KiB
TypeScript
150 lines
5.3 KiB
TypeScript
import type { InterfaceRevisionId, MemberId, ValueType } from "../capability-model/types.js";
|
|
import { valueType } from "../capability-model/types.js";
|
|
import { QueryCompileError, type QueryArgument, type QueryUse } from "./types.js";
|
|
|
|
/** A row is an input membership, not a newly allocated Camino object. */
|
|
export type QueryRow =
|
|
| {
|
|
kind: "object";
|
|
interfaceRevisionId: InterfaceRevisionId;
|
|
membership?: { keyType?: "string" | "boolean" | "int64" };
|
|
}
|
|
| { kind: "pair"; source: QueryRow; target: QueryRow };
|
|
|
|
export type AggregateOperator = "count" | "countPresent" | "sum" | "avg" | "min" | "max";
|
|
export const aggregateOperators: readonly AggregateOperator[] = ["count", "countPresent", "sum", "avg", "min", "max"];
|
|
|
|
export function aggregateType(operator: AggregateOperator, input?: ValueType): ValueType {
|
|
if (operator === "count") return valueType.uint64;
|
|
const base = input?.kind === "optional" ? input.value : input;
|
|
if (operator === "countPresent" && (base?.kind === "scalar" || base?.kind === "object-ref")) return valueType.uint64;
|
|
if (base?.kind !== "scalar")
|
|
throw new QueryCompileError("QUERY_AGGREGATE_TYPE", `${operator} requires a supported scalar operand`);
|
|
const numeric = ["int32", "uint32", "int64", "uint64", "double"].includes(base.name);
|
|
if ((operator === "min" || operator === "max") && (numeric || base.name === "string"))
|
|
return valueType.optional(base);
|
|
if (numeric && operator === "avg") return valueType.optional(valueType.double);
|
|
if (numeric && operator === "sum")
|
|
return valueType.optional(
|
|
base.name === "double" ? valueType.double : base.name.startsWith("u") ? valueType.uint64 : valueType.int64,
|
|
);
|
|
throw new QueryCompileError("QUERY_AGGREGATE_TYPE", `${operator} does not accept ${base.name}`);
|
|
}
|
|
|
|
export function queryKeyType(type: ValueType): boolean {
|
|
if (type.kind === "optional") return queryKeyType(type.value);
|
|
return type.kind === "object-ref" || (type.kind === "scalar" && type.name !== "bytes");
|
|
}
|
|
|
|
/** Paths are resolved once by the compiler. Runtime never resolves authored names. */
|
|
export type QueryPathStep =
|
|
| { kind: "source" | "target" }
|
|
| {
|
|
kind: "relation";
|
|
interfaceRevisionId: InterfaceRevisionId;
|
|
memberId: MemberId;
|
|
targetInterfaceRevisionId: InterfaceRevisionId;
|
|
optional: boolean;
|
|
};
|
|
export interface QueryExpression {
|
|
path: QueryPathStep[];
|
|
leaf:
|
|
| { kind: "field"; interfaceRevisionId: InterfaceRevisionId; memberId: MemberId }
|
|
| { kind: "ref"; interfaceRevisionId: InterfaceRevisionId }
|
|
| { kind: "entry" | "mapKey" };
|
|
type: ValueType;
|
|
}
|
|
export interface QueryReduction {
|
|
operator: AggregateOperator;
|
|
operand?: QueryExpression;
|
|
type: ValueType;
|
|
}
|
|
export type QueryPredicate =
|
|
| { kind: "and" | "or"; children: QueryPredicate[] }
|
|
| { kind: "not"; child: QueryPredicate }
|
|
| {
|
|
kind: "compare";
|
|
expression: QueryExpression;
|
|
operator: "eq" | "in" | "isNull" | "lt" | "lte" | "gt" | "gte";
|
|
value: QueryArgument;
|
|
}
|
|
| {
|
|
kind: "relation";
|
|
path: QueryPathStep[];
|
|
interfaceRevisionId: InterfaceRevisionId;
|
|
memberId: MemberId;
|
|
targetInterfaceRevisionId: InterfaceRevisionId;
|
|
operator: "some" | "none" | "is" | "isNull";
|
|
predicate?: QueryPredicate;
|
|
value?: QueryArgument;
|
|
}
|
|
| {
|
|
kind: "reduce";
|
|
path: QueryPathStep[];
|
|
interfaceRevisionId: InterfaceRevisionId;
|
|
memberId: MemberId;
|
|
targetInterfaceRevisionId: InterfaceRevisionId;
|
|
where?: QueryPredicate;
|
|
having: QueryAggregatePredicate;
|
|
};
|
|
export type QueryAggregatePredicate =
|
|
| { kind: "and" | "or"; children: QueryAggregatePredicate[] }
|
|
| { kind: "not"; child: QueryAggregatePredicate }
|
|
| {
|
|
kind: "compare";
|
|
expression: QueryExpression | QueryReduction;
|
|
operator: "eq" | "in" | "isNull" | "lt" | "lte" | "gt" | "gte";
|
|
value: QueryArgument;
|
|
};
|
|
export interface QueryKey {
|
|
path: string[];
|
|
expression: QueryExpression;
|
|
}
|
|
export interface QueryRelationalStage {
|
|
id: number;
|
|
input?: number;
|
|
row: QueryRow;
|
|
operation:
|
|
| {
|
|
kind: "source";
|
|
interfaceRevisionId: InterfaceRevisionId;
|
|
memberId: MemberId;
|
|
targetInterfaceRevisionId: InterfaceRevisionId;
|
|
}
|
|
| { kind: "filter"; predicate: QueryPredicate }
|
|
| {
|
|
kind: "expand";
|
|
path: QueryPathStep[];
|
|
interfaceRevisionId: InterfaceRevisionId;
|
|
memberId: MemberId;
|
|
targetInterfaceRevisionId: InterfaceRevisionId;
|
|
}
|
|
| { kind: "distinct"; keys: QueryKey[] };
|
|
}
|
|
export interface QueryRelationalTerminal {
|
|
stage: number;
|
|
path: string[];
|
|
kind: "aggregate" | "groups";
|
|
keys: QueryKey[];
|
|
reductions: { path: string[]; reduction: QueryReduction }[];
|
|
where?: QueryPredicate;
|
|
having?: QueryAggregatePredicate;
|
|
order: { expression: QueryExpression | QueryReduction; descending: boolean }[];
|
|
first?: QueryArgument;
|
|
all?: QueryArgument;
|
|
after?: QueryArgument;
|
|
/** Projection tree remains separate from the operator plan. */
|
|
selection: import("./types.js").QuerySelection[];
|
|
residual: boolean;
|
|
}
|
|
export interface QueryRelationalPlan {
|
|
stages: QueryRelationalStage[];
|
|
terminals: QueryRelationalTerminal[];
|
|
}
|
|
export type QueryEffectRecorder = (
|
|
id: InterfaceRevisionId,
|
|
name: string,
|
|
use: QueryUse,
|
|
node: import("graphql").ASTNode,
|
|
) => void;
|