111 lines
4.9 KiB
TypeScript
111 lines
4.9 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { compileAggregation as compile, compileAggregationDocument } from "./fixtures/query-aggregation.js";
|
|
import type { QuerySelection } from "../src/query/types.js";
|
|
import type { QueryRelationalPlan } from "../src/query/relational.js";
|
|
|
|
function findPlan(selections: QuerySelection[]): QueryRelationalPlan | undefined {
|
|
for (const selection of selections) {
|
|
const plan = selection.relational ?? findPlan(selection.selection);
|
|
if (plan) return plan;
|
|
}
|
|
}
|
|
|
|
test("aggregate output uses exact count and sum types and nullable reductions", async () => {
|
|
const checked = await compile(
|
|
`aggregate { count countPresent { estimatedHours } sum { estimatedHours cost } avg { cost } }`,
|
|
);
|
|
const json = JSON.stringify(checked.output);
|
|
assert.match(json, /"count":\{"kind":"scalar","name":"uint64"\}/);
|
|
assert.match(json, /"cost":\{"kind":"optional","value":\{"kind":"scalar","name":"int64"\}\}/);
|
|
assert.ok(checked.effects.some((e) => e.memberId === "hours" && e.uses.includes("aggregate")));
|
|
const plan = findPlan(checked.selection);
|
|
assert.ok(plan);
|
|
assert.equal(plan.terminals[0]!.reductions.length, 5);
|
|
});
|
|
|
|
test("explicit expansion and identity distinct retain checked grouped operands", async () => {
|
|
const checked = await compile(`expand { tags {
|
|
distinct(by: {source: {_qx: {ref: true}}, target: {color: true}}) {
|
|
groups(by: {target: {color: true}}, first: 50,
|
|
having: {sum: {source: {estimatedHours: {gt: 0}}}},
|
|
orderBy: [{sum: {source: {estimatedHours: DESC}}}]) {
|
|
entries { group {target {color}} aggregate {sum {source {estimatedHours}}} }
|
|
pageInfo {hasNextPage endCursor}
|
|
}
|
|
}
|
|
} }`);
|
|
const plan = findPlan(checked.selection)!;
|
|
assert.deepEqual(
|
|
plan.stages.map((s) => s.operation.kind),
|
|
["source", "expand", "distinct"],
|
|
);
|
|
assert.equal(plan.terminals[0]!.kind, "groups");
|
|
assert.ok(
|
|
checked.effects.some(
|
|
(e) => e.memberId === "hours" && ["aggregate", "predicate", "order"].every((u) => e.uses.includes(u as never)),
|
|
),
|
|
);
|
|
});
|
|
|
|
test("distinct and grouping reject fields not determined by their selected keys", async () => {
|
|
await assert.rejects(compile(`distinct(by: {cost: true}) {aggregate {sum {estimatedHours}}}`), /dropped by distinct/);
|
|
await assert.rejects(compile(`groups(by: {cost: true}, first: 10) {entries {group {estimatedHours}}}`), /group key/);
|
|
});
|
|
|
|
test("relationship existence predicates compile to typed plans", async () => {
|
|
const checked = await compile(
|
|
`filter(where: {_qx: {relations: {tags: {some: {color: {in: ["red","blue"]}}}}}}) {aggregate {count}}`,
|
|
);
|
|
const plan = findPlan(checked.selection)!;
|
|
assert.equal(plan.stages[1]!.operation.kind, "filter");
|
|
assert.ok(checked.effects.some((e) => e.memberId === "color" && e.uses.includes("predicate")));
|
|
});
|
|
|
|
test("group structure is literal, bounded and cannot project a representative field", async () => {
|
|
await assert.rejects(compile(`groups(by: {}, first: 2) {entries {aggregate {count}}}`), /key|empty/i);
|
|
await assert.rejects(compile(`groups(by: {cost: false}, first: 2) {entries {aggregate {count}}}`), /true/);
|
|
await assert.rejects(compile(`groups(by: {cost: true}) {entries {aggregate {count}}}`), /first or all/);
|
|
await assert.rejects(
|
|
compile(
|
|
`groups(by: {cost: true}, first: 2, orderBy: [{sum: {cost: ASC, estimatedHours: DESC}}]) {entries {aggregate {count}}}`,
|
|
),
|
|
/exactly one field/,
|
|
);
|
|
await assert.rejects(compile(`expand {tags {aggregate {sum {target {color}}}}}`), /QUERY_VALIDATION/);
|
|
});
|
|
|
|
test("RPC continuation rejection is local to the affected membership boundary", async () => {
|
|
await assert.rejects(
|
|
compile(`groups(by: {cost: true}, first: 2, after: null) {entries {aggregate {sum {score}}}}`),
|
|
/without continuation/,
|
|
);
|
|
await assert.rejects(
|
|
compileAggregationDocument(
|
|
`query Report {root {tasks(first: 2, after: null, where: {score: {gt: 0}}) {entries {node {cost}}}}}`,
|
|
),
|
|
/without continuation/,
|
|
);
|
|
const checked = await compileAggregationDocument(`query Report($after: Cursor) {root {
|
|
tasks(first: 2, after: $after) {entries {node {cost}}}
|
|
_qx {relations {tasks {aggregate {sum {score}}}}}
|
|
}}`);
|
|
assert.equal(findPlan(checked.selection)!.terminals[0]!.residual, true);
|
|
});
|
|
|
|
test("typed reference operands and scalar membership tests reject unbounded literal lists", async () => {
|
|
const checked = await compile(
|
|
`filter(where: {_qx: {ref: {in: $tasks}}}) {aggregate {count}}`,
|
|
"($tasks: [QxRef_Task!]!)",
|
|
);
|
|
assert.match(JSON.stringify(checked.variables), /object-ref/);
|
|
await assert.rejects(
|
|
compile(`filter(where: {_qx: {ref: {eq: "obj:made-up"}}}) {aggregate {count}}`),
|
|
/typed variables/,
|
|
);
|
|
await assert.rejects(
|
|
compile(`filter(where: {cost: {in: [${Array(1001).fill("1").join(",")}]}}) {aggregate {count}}`),
|
|
/1000 operands/,
|
|
);
|
|
});
|