chore: add add_plan operation in migrations

This commit is contained in:
Charlie Lamb
2026-05-13 12:26:04 +01:00
parent 6fc9af674e
commit f0684dd048
17 changed files with 822 additions and 27 deletions

View File

@@ -1,10 +1,15 @@
import type { CustomerFilter } from "../../../filters/customerFilter.js";
import type { PlanFilter } from "../../../filters/planFilter.js";
import type { IRNav } from "../../ir/irTypes.js";
import type { IRNav, IRNode, Quantifier } from "../../ir/irTypes.js";
import { isQuantifierWrapper } from "../helpers/isQuantifierWrapper.js";
import type { ResolutionContext } from "../resolutionContext.js";
import { parsePlanFilter } from "../scopes/parsePlanFilter.js";
const QUANTIFIER_KEYS: Record<string, Quantifier> = {
$some: "some",
$none: "none",
};
export function parsePlanNav({
raw,
ctx,
@@ -12,13 +17,30 @@ export function parsePlanNav({
raw: NonNullable<CustomerFilter["plan"]>;
ctx: ResolutionContext;
}): IRNav {
// Phase 1: only $some (implicit if bare). $every / $none deferred.
const planFilter = isQuantifierWrapper(raw) ? raw.$some : (raw as PlanFilter);
if (!planFilter) throw new Error("plan: only $some is supported in phase 1");
return {
kind: "nav",
name: "plan",
quantifier: "some",
child: parsePlanFilter({ filter: planFilter, ctx }),
};
if (!isQuantifierWrapper(raw))
return buildNav({ quantifier: "some", filter: raw as PlanFilter, ctx });
for (const [key, quantifier] of Object.entries(QUANTIFIER_KEYS)) {
const filter = (raw as Record<string, unknown>)[key] as PlanFilter | undefined;
if (filter !== undefined) return buildNav({ quantifier, filter, ctx });
}
const unsupported = Object.keys(raw).find((k) => k.startsWith("$"));
throw new Error(`plan: ${unsupported ?? "unknown quantifier"} is not supported yet`);
}
function buildNav({
quantifier,
filter,
ctx,
}: {
quantifier: Quantifier;
filter: PlanFilter;
ctx: ResolutionContext;
}): IRNav {
const hasFields = Object.keys(filter).length > 0;
const child: IRNode = hasFields
? parsePlanFilter({ filter, ctx })
: { kind: "and", children: [] };
return { kind: "nav", name: "plan", quantifier, child };
}

View File

@@ -38,12 +38,13 @@ export type IROr = {
children: readonly IRNode[];
};
export type Quantifier = "some" | "none";
export type IRNav = {
kind: "nav";
/** Single segment naming the nav, e.g. "plan" or "item". */
name: string;
/** Phase 1: only "some" is supported. */
quantifier: "some";
quantifier: Quantifier;
child: IRNode;
};

View File

@@ -1,4 +1,4 @@
import type { IRLeaf, IRNode } from "../ir/irTypes.js";
import type { IRLeaf, IRNode, Quantifier } from "../ir/irTypes.js";
import type {
AmbientPredicate,
FieldDef,
@@ -112,6 +112,7 @@ function compileNode({
return existsForScope({
scope: def.scope,
child: node.child,
quantifier: node.quantifier,
params,
ambient,
});
@@ -120,11 +121,13 @@ function compileNode({
function existsForScope({
scope,
child,
quantifier,
params,
ambient,
}: {
scope: NavScope;
child: IRNode;
quantifier: Quantifier;
params: unknown[];
ambient: AmbientContext;
}): string {
@@ -139,10 +142,12 @@ function existsForScope({
params,
ambient,
});
const conditions = [scope.correlation, ...ambientPreds, childSql].join(
" AND ",
const conditions = [scope.correlation, ...ambientPreds, childSql].filter(
(s) => s.length > 0 && s !== "TRUE",
);
return `EXISTS (SELECT 1 FROM ${scope.from} WHERE ${conditions})`;
const whereClause = conditions.length > 0 ? conditions.join(" AND ") : "TRUE";
const keyword = quantifier === "none" ? "NOT EXISTS" : "EXISTS";
return `${keyword} (SELECT 1 FROM ${scope.from} WHERE ${whereClause})`;
}
function compileLeaf({