Files
quixos-protocol/src/query/relational.ts
T

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;