// Query plans: a random query as plain data. // // The generator never touches the builder. It produces a `Plan` — which source, // which conditions, what to select — that `build.ts` turns into builder calls // and `model.ts` evaluates in JS. Keeping the plan as data is what makes a // failure readable: fast-check shrinks the plan, and the report prints it. // // Expressions are generated by *sort* (number, string, nullable string, time) // so a plan is well-typed by construction, and the builder refusing one is a // finding rather than noise. import fc from "fast-check" import type { FuzzDialect } from "./dialects/types" import { nastyString, wellFormed } from "./values" // The fixture (tests/core-cases.ts): five orders, four customers, two orgs. export const ORDER_COLUMNS = { OrgId: "str", Id: "num", Customer: "str", Amount: "num", Status: "str", Note: "nstr", Created: "time", } as const export const CUSTOMER_COLUMNS = { OrgId: "str", Name: "str", Tier: "str" } as const export type OrderColumn = keyof typeof ORDER_COLUMNS export type CustomerColumn = keyof typeof CUSTOMER_COLUMNS export type Sort = "num" | "str" | "nstr" | "time" /** Values the fixture's string columns hold, so equality and LIKE sometimes match. */ const FIXTURE_STRINGS = ["org_1", "org_2", "acme", "globex", "initech", "umbrella", "paid", "open", "void", "rush", "it's", "gold", "silver", "bronze", "platinum"] const T0 = Date.UTC(2026, 0, 1) /** Around the fixture's `Created` range (00:00–00:40), at millisecond precision. */ const time = fc.oneof( fc.integer({ min: 0, max: 4 }).map((i) => T0 + i * 600_000), fc.integer({ min: T0 - 3_600_000, max: T0 + 6_000_000 }), ) // Expressions export type ArithOp = "add" | "sub" | "mul" | "div" | "mod" export type Expr = | { readonly k: "col"; readonly col: OrderColumn } | { readonly k: "jcol"; readonly col: CustomerColumn } | { readonly k: "num"; readonly v: number } | { readonly k: "str"; readonly v: string } | { readonly k: "arith"; readonly op: ArithOp; readonly l: Expr; readonly r: Expr | number } | { readonly k: "coalesce"; readonly e: Expr; readonly fallback: Expr } /** A comparison's right-hand side: a plain value, a param, or another expression. */ export type Operand = | { readonly k: "value"; readonly v: number | string } /** A time, in the three forms a DateTime comparison accepts. */ | { readonly k: "time"; readonly ms: number; readonly form: "DateTime" | "Date" | "string" } | { readonly k: "param"; readonly kind: "int" | "string" | "dateTime"; readonly v: number | string } | { readonly k: "expr"; readonly e: Expr } export type CmpOp = "eq" | "neq" | "gt" | "gte" | "lt" | "lte" export type Cond = | { readonly k: "cmp"; readonly op: CmpOp; readonly l: Expr; readonly r: Operand } | { readonly k: "like"; readonly op: "like" | "notLike" | "ilike"; readonly l: Expr; readonly pattern: string } | { readonly k: "null"; readonly op: "isNull" | "isNotNull"; readonly e: Expr } | { readonly k: "between"; readonly neg: boolean; readonly e: Expr; readonly lo: Operand; readonly hi: Operand } | { readonly k: "in"; readonly neg: boolean; readonly e: Expr; readonly values: ReadonlyArray } | { readonly k: "and" | "or"; readonly style: "method" | "fn"; readonly cs: ReadonlyArray } | { readonly k: "not"; readonly c: Cond } | { readonly k: "const"; readonly v: boolean } /** `$.OrgId.eq(param.string("orgId"))`: the tenant predicate, wherever it lands. */ | { readonly k: "tenant"; readonly side: "orders" | "customers" } export type Agg = | { readonly k: "count" } | { readonly k: "countIf"; readonly c: Cond } | { readonly k: "sum" | "min" | "max"; readonly e: Expr } export interface RowsShape { readonly k: "rows" readonly select: ReadonlyArray readonly distinct: boolean readonly orderBy: ReadonlyArray readonly limit?: number readonly offset?: number } export interface AggShape { readonly k: "agg" readonly keys: ReadonlyArray readonly aggs: ReadonlyArray readonly having: ReadonlyArray<{ readonly agg: Agg; readonly op: CmpOp; readonly v: number }> readonly orderBy: ReadonlyArray readonly limit?: number readonly offset?: number } export interface Plan { /** The value `param.string("orgId")` is given. */ readonly orgId: string readonly join?: "inner" | "left" readonly where: ReadonlyArray readonly shape: RowsShape | AggShape /** The same rows through a subquery: `fromQuery(query, "w").select()`, or `.select(...names)`. */ readonly wrap: false | "all" | "names" readonly crossTenant: boolean } // Generators export interface GenOptions { /** Strings for comparisons and LIKE patterns: anything, or only what sorts the same everywhere. */ readonly strings: fc.Arbitrary /** Whether the dialect has ILIKE (`FuzzDialect.ilike`). */ readonly ilike: boolean /** Maximum recursive expression/condition depth; smoke plans default to two. */ readonly depth?: number } /** Mostly the fixture's own numbers (Id 1–5, Amount 5/7/10/20/99), so comparisons split the rows. */ const smallInt = fc.oneof( { weight: 2, arbitrary: fc.constantFrom(0, 1, 2, 3, 4, 5, 6, 7, 10, 20, 99) }, { weight: 1, arbitrary: fc.integer({ min: -20, max: 120 }) }, ) const divisor = fc.oneof(fc.integer({ min: 1, max: 9 }), fc.integer({ min: -9, max: -1 })) const expr = (sort: Sort, joined: boolean, depth: number, options: GenOptions): fc.Arbitrary => { const cols = (Object.keys(ORDER_COLUMNS) as OrderColumn[]).filter((c) => ORDER_COLUMNS[c] === sort) const jcols = joined ? (Object.keys(CUSTOMER_COLUMNS) as CustomerColumn[]).filter((c) => CUSTOMER_COLUMNS[c] === sort) : [] const leaves: Array> = [ ...(cols.length > 0 ? [fc.constantFrom(...cols).map((col): Expr => ({ k: "col", col }))] : []), ...(jcols.length > 0 ? [fc.constantFrom(...jcols).map((col): Expr => ({ k: "jcol", col }))] : []), ] if (sort === "num") { leaves.push(smallInt.map((v): Expr => ({ k: "num", v }))) if (depth > 0) { const sub = expr("num", joined, depth - 1, options) leaves.push( fc.record({ k: fc.constant("arith" as const), op: fc.constantFrom("add", "sub", "mul", "div", "mod"), l: sub, r: fc.oneof(divisor, sub), }), ) } } if (sort === "str") { leaves.push(fc.oneof(fc.constantFrom(...FIXTURE_STRINGS), options.strings).map((v): Expr => ({ k: "str", v }))) if (depth > 0) { leaves.push( fc.record({ k: fc.constant("coalesce" as const), e: expr("nstr", joined, depth - 1, options), fallback: expr("str", joined, 0, options), }), ) } } return fc.oneof(...leaves) } const valueOf = (sort: Sort, options: GenOptions): fc.Arbitrary => { switch (sort) { case "num": return fc.oneof( { weight: 3, arbitrary: smallInt.map((v): Operand => ({ k: "value", v })) }, { weight: 3, arbitrary: smallInt.map((v): Operand => ({ k: "param", kind: "int", v })) }, // A fraction against an integer column: `Amount > 9.5`. { weight: 1, arbitrary: fc.integer({ min: -40, max: 240 }).map((n): Operand => ({ k: "value", v: n / 2 + 0.25 })) }, ) case "str": case "nstr": return fc.oneof( fc.oneof(fc.constantFrom(...FIXTURE_STRINGS), options.strings).map((v): Operand => ({ k: "value", v })), fc.oneof(fc.constantFrom(...FIXTURE_STRINGS), options.strings).map((v): Operand => ({ k: "param", kind: "string", v })), ) case "time": return fc.oneof( fc.record({ k: fc.constant("time" as const), ms: time, form: fc.constantFrom("DateTime" as const, "Date" as const, "string" as const) }), time.map((ms): Operand => ({ k: "param", kind: "dateTime", v: ms })), ) } } /** A comparable right-hand side for `sort`: mostly values, sometimes another expression of the sort. */ const operand = (sort: Sort, joined: boolean, options: GenOptions): fc.Arbitrary => sort === "time" ? valueOf(sort, options) : fc.oneof( { weight: 3, arbitrary: valueOf(sort, options) }, { weight: 1, arbitrary: expr(sort === "nstr" ? "str" : sort, joined, 1, options).map((e): Operand => ({ k: "expr", e })) }, ) /** LIKE patterns over the fixture's letters; `\` only ever escapes `%`, `_` or `\`, which every database must agree on. */ const soupPattern = fc .array(fc.constantFrom("%", "_", "\\%", "\\_", "\\\\", "a", "c", "e", "m", "i", "o", "p", "AC", "G", "'", "s"), { maxLength: 5 }) .map((parts) => parts.join("")) /** * A fixture value, edited letter by letter: re-cased, or replaced by `_` or `%`; * a literal `_` or `%` in the value is escaped. Unlike letter soup, these * patterns select rows, so case and escape rules change the result where * dialects differ. A pattern that matches nothing tests nothing. */ const fixturePattern = fc .tuple(fc.constantFrom(...FIXTURE_STRINGS), fc.array(fc.constantFrom("keep", "keep", "keep", "upper", "under", "pct"), { minLength: 1, maxLength: 9 })) .map(([value, edits]) => [...value] .map((ch, i) => { const edit = edits[i % edits.length] if (edit === "under") return "_" if (edit === "pct") return "%" const kept = edit === "upper" ? ch.toUpperCase() : ch return kept === "_" || kept === "%" || kept === "\\" ? `\\${kept}` : kept }) .join(""), ) const likePattern = fc.oneof(soupPattern, fixturePattern) const cond = (joined: boolean, depth: number, options: GenOptions): fc.Arbitrary => { const sorts: ReadonlyArray = ["num", "str", "nstr", "time"] const leaf = fc.oneof( fc.constantFrom(...sorts).chain((sort) => fc.record({ k: fc.constant("cmp" as const), op: fc.constantFrom("eq", "neq", "gt", "gte", "lt", "lte"), l: expr(sort, joined, 1, options), r: operand(sort, joined, options), }), ), fc.constantFrom("str", "nstr").chain((sort) => fc.record({ k: fc.constant("like" as const), op: fc.constantFrom("like" as const, "notLike" as const, ...(options.ilike ? (["ilike"] as const) : [])), l: expr(sort, joined, 0, options), pattern: likePattern, }), ), fc.record({ k: fc.constant("null" as const), op: fc.constantFrom("isNull" as const, "isNotNull" as const), e: fc.oneof(expr("nstr", joined, 0, options), expr("str", joined, 0, options)), }), fc.constantFrom("num", "str", "time").chain((sort) => fc.record({ k: fc.constant("between" as const), neg: fc.boolean(), e: expr(sort, joined, 1, options), lo: valueOf(sort, options), hi: valueOf(sort, options) }), ), fc.constantFrom("num", "str", "nstr").chain((sort) => fc.record({ k: fc.constant("in" as const), neg: fc.boolean(), e: expr(sort, joined, 0, options), values: fc.array( sort === "num" ? smallInt : fc.oneof(fc.constantFrom(...FIXTURE_STRINGS), options.strings), { maxLength: 4 }, ), }), ), fc.boolean().map((v): Cond => ({ k: "const", v })), fc.constantFrom({ k: "tenant", side: "orders" }, ...(joined ? [{ k: "tenant", side: "customers" } as const] : [])), ) if (depth === 0) return leaf const sub = cond(joined, depth - 1, options) return fc.oneof( { weight: 4, arbitrary: leaf }, { weight: 2, arbitrary: fc.record({ k: fc.constantFrom("and" as const, "or" as const), style: fc.constantFrom("method" as const, "fn" as const), cs: fc.array(sub, { minLength: 2, maxLength: 3 }), }), }, { weight: 1, arbitrary: sub.map((c): Cond => ({ k: "not", c })) }, ) } const agg = (joined: boolean, options: GenOptions): fc.Arbitrary => fc.oneof( fc.constant({ k: "count" }), cond(joined, 1, options).map((c): Agg => ({ k: "countIf", c })), fc.record({ k: fc.constantFrom("sum" as const, "min" as const, "max" as const), e: expr("num", joined, 1, options) }), ) const aliasesOf = (n: number, prefix: string) => Array.from({ length: n }, (_, i) => `${prefix}${i}`) /** An ORDER BY over `aliases`, completed with `unique` so LIMIT/OFFSET pick a determined set. */ const ordering = (aliases: ReadonlyArray, unique: ReadonlyArray) => fc .record({ keys: fc.subarray([...aliases]).chain((keys) => fc.tuple(...keys.map((key) => fc.constant(key).chain((k) => fc.constantFrom("asc" as const, "desc" as const).map((d) => [k, d] as const))))), limit: fc.option(fc.oneof({ weight: 4, arbitrary: fc.integer({ min: 1, max: 6 }) }, { weight: 1, arbitrary: fc.constant(0) }), { nil: undefined }), offset: fc.option(fc.integer({ min: 0, max: 3 }), { nil: undefined, freq: 3 }), total: fc.boolean(), }) .map(({ keys, limit, offset, total }) => { const paging = limit !== undefined || offset !== undefined const complete = paging || total const tail = complete ? unique.filter((u) => !keys.some(([k]) => k === u)).map((u) => [u, "asc"] as const) : [] return { orderBy: [...keys, ...tail], limit, offset } }) const rowsShape = (joined: boolean, options: GenOptions): fc.Arbitrary => fc .array(fc.constantFrom("num", "str", "nstr", "time").chain((sort) => expr(sort, joined, options.depth ?? 2, options)), { minLength: 0, maxLength: 4 }) .chain((exprs) => { // `id` identifies orders; joined rows also order by every selected alias to break duplicate-key ties. const select: Array = [["id", { k: "col", col: "Id" }], ...exprs.map((e, i) => [`e${i}`, e] as const)] return fc.record({ distinct: fc.boolean(), order: ordering(select.map(([a]) => a), joined ? select.map(([a]) => a) : ["id"]) }).map( ({ distinct, order }): RowsShape => ({ k: "rows", select, distinct, ...order }), ) }) const keyExpr = (joined: boolean): fc.Arbitrary => fc.constantFrom( { k: "col", col: "Customer" }, { k: "col", col: "Status" }, { k: "col", col: "OrgId" }, { k: "col", col: "Note" }, ...(joined ? [{ k: "jcol", col: "Tier" } as const, { k: "jcol", col: "Name" } as const] : []), ) const aggShape = (joined: boolean, options: GenOptions): fc.Arbitrary => fc .record({ keys: fc.uniqueArray(keyExpr(joined), { maxLength: 2, selector: (e) => JSON.stringify(e) }), aggs: fc.array(agg(joined, options), { minLength: 1, maxLength: 3 }), }) .chain(({ keys, aggs }) => { const keyAliases = aliasesOf(keys.length, "k") const aggAliases = aliasesOf(aggs.length, "a") return fc .record({ having: keys.length === 0 ? fc.constant([]) : fc.array(fc.record({ agg: agg(joined, options), op: fc.constantFrom("eq", "neq", "gt", "gte", "lt", "lte"), v: smallInt }), { maxLength: 2 }), // A global aggregate is one row; only grouped results page. order: keys.length === 0 ? fc.constant({ orderBy: [], limit: undefined, offset: undefined }) : ordering([...keyAliases, ...aggAliases], keyAliases), }) .map(({ having, order }): AggShape => ({ k: "agg", keys: keys.map((e, i) => [keyAliases[i]!, e] as const), aggs: aggs.map((a, i) => [aggAliases[i]!, a] as const), having, ...order, })) }) const orgIdArb = fc.oneof({ weight: 6, arbitrary: fc.constant("org_1") }, { weight: 2, arbitrary: fc.constant("org_2") }, { weight: 1, arbitrary: fc.constant("org_3") }) const joinArb = fc.option(fc.constantFrom("inner" as const, "left" as const), { nil: undefined, freq: 3 }) // Short: every entry is ANDed, and a long conjunction of random predicates is almost always empty. const whereArb = (joined: boolean, options: GenOptions): fc.Arbitrary> => fc.oneof( fc.constant([]), fc.array(fc.oneof({ weight: 1, arbitrary: fc.constant({ k: "tenant", side: "orders" }) }, { weight: 3, arbitrary: cond(joined, options.depth ?? 2, options) }), { minLength: 1, maxLength: 2 }), ) /** * `unionAll(branch, …)` with the union's own ORDER BY, LIMIT and OFFSET. Each * branch is a row plan selecting the same aliases, each alias of one sort in * every branch, so the union is well-typed by construction; a branch may have * its own join, filter, DISTINCT, ORDER BY and LIMIT. */ export interface UnionPlan { readonly orgId: string readonly branches: ReadonlyArray readonly orderBy: ReadonlyArray readonly limit?: number readonly offset?: number readonly crossTenant: boolean } const branch = (orgId: string, sorts: ReadonlyArray, options: GenOptions): fc.Arbitrary => joinArb.chain((join) => { const joined = join !== undefined return fc.tuple(...sorts.map((sort) => expr(sort, joined, options.depth ?? 2, options))).chain((exprs) => { const select: Array = [["id", { k: "col", col: "Id" }], ...exprs.map((e, i) => [`e${i}`, e] as const)] return fc.record({ where: whereArb(joined, options), distinct: fc.boolean(), order: ordering(select.map(([a]) => a), joined ? select.map(([a]) => a) : ["id"]) }).map( ({ where, distinct, order }): Plan => ({ orgId, join, where, shape: { k: "rows", select, distinct, ...order }, wrap: false, crossTenant: false }), ) }) }) /** * The union's ordering. Rows of different branches can tie on every alias but * one, so it pages only when ordered by every alias: then tied rows are equal * rows, and which of them LIMIT keeps does not matter. */ const unionOrdering = (aliases: ReadonlyArray) => fc.oneof( fc.constant({ orderBy: [] as ReadonlyArray, limit: undefined, offset: undefined }), fc .record({ keys: fc.shuffledSubarray([...aliases], { minLength: aliases.length }), dirs: fc.array(fc.constantFrom("asc" as const, "desc" as const), { minLength: aliases.length, maxLength: aliases.length }), limit: fc.option(fc.integer({ min: 0, max: 8 }), { nil: undefined }), offset: fc.option(fc.integer({ min: 0, max: 4 }), { nil: undefined }), }) .map(({ keys, dirs, limit, offset }) => ({ orderBy: keys.map((k, i) => [k, dirs[i]!] as const), limit, offset })), fc.shuffledSubarray([...aliases], { minLength: 1 }).map((keys) => ({ orderBy: keys.map((k) => [k, "asc" as const] as const), limit: undefined, offset: undefined })), ) export const unionPlan = (options: GenOptions): fc.Arbitrary => fc .record({ orgId: orgIdArb, sorts: fc.array(fc.constantFrom("num", "str", "nstr", "time"), { maxLength: 3 }), branches: fc.integer({ min: 2, max: 3 }), crossTenant: fc.boolean(), }) .chain(({ orgId, sorts, branches, crossTenant }) => fc .record({ branches: fc.tuple(...Array.from({ length: branches }, () => branch(orgId, sorts, options))), order: unionOrdering(["id", ...sorts.map((_, i) => `e${i}`)]), }) .map(({ branches, order }): UnionPlan => ({ orgId, branches, crossTenant, ...order })), ) export const plan = (options: GenOptions): fc.Arbitrary => joinArb.chain((join) => { const joined = join !== undefined return fc.record({ orgId: orgIdArb, join: fc.constant(join), where: whereArb(joined, options), shape: fc.oneof({ weight: 3, arbitrary: rowsShape(joined, options) }, { weight: 2, arbitrary: aggShape(joined, options) }), wrap: fc.constantFrom(false as const, "all" as const, "names" as const), crossTenant: fc.boolean(), }) }) /** Every string the oracle compares by byte order: plain, so collation never enters. */ export const plainOptions = (dialect: FuzzDialect): GenOptions => ({ strings: fc.string({ unit: fc.constantFrom(..."abcdeghiklmnoprstuwxy_' -"), maxLength: 8 }), ilike: dialect.ilike, }) /** Strings from the nasty generator: for escaping, where the model only needs equality. */ export const nastyOptions = (dialect: FuzzDialect): GenOptions => ({ strings: nastyString(dialect.strings).filter((s) => wellFormed(s)), ilike: dialect.ilike, })