Implement checked aggregation plans, native SQL, and bounded RPC capture

This commit is contained in:
Timothy J. Aveni
2026-09-17 18:23:16 -07:00
parent cd13120937
commit e61b0a36ac
17 changed files with 2968 additions and 100 deletions
+497
View File
@@ -0,0 +1,497 @@
import {
GraphQLBoolean,
GraphQLString,
GraphQLInt,
GraphQLScalarType,
GraphQLObjectType,
GraphQLInputObjectType,
GraphQLList,
GraphQLNonNull,
type GraphQLInputType,
type GraphQLOutputType,
type GraphQLInputFieldConfigMap,
type GraphQLFieldConfigMap,
type GraphQLEnumType,
type DocumentNode,
type SelectionSetNode,
type FragmentDefinitionNode,
} from "graphql";
import { createHash } from "node:crypto";
import {
valueType,
type ValueType,
type InterfaceRevision,
type InterfaceRevisionId,
type RelationshipInterfaceMember,
} from "../capability-model/types.js";
import {
aggregateType,
aggregateOperators,
queryKeyType,
type QueryRow,
type AggregateOperator,
} from "./relational.js";
import { QueryCompileError } from "./types.js";
export const rowKey = (row: QueryRow): string => JSON.stringify(row);
export const objectRow = (
interfaceRevisionId: InterfaceRevisionId,
membership?: { keyType?: "string" | "boolean" | "int64" },
): QueryRow => ({ kind: "object", interfaceRevisionId, ...(membership ? { membership } : {}) });
export type SchemaRowField = { kind: "leaf"; type: ValueType } | { kind: "row"; row: QueryRow; optional: boolean };
export interface RelationalSchemaEnvironment {
contracts: Map<InterfaceRevisionId, InterfaceRevision>;
target(member: RelationshipInterfaceMember): InterfaceRevisionId;
name(id: InterfaceRevisionId): string;
scalar(type: ValueType): GraphQLInputType & GraphQLOutputType;
comparison(type: ValueType): GraphQLInputObjectType;
scalarTypes: Map<string, ValueType>;
direction: GraphQLEnumType;
cursor: GraphQLScalarType;
pageInfo: GraphQLObjectType;
}
export function relationalSchema(env: RelationalSchemaEnvironment, document?: DocumentNode) {
const outputCache = new Map<string, GraphQLObjectType>();
const inputCache = new Map<string, GraphQLInputObjectType>();
const referenceCache = new Map<string, GraphQLScalarType>();
const rowsets = new Map<string, QueryRow>();
const expansionPaths = new Map<string, Map<string, string[]>>();
const roles = new Map<
string,
{ kind: "rowset" | "aggregate" | "groups" | "keys" | "expand" | "relations"; row: QueryRow }
>();
const many = (member: RelationshipInterfaceMember) =>
member.cardinality === "many" || member.cardinality === "many-unique";
const relationRow = (member: RelationshipInterfaceMember) =>
objectRow(env.target(member), { keyType: member.keyType });
const relations = (row: QueryRow) =>
row.kind === "object"
? env.contracts
.get(row.interfaceRevisionId)!
.members.filter(
(member): member is RelationshipInterfaceMember => member.kind === "relationship" && !!member.queryRead,
)
: [];
const fields = (row: QueryRow): Map<string, SchemaRowField> => {
if (row.kind === "pair")
return new Map([
["source", { kind: "row", row: row.source, optional: false }],
["target", { kind: "row", row: row.target, optional: false }],
]);
return new Map(
env.contracts.get(row.interfaceRevisionId)!.members.flatMap((member): [string, SchemaRowField][] => {
if (member.kind === "operation" || !member.queryRead) return [];
if (member.kind === "value") return [[member.displayName, { kind: "leaf", type: member.valueType }]];
if (many(member)) return [];
return [
[
member.displayName,
{ kind: "row", row: objectRow(env.target(member)), optional: member.cardinality === "optional-one" },
],
];
}),
);
};
const refs = (id: InterfaceRevisionId) => {
let result = referenceCache.get(id);
if (!result) {
result = new GraphQLScalarType({
name: `QxRef_${env.name(id)}`,
parseValue: (value) => value,
parseLiteral() {
throw new Error("Managed references must be supplied as typed variables");
},
});
referenceCache.set(id, result);
env.scalarTypes.set(result.name, valueType.interfaceRef(id));
}
return result;
};
const scalar = (type: ValueType): GraphQLInputType & GraphQLOutputType => {
if (type.kind === "optional") return scalar(type.value);
return type.kind === "object-ref" && type.expectation.kind === "interface"
? refs(type.expectation.interfaceRevisionId)
: env.scalar(type);
};
const leafFields = (row: QueryRow) => {
const result = new Map<string, ValueType>();
if (row.kind === "object") result.set("ref", valueType.interfaceRef(row.interfaceRevisionId));
if (row.kind === "pair" || row.membership) result.set("entry", valueType.string);
if (row.kind === "object" && row.membership?.keyType)
result.set(
"mapKey",
row.membership.keyType === "boolean"
? valueType.bool
: row.membership.keyType === "int64"
? valueType.int64
: valueType.string,
);
return result;
};
const typeName = (role: string, row: QueryRow, suffix = "") =>
`Qx${role}_${createHash("sha256")
.update(rowKey(row) + suffix)
.digest("hex")
.slice(0, 16)}`;
const out = (role: string, row: QueryRow, make: () => GraphQLFieldConfigMap<unknown, unknown>, suffix = "") => {
const key = typeName(role, row, suffix);
let result = outputCache.get(key);
if (!result) {
result = new GraphQLObjectType({ name: key, fields: make });
outputCache.set(key, result);
}
return result;
};
const inp = (role: string, row: QueryRow, make: () => GraphQLInputFieldConfigMap, suffix = "") => {
const key = typeName(role, row, suffix);
let result = inputCache.get(key);
if (!result) {
result = new GraphQLInputObjectType({ name: key, fields: make });
inputCache.set(key, result);
}
return result;
};
const compare = (type: ValueType) => {
const base = type.kind === "optional" ? type.value : type;
if (base.kind !== "object-ref") return env.comparison(type);
return inp(
"RefCompare",
objectRow(
base.expectation.kind === "interface"
? base.expectation.interfaceRevisionId
: (() => {
throw new Error("Expected interface reference");
})(),
),
() => ({ eq: { type: scalar(type) }, in: { type: new GraphQLList(new GraphQLNonNull(scalar(type))) } }),
);
};
// Recursive interface graphs are interned. Synthetic pair graphs are finite and document-driven.
const columns = (
row: QueryRow,
operator?: Exclude<AggregateOperator, "count">,
nullable = false,
): GraphQLObjectType =>
out(
"Columns",
row,
() => {
const result: GraphQLFieldConfigMap<unknown, unknown> = {};
for (const [name, field] of fields(row)) {
if (field.kind === "row")
result[name] = { type: new GraphQLNonNull(columns(field.row, operator, nullable || field.optional)) };
else {
let type = field.type;
if (operator) {
try {
type = aggregateType(operator, type);
} catch {
continue;
}
} else if (!queryKeyType(type)) continue;
result[name] = {
type:
type.kind === "optional" || (!operator && nullable) ? scalar(type) : new GraphQLNonNull(scalar(type)),
};
}
}
const metadata: GraphQLFieldConfigMap<unknown, unknown> = {};
for (const [name, type] of leafFields(row)) {
if (operator && operator !== "countPresent") continue;
const t = scalar(operator ? aggregateType(operator, type) : type);
metadata[name] = { type: !operator && nullable ? t : new GraphQLNonNull(t) };
}
if (Object.keys(metadata).length)
result._qx = { type: new GraphQLNonNull(out("ColumnsMeta", row, () => metadata, `${operator}:${nullable}`)) };
// A type may have no matching scalar at this node; a private schema sentinel
// keeps it valid while compiler validation forbids selecting that sentinel.
if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean };
return result;
},
`${operator}:${nullable}`,
);
const aggregate = (row: QueryRow): GraphQLObjectType => {
const result = out("Aggregate", row, () =>
Object.fromEntries(
aggregateOperators.map((op) => [
op,
{ type: new GraphQLNonNull(op === "count" ? env.scalar(valueType.uint64) : columns(row, op)) },
]),
),
);
roles.set(result.name, { kind: "aggregate", row });
return result;
};
const keys = (row: QueryRow): GraphQLInputObjectType =>
inp("Keys", row, () => {
const result: GraphQLInputFieldConfigMap = {};
for (const [name, field] of fields(row)) {
if (field.kind === "row") result[name] = { type: keys(field.row) };
else if (queryKeyType(field.type)) result[name] = { type: GraphQLBoolean };
}
const meta = Object.fromEntries([...leafFields(row)].map(([name]) => [name, { type: GraphQLBoolean }]));
if (Object.keys(meta).length) result._qx = { type: inp("KeyMeta", row, () => meta) };
return result;
});
const comparisons = (
row: QueryRow,
operator?: Exclude<AggregateOperator, "count">,
ordering = false,
): GraphQLInputObjectType =>
inp(
"Expressions",
row,
() => {
const result: GraphQLInputFieldConfigMap = {};
for (const [name, field] of fields(row)) {
if (field.kind === "row") result[name] = { type: comparisons(field.row, operator, ordering) };
else {
let type = field.type;
if (operator) {
try {
type = aggregateType(operator, type);
} catch {
continue;
}
}
result[name] = { type: ordering ? env.direction : compare(type) };
}
}
const meta: GraphQLInputFieldConfigMap = {};
for (const [name, type] of leafFields(row))
if (!operator || operator === "countPresent")
meta[name] = { type: ordering ? env.direction : compare(operator ? aggregateType(operator, type) : type) };
if (Object.keys(meta).length)
result._qx = { type: inp("ExpressionMeta", row, () => meta, `${operator}:${ordering}`) };
if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean };
return result;
},
`${operator}:${ordering}`,
);
const having = (row: QueryRow, ordering = false): GraphQLInputObjectType =>
inp(
"Having",
row,
() => {
const result: GraphQLInputFieldConfigMap = { group: { type: comparisons(row, undefined, ordering) } };
for (const op of aggregateOperators)
result[op] = {
type:
op === "count" ? (ordering ? env.direction : compare(valueType.uint64)) : comparisons(row, op, ordering),
};
if (!ordering)
Object.assign(result, {
and: { type: new GraphQLList(new GraphQLNonNull(having(row))) },
or: { type: new GraphQLList(new GraphQLNonNull(having(row))) },
not: { type: having(row) },
});
return result;
},
String(ordering),
);
const where = (row: QueryRow): GraphQLInputObjectType =>
inp("Where", row, () => {
const result: GraphQLInputFieldConfigMap = {
and: { type: new GraphQLList(new GraphQLNonNull(where(row))) },
or: { type: new GraphQLList(new GraphQLNonNull(where(row))) },
not: { type: where(row) },
};
for (const [name, field] of fields(row)) {
if (row.kind === "object" && field.kind === "row") continue;
if (name in result) throw new QueryCompileError("QUERY_SCHEMA_NAME", `Reserved predicate name ${name}`);
result[name] = { type: field.kind === "row" ? where(field.row) : compare(field.type) };
}
result._qx = {
type: inp("WhereMeta", row, () => {
const meta: GraphQLInputFieldConfigMap = {};
for (const [name, type] of leafFields(row)) meta[name] = { type: compare(type) };
const edges = relations(row);
if (edges.length)
meta.relations = {
type: inp("WhereRelations", row, () =>
Object.fromEntries(
edges.map((member) => {
const target = relationRow(member);
return [
member.displayName,
{
type: inp(
"RelationPredicate",
target,
(): GraphQLInputFieldConfigMap =>
many(member)
? {
some: { type: where(target) },
none: { type: where(target) },
aggregate: {
type: inp("ReductionPredicate", target, () => ({
where: { type: where(target) },
having: { type: new GraphQLNonNull(having(target)) },
})),
},
}
: { is: { type: where(target) }, isNull: { type: GraphQLBoolean } },
rowKey(row) + member.id,
),
},
];
}),
),
),
};
return meta;
}),
};
return result;
});
const fragments = new Map(
(document?.definitions ?? [])
.filter((d): d is FragmentDefinitionNode => d.kind === "FragmentDefinition")
.map((d) => [d.name.value, d]),
);
let visited = 0;
const selections = (set: SelectionSetNode, active: ReadonlySet<string> = new Set()): import("graphql").FieldNode[] =>
set.selections.flatMap((node) => {
if (++visited > 10000)
throw new QueryCompileError("QUERY_WORK_LIMIT", "Query schema discovery exceeds 10000 nodes");
if (node.kind === "Field") return [node];
if (node.kind === "FragmentSpread") {
if (active.has(node.name.value) || active.size >= 128)
throw new QueryCompileError("QUERY_VALIDATION", "Cyclic or excessively nested fragments");
const fragment = fragments.get(node.name.value);
return fragment ? selections(fragment.selectionSet, new Set([...active, node.name.value])) : [];
}
return [];
});
const discoverExpansion = (root: QueryRow, current: QueryRow, set: SelectionSetNode, path: string[] = []) => {
for (const node of selections(set)) {
if (!node.selectionSet) continue;
const member = relations(current).find((m) => m.displayName === node.name.value);
const next = [...path, node.name.value];
if (member && many(member)) {
const map = expansionPaths.get(rowKey(root)) ?? new Map();
map.set(next.join("."), next);
expansionPaths.set(rowKey(root), map);
discoverRows({ kind: "pair", source: root, target: relationRow(member) }, node.selectionSet);
} else {
const field = fields(current).get(node.name.value);
if (field?.kind === "row") discoverExpansion(root, field.row, node.selectionSet, next);
}
}
};
const discoverRows = (row: QueryRow, set: SelectionSetNode) => {
rowsets.set(rowKey(row), row);
if (rowsets.size > 256) throw new QueryCompileError("QUERY_WORK_LIMIT", "Query exceeds 256 row shapes");
for (const node of selections(set))
if (node.selectionSet) {
if (node.name.value === "expand") discoverExpansion(row, row, node.selectionSet);
else if (node.name.value === "filter" || node.name.value === "distinct") discoverRows(row, node.selectionSet);
}
};
const discoverObject = (row: QueryRow, set: SelectionSetNode) => {
for (const node of selections(set))
if (node.selectionSet) {
if (node.name.value === "_qx") {
for (const meta of selections(node.selectionSet))
if (meta.name.value === "relations" && meta.selectionSet)
for (const edge of selections(meta.selectionSet)) {
const member = relations(row).find((m) => m.displayName === edge.name.value);
if (member && many(member) && edge.selectionSet) discoverRows(relationRow(member), edge.selectionSet);
}
} else {
const member = relations(row).find((m) => m.displayName === node.name.value);
if (member) {
if (!many(member)) discoverObject(objectRow(env.target(member)), node.selectionSet);
else
for (const entries of selections(node.selectionSet))
if (entries.name.value === "entries" && entries.selectionSet)
for (const child of selections(entries.selectionSet))
if (child.name.value === "node" && child.selectionSet)
discoverObject(objectRow(env.target(member)), child.selectionSet);
}
}
}
};
const discover = (root: InterfaceRevisionId) => {
for (const operation of document?.definitions ?? [])
if (operation.kind === "OperationDefinition")
for (const node of selections(operation.selectionSet))
if (node.name.value === "root" && node.selectionSet) discoverObject(objectRow(root), node.selectionSet);
};
const expand = (root: QueryRow, current: QueryRow, path: string[] = []): GraphQLObjectType =>
out(
"Expand",
current,
() => {
const result: GraphQLFieldConfigMap<unknown, unknown> = {};
const paths = [...(expansionPaths.get(rowKey(root))?.values() ?? [])].filter((p) =>
path.every((part, i) => p[i] === part),
);
for (const name of new Set(paths.map((p) => p[path.length]).filter((v): v is string => !!v))) {
const member = relations(current).find((m) => m.displayName === name);
if (member && many(member))
result[name] = {
type: new GraphQLNonNull(rowset({ kind: "pair", source: root, target: relationRow(member) })),
};
else {
const field = fields(current).get(name);
if (field?.kind === "row")
result[name] = { type: new GraphQLNonNull(expand(root, field.row, [...path, name])) };
}
}
if (!Object.keys(result).length) result._unavailable = { type: GraphQLBoolean };
return result;
},
rowKey(root) + JSON.stringify(path),
);
const rowset = (row: QueryRow): GraphQLObjectType => {
const result = out("Rows", row, () => {
const entry = out("GroupEntry", row, () => ({
key: { type: new GraphQLNonNull(GraphQLString) },
cursor: { type: env.cursor },
group: { type: new GraphQLNonNull(columns(row)) },
aggregate: { type: new GraphQLNonNull(aggregate(row)) },
}));
const connection = out("Groups", row, () => ({
entries: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(entry))) },
pageInfo: { type: new GraphQLNonNull(env.pageInfo) },
}));
roles.set(connection.name, { kind: "groups", row });
return {
filter: { type: new GraphQLNonNull(rowset(row)), args: { where: { type: new GraphQLNonNull(where(row)) } } },
distinct: { type: new GraphQLNonNull(rowset(row)), args: { by: { type: new GraphQLNonNull(keys(row)) } } },
expand: { type: new GraphQLNonNull(expand(row, row)) },
aggregate: { type: new GraphQLNonNull(aggregate(row)), args: { where: { type: where(row) } } },
groups: {
type: new GraphQLNonNull(connection),
args: {
by: { type: new GraphQLNonNull(keys(row)) },
where: { type: where(row) },
having: { type: having(row) },
orderBy: { type: new GraphQLList(new GraphQLNonNull(having(row, true))) },
first: { type: GraphQLInt },
all: { type: GraphQLInt },
after: { type: env.cursor },
},
},
};
});
roles.set(result.name, { kind: "rowset", row });
return result;
};
const helpers = (id: InterfaceRevisionId): GraphQLObjectType | undefined => {
const row = objectRow(id),
edges = relations(row).filter(many);
if (!edges.length) return undefined;
const result = out("Relations", row, () =>
Object.fromEntries(
edges.map((member) => [member.displayName, { type: new GraphQLNonNull(rowset(relationRow(member))) }]),
),
);
roles.set(result.name, { kind: "relations", row });
return result;
};
return { refs, fields, relations, relationRow, where, rowset, helpers, roles, discover, scalar, compare };
}