Skip to content
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1f944c9
spec(ast): add kind-discriminant migration spec for TML-2096
wmadden Mar 25, 2026
947625c
plan(ast): add execution plan for kind-discriminant migration
wmadden Mar 25, 2026
6e7a1a4
add kind discriminant tags to all AST node classes
wmadden Mar 25, 2026
5dab01e
migrate AST internals and guards from instanceof to kind dispatch
wmadden Mar 25, 2026
bd01359
migrate postgres adapter from instanceof to kind dispatch
wmadden Mar 25, 2026
e86fc3d
migrate sql-runtime lints from instanceof to kind dispatch
wmadden Mar 25, 2026
9c4b253
migrate ORM client from instanceof to kind dispatch
wmadden Mar 25, 2026
2018719
migrate Kysely lane from instanceof to kind dispatch
wmadden Mar 25, 2026
4020602
migrate sql-lane from instanceof to kind dispatch
wmadden Mar 25, 2026
a2505ae
migrate test assertions from instanceof to kind-based dispatch
wmadden Mar 25, 2026
1775c86
migrate budgets plugin from instanceof to kind dispatch
wmadden Mar 25, 2026
1378d55
narrow kind on intermediate abstract classes for compile-time safety
wmadden Mar 25, 2026
51d1596
chore: reformat plan markdown tables and task lists
wmadden Mar 25, 2026
6f89648
remove dead branch in JoinAst.rewrite
wmadden Mar 25, 2026
ee6869a
migrate all toBeInstanceOf assertions to kind-based checks
wmadden Mar 25, 2026
f664f15
remove redundant kind assertions that test compile-time constants
wmadden Mar 25, 2026
a166dcf
export canonical kind sets from relational-core
wmadden Mar 25, 2026
e7d3be1
export isQueryAst and isWhereExpr guards from relational-core
wmadden Mar 25, 2026
f88d415
fix typecheck errors from abstract class kind narrowing
wmadden Mar 25, 2026
ea84c2f
strengthen test assertions: use kind discriminants instead of toBeDef…
wmadden Mar 26, 2026
ceda6cf
prevent shorthand filters from being misclassified as AST nodes
wmadden Mar 26, 2026
fcb3ae7
fix(budgets): address review findings — explain sig, concurrency, dea…
wmadden Mar 26, 2026
ed06563
fix(budgets): latency shouldBlock uses mode only, not severity
wmadden Mar 26, 2026
c957135
make all kind-dispatch switches exhaustive with never checks
wmadden Mar 26, 2026
7aedb4b
replace abstract class types with discriminated unions in AST
wmadden Mar 27, 2026
8ee4f5a
propagate union types to sql-lane, kysely-lane, and sql-runtime
wmadden Mar 27, 2026
248e7a8
propagate union types to postgres adapter
wmadden Mar 27, 2026
3c91436
propagate union types to sql-orm-client
wmadden Mar 27, 2026
e130fa4
add spec and plan for AST union-typed fields
wmadden Mar 27, 2026
a9d18ea
add explanatory comment on Expression.toExpr() double cast
wmadden Mar 27, 2026
c1cb7e9
address code review feedback on union-typed fields
wmadden Mar 27, 2026
bb7a447
$(cat <<'EOF'
wmadden Mar 27, 2026
8b7ae37
refactor: remove some redundant type assertions
aqrln Mar 27, 2026
7e71428
refactor: simplify exhaustiveness checks
aqrln Mar 27, 2026
6e210df
refactor: type safe conversions from base classes to unions
aqrln Mar 27, 2026
f008607
style: fix formatting issues
aqrln Mar 27, 2026
9fd5b10
refactor: simplify more exhaustiveness checks
aqrln Mar 27, 2026
d736dbd
refactor: remove extractRefsFromAst function
aqrln Mar 27, 2026
a46c196
refactor: simplify all exhaustiveness checks
aqrln Mar 27, 2026
6678a48
style: remove unused import
aqrln Mar 27, 2026
95ae31b
refactor: remove unnecessary SelectAst casts
aqrln Mar 27, 2026
2dcb590
refactor: remove unreachable check
aqrln Mar 27, 2026
994d39d
refactor: remove more obsolete type assertions
aqrln Mar 27, 2026
059521a
fix: resolve naming collision with a duck-typed ToWhereExpr interface
aqrln Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/scripts/worktree-create.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env node

import { execSync } from 'node:child_process';
import { text } from 'node:stream/consumers';
import { resolve } from 'node:path';
import { text } from 'node:stream/consumers';

const input = JSON.parse(await text(process.stdin));

Expand Down
6 changes: 3 additions & 3 deletions packages/2-sql/4-lanes/kysely-lane/src/plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
import {
type BinaryExpr,
ParamRef,
SelectAst,
type SelectAst,
TableSource,
} from '@prisma-next/sql-relational-core/ast';
import type { CompiledQuery } from 'kysely';
Expand Down Expand Up @@ -99,7 +99,7 @@ describe('buildKyselyPlan', () => {
it('assembles plan metadata with stable refs and limit annotations', () => {
const plan = buildKyselyPlan(contract, createSelectCompiledQuery());

expect(plan.ast).toBeInstanceOf(SelectAst);
expect(plan.ast.kind).toBe('select');
const ast = plan.ast as SelectAst;
expect(ast.from).toEqual(TableSource.named('user'));
expect((ast.where as BinaryExpr).left).toEqual(ast.projection[0]!.expr);
Expand Down Expand Up @@ -275,7 +275,7 @@ describe('buildKyselyPlan', () => {
} as unknown as CompiledQuery<unknown>;

const plan = buildKyselyPlan(contract, query);
expect(plan.ast).toBeInstanceOf(SelectAst);
expect(plan.ast.kind).toBe('select');
const ast = plan.ast as SelectAst;
expect(ast.joins).toHaveLength(1);
expect(ast.joins?.[0]?.joinType).toBe('left');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { ParamDescriptor, PlanRefs } from '@prisma-next/contract/types';
import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
import type { QueryAst } from '@prisma-next/sql-relational-core/ast';
import type { AnyQueryAst } from '@prisma-next/sql-relational-core/ast';

export interface TransformResult {
readonly ast: QueryAst;
readonly ast: AnyQueryAst;
readonly metaAdditions: {
readonly refs: PlanRefs;
readonly paramDescriptors: ReadonlyArray<ParamDescriptor>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
InsertAst,
InsertOnConflict,
type InsertValue,
ParamRef,
type ParamRef,
UpdateAst,
} from '@prisma-next/sql-relational-core/ast';
import {
Expand Down Expand Up @@ -37,7 +37,7 @@ import { expandSelectAll } from './transform-select';
import { resolveColumnRef, transformTableRef, validateColumn } from './transform-validate';

function assertParamRef(value: ReturnType<typeof transformValue>): ParamRef {
if (!(value instanceof ParamRef)) {
if (value.kind !== 'param-ref') {
throw new KyselyTransformError(
'Only parameterized VALUES are supported in Kysely transform lane',
KYSELY_TRANSFORM_ERROR_CODES.UNSUPPORTED_NODE,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { ParamDescriptor } from '@prisma-next/contract/types';
import type { BinaryOp, JoinOnExpr, WhereExpr } from '@prisma-next/sql-relational-core/ast';
import type { AnyWhereExpr, BinaryOp, JoinOnExpr } from '@prisma-next/sql-relational-core/ast';
import {
AndExpr,
BinaryExpr,
ColumnRef,
type ColumnRef,
EqColJoinOn,
ListLiteralExpr,
LiteralExpr,
Expand Down Expand Up @@ -113,7 +113,7 @@ function flattenLogical(
logicalKind: 'and' | 'or',
ctx: TransformContext,
defaultTable: string | undefined,
out: WhereExpr[],
out: AnyWhereExpr[],
): void {
const current = ParensNode.is(node) ? node.node : node;
if (logicalKind === 'and' && AndNode.is(current)) {
Expand Down Expand Up @@ -159,7 +159,7 @@ export function transformWhereExpr(
node: unknown,
ctx: TransformContext,
defaultTable?: string,
): WhereExpr | undefined {
): AnyWhereExpr | undefined {
if (!node) {
return undefined;
}
Expand All @@ -173,15 +173,15 @@ export function transformWhereExpr(
}

if (AndNode.is(node)) {
const exprs: WhereExpr[] = [];
const exprs: AnyWhereExpr[] = [];
flattenLogical(node, 'and', ctx, defaultTable, exprs);
if (exprs.length === 0) return undefined;
if (exprs.length === 1) return exprs[0];
return AndExpr.of(exprs);
}

if (OrNode.is(node)) {
const exprs: WhereExpr[] = [];
const exprs: AnyWhereExpr[] = [];
flattenLogical(node, 'or', ctx, defaultTable, exprs);
if (exprs.length === 0) return undefined;
if (exprs.length === 1) return exprs[0];
Expand Down Expand Up @@ -252,10 +252,10 @@ export function transformJoinOn(
}

if (
expr instanceof BinaryExpr &&
expr.kind === 'binary' &&
expr.op === 'eq' &&
expr.left instanceof ColumnRef &&
expr.right instanceof ColumnRef
expr.left.kind === 'column-ref' &&
expr.right.kind === 'column-ref'
) {
return EqColJoinOn.of(expr.left, expr.right);
}
Expand Down
61 changes: 29 additions & 32 deletions packages/2-sql/4-lanes/kysely-lane/src/transform/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
* - Ambiguous selectAll in multi-table scope → AMBIGUOUS_SELECT_ALL
* - Unsupported node kinds → UNSUPPORTED_NODE
*/
import type { PlanRefs } from '@prisma-next/contract/types';
import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
import { ColumnRef, type QueryAst, SelectAst } from '@prisma-next/sql-relational-core/ast';
import type { AnyQueryAst } from '@prisma-next/sql-relational-core/ast';
import { ifDefined } from '@prisma-next/utils/defined';
import { DeleteQueryNode, InsertQueryNode, SelectQueryNode, UpdateQueryNode } from 'kysely';
import { KYSELY_TRANSFORM_ERROR_CODES, KyselyTransformError } from './errors';
Expand All @@ -21,10 +20,6 @@ import { transformSelect } from './transform-select';

export type { TransformResult };

function extractRefsFromAst(ast: QueryAst): PlanRefs {
return ast.collectRefs();
}

export interface TransformResultWithParams extends TransformResult {
readonly params: readonly unknown[];
}
Expand All @@ -49,7 +44,7 @@ export function transformKyselyToPnAst(

const ctx = createContext(contract, parameters);

let ast: QueryAst;
let ast: AnyQueryAst;
if (SelectQueryNode.is(query)) {
ast = transformSelect(query, ctx);
} else if (InsertQueryNode.is(query)) {
Expand All @@ -68,7 +63,7 @@ export function transformKyselyToPnAst(
);
}

const refs = extractRefsFromAst(ast);
const refs = ast.collectRefs();

const paramDescriptors = ctx.paramDescriptors.map((descriptor, index) => ({
...descriptor,
Expand All @@ -77,19 +72,20 @@ export function transformKyselyToPnAst(

let projection: Record<string, string> | undefined;
let projectionTypes: Record<string, string> | undefined;
if (ast instanceof SelectAst) {
const select = ast.kind === 'select' ? ast : undefined;
if (select) {
projection = Object.fromEntries(
ast.projection.map((projected) => [
projected.alias,
projected.expr instanceof ColumnRef ? projected.expr.column : projected.alias,
]),
select.projection.map((projected) => {
const col = projected.expr.kind === 'column-ref' ? projected.expr : undefined;
return [projected.alias, col?.column ?? projected.alias];
}),
);

projectionTypes = {};
for (const projected of ast.projection) {
if (projected.expr instanceof ColumnRef) {
const column =
ctx.contract.storage.tables[projected.expr.table]?.columns[projected.expr.column];
for (const projected of select.projection) {
const col = projected.expr.kind === 'column-ref' ? projected.expr : undefined;
if (col) {
const column = ctx.contract.storage.tables[col.table]?.columns[col.column];
if (column) {
projectionTypes[projected.alias] = column.codecId;
}
Expand All @@ -105,8 +101,8 @@ export function transformKyselyToPnAst(
'projectionTypes',
projectionTypes && Object.keys(projectionTypes).length > 0 ? projectionTypes : undefined,
),
...ifDefined('selectAllIntent', ast instanceof SelectAst ? ast.selectAllIntent : undefined),
...ifDefined('limit', ast instanceof SelectAst ? ast.limit : undefined),
...ifDefined('selectAllIntent', select?.selectAllIntent),
...ifDefined('limit', select?.limit),
};

return { ast, metaAdditions };
Expand All @@ -131,7 +127,7 @@ export function transformKyselyToPnAstCollectingParams(

const ctx = createContext(contract);

let ast: QueryAst;
let ast: AnyQueryAst;
if (SelectQueryNode.is(query)) {
ast = transformSelect(query, ctx);
} else if (InsertQueryNode.is(query)) {
Expand All @@ -150,7 +146,7 @@ export function transformKyselyToPnAstCollectingParams(
);
}

const refs = extractRefsFromAst(ast);
const refs = ast.collectRefs();

const paramDescriptors = ctx.paramDescriptors.map((descriptor, index) => ({
...descriptor,
Expand All @@ -159,19 +155,20 @@ export function transformKyselyToPnAstCollectingParams(

let projection: Record<string, string> | undefined;
let projectionTypes: Record<string, string> | undefined;
if (ast instanceof SelectAst) {
const select = ast.kind === 'select' ? ast : undefined;
if (select) {
projection = Object.fromEntries(
ast.projection.map((projected) => [
projected.alias,
projected.expr instanceof ColumnRef ? projected.expr.column : projected.alias,
]),
select.projection.map((projected) => {
const col = projected.expr.kind === 'column-ref' ? projected.expr : undefined;
return [projected.alias, col?.column ?? projected.alias];
}),
);

projectionTypes = {};
for (const projected of ast.projection) {
if (projected.expr instanceof ColumnRef) {
const column =
ctx.contract.storage.tables[projected.expr.table]?.columns[projected.expr.column];
for (const projected of select.projection) {
const col = projected.expr.kind === 'column-ref' ? projected.expr : undefined;
if (col) {
const column = ctx.contract.storage.tables[col.table]?.columns[col.column];
if (column) {
projectionTypes[projected.alias] = column.codecId;
}
Expand All @@ -187,8 +184,8 @@ export function transformKyselyToPnAstCollectingParams(
'projectionTypes',
projectionTypes && Object.keys(projectionTypes).length > 0 ? projectionTypes : undefined,
),
...ifDefined('selectAllIntent', ast instanceof SelectAst ? ast.selectAllIntent : undefined),
...ifDefined('limit', ast instanceof SelectAst ? ast.limit : undefined),
...ifDefined('selectAllIntent', select?.selectAllIntent),
...ifDefined('limit', select?.limit),
};

return { ast, params: ctx.params, metaAdditions };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,13 @@ describe('buildKyselyWhereExpr nested AST traversal', () => {
expect(bound.expr).toBeInstanceOf(ExistsExpr);
const exists = bound.expr as ExistsExpr;
const subquery = exists.subquery;
expect(subquery.from).toBeInstanceOf(DerivedTableSource);
expect(subquery.from.kind).toEqual('derived-table-source');
expect(((subquery.from as DerivedTableSource).query.where as BinaryExpr).right).toEqual(
ParamRef.of(3, 'kind'),
);

const join = subquery.joins?.[0];
expect(join?.source).toBeInstanceOf(DerivedTableSource);
expect(join?.source.kind).toEqual('derived-table-source');
expect((join?.on as BinaryExpr).right).toEqual(ParamRef.of(1, 'userId'));
expect(((join?.source as DerivedTableSource).query.where as BinaryExpr).right).toEqual(
ParamRef.of(4, 'title'),
Expand Down
6 changes: 2 additions & 4 deletions packages/2-sql/4-lanes/kysely-lane/src/where-expr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
import {
type BoundWhereExpr,
ListLiteralExpr,
ParamRef,
SelectAst,
type ToWhereExpr,
} from '@prisma-next/sql-relational-core/ast';
import type { CompiledQuery } from 'kysely';
Expand All @@ -29,7 +27,7 @@ export function buildKyselyWhereExpr<Row>(
options: BuildKyselyPlanOptions = {},
): ToWhereExpr {
const plan = buildKyselyPlan(contract, compiledQuery, options);
if (!(plan.ast instanceof SelectAst) || !plan.ast.where) {
if (plan.ast.kind !== 'select' || !plan.ast.where) {
throw new Error('whereExpr(...) requires a select query with a where clause');
}

Expand Down Expand Up @@ -97,7 +95,7 @@ function remapParamIndexes(
listLiteral: (list) =>
new ListLiteralExpr(
list.values.map((value) => {
if (!(value instanceof ParamRef)) {
if (value.kind !== 'param-ref') {
return value;
}
const newIndex = remap.get(value.index);
Expand Down
4 changes: 2 additions & 2 deletions packages/2-sql/4-lanes/relational-core/src/ast/join.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { planInvalid } from '@prisma-next/plan';
import type { AnyColumnBuilder, JoinOnBuilder, JoinOnPredicate } from '../types';
import { isColumnBuilder } from '../types';
import { EqColJoinOn, type FromSource, JoinAst } from './types';
import { EqColJoinOn, JoinAst } from './types';

class JoinOnBuilderImpl implements JoinOnBuilder {
eqCol(left: AnyColumnBuilder, right: AnyColumnBuilder): JoinOnPredicate {
Expand All @@ -27,7 +27,7 @@ class JoinOnBuilderImpl implements JoinOnBuilder {
}
}

export { EqColJoinOn, JoinAst, type FromSource };
export { EqColJoinOn, JoinAst };

export function createJoinOnBuilder(): JoinOnBuilder {
return new JoinOnBuilderImpl();
Expand Down
Loading
Loading