Skip to content

Commit 9345ca5

Browse files
wmaddenaqrln
andauthored
refactor(sql-ast): replace instanceof with kind discriminants (#253)
closes [TML-2096](https://linear.app/prisma-company/issue/TML-2096/avoid-instanceof-in-sql-query-ast-methods) ## Before / After ```ts // BEFORE — dispatch on class identity (breaks with duplicate packages) if (ast instanceof SelectAst) { ... } else if (ast instanceof InsertAst) { ... } // fields typed as abstract classes — consumers must cast to narrow const expr: Expression = binary.left; const node = expr as AnyExpression; switch (node.kind) { ... } ``` ```ts // AFTER — dispatch on structural kind tag (works across module boundaries) switch (ast.kind) { case 'select': ... case 'insert': ... default: { const _: never = ast; throw new Error(...); } } // fields typed as unions — consumers narrow directly const expr: AnyExpression = binary.left; switch (expr.kind) { ... } // no cast needed ``` ## Intent Replace all `instanceof`-based dispatch on SQL query AST node classes with structural `kind` discriminant tags, then make discriminated union types the public API surface for all polymorphic AST references. Abstract base classes become internal implementation details. `instanceof` checks fail silently when duplicate copies of a package exist in `node_modules` (a common pnpm workspace scenario). Structural `kind` dispatch eliminates this failure mode and enables TypeScript exhaustive `switch` checking at every dispatch site. Using union types for fields and parameters means `switch (node.kind)` narrows without casting. ## Change map ### Phase 1: Kind discriminants - **Implementation (AST definitions)**: - [relational-core/src/ast/types.ts](packages/2-sql/4-lanes/relational-core/src/ast/types.ts) — `kind` tags on 28 concrete classes, narrowed `kind` unions on 5 abstract classes, 5 discriminated union types - [relational-core/src/utils/guards.ts](packages/2-sql/4-lanes/relational-core/src/utils/guards.ts) — simplified `getColumnInfo` - **Implementation (dispatch migration, 13 files across 6 packages)**: - [postgres/src/core/adapter.ts](packages/3-targets/6-adapters/postgres/src/core/adapter.ts) — 39 checks - [sql-runtime/src/plugins/lints.ts](packages/2-sql/5-runtime/src/plugins/lints.ts) — 6 checks - [sql-runtime/src/plugins/budgets.ts](packages/2-sql/5-runtime/src/plugins/budgets.ts) — 2 checks - [sql-orm-client/src/where-binding.ts](packages/3-extensions/sql-orm-client/src/where-binding.ts), [collection.ts](packages/3-extensions/sql-orm-client/src/collection.ts), [query-plan-aggregate.ts](packages/3-extensions/sql-orm-client/src/query-plan-aggregate.ts), [query-plan-meta.ts](packages/3-extensions/sql-orm-client/src/query-plan-meta.ts) — 19 checks - [kysely-lane/src/transform/](packages/2-sql/4-lanes/kysely-lane/src/transform/) (3 files), [where-expr.ts](packages/2-sql/4-lanes/kysely-lane/src/where-expr.ts) — 17 checks - [sql-lane/src/sql/predicate-builder.ts](packages/2-sql/4-lanes/sql-lane/src/sql/predicate-builder.ts) — 5 checks - **Tests (evidence)**: - [kind-discriminants.test.ts](packages/2-sql/4-lanes/relational-core/test/ast/kind-discriminants.test.ts) — foundation tests: all 28 tags, uniqueness, cross-module structural dispatch - 31 test files migrated from `toBeInstanceOf` to `.kind` assertions ### Phase 2: Union-typed fields - **Implementation (AST core)**: - [relational-core/src/ast/types.ts](packages/2-sql/4-lanes/relational-core/src/ast/types.ts) — un-export 6 abstract classes, update ~15 field types + constructors + method return types to union types, update `ExpressionRewriter`/`AstRewriter` interfaces, remove `SqlComparable` alias, add `AnySqlComparable`/`AnyOperationArg` union types, make `rewriteComparable`/`foldComparable` exhaustive - [relational-core/src/plan.ts](packages/2-sql/4-lanes/relational-core/src/plan.ts), [src/types.ts](packages/2-sql/4-lanes/relational-core/src/types.ts), [src/ast/join.ts](packages/2-sql/4-lanes/relational-core/src/ast/join.ts), [src/operations-registry.ts](packages/2-sql/4-lanes/relational-core/src/operations-registry.ts) — relational-core internal consumers - **Implementation (propagation, 24 files across 5 packages)**: - [kysely-lane/src/transform/](packages/2-sql/4-lanes/kysely-lane/src/transform/) — `WhereExpr` → `AnyWhereExpr`, `ColumnRef` casts removed - [sql-lane/src/sql/](packages/2-sql/4-lanes/sql-lane/src/sql/) — `Expression` → `AnyExpression`, `WhereExpr` → `AnyWhereExpr`, `SqlComparable` → `AnySqlComparable` - [sql-runtime/src/](packages/2-sql/5-runtime/src/) — `QueryAst` → `AnyQueryAst`, `FromSource` → `AnyFromSource`, exhaustive switches added - [sql-orm-client/src/](packages/3-extensions/sql-orm-client/src/) (~12 files) — all abstract class imports → union types - [postgres/src/](packages/3-targets/6-adapters/postgres/src/) — `QueryAst` → `AnyQueryAst`, `Expression` → `AnyExpression`, `FromSource` → `AnyFromSource`, `WhereExpr` → `AnyWhereExpr`, casts removed, error messages aligned to `kind`-based pattern - **Tests (evidence)**: - [adapter.test.ts](packages/3-targets/6-adapters/postgres/test/adapter.test.ts) — `class UnsupportedAst extends QueryAst` replaced with plain-object cast (abstract class no longer importable) - [test-helpers.ts](packages/2-sql/4-lanes/relational-core/test/ast/test-helpers.ts), [rich-ast.test.ts](packages/2-sql/4-lanes/relational-core/test/ast/rich-ast.test.ts) — `Expression | ParamRef | LiteralExpr` → `AnyOperationArg` - 5 ORM client test files updated from `WhereExpr` to `AnyWhereExpr` type annotations ## The story ### Phase 1: Kind discriminants 1. **Add structural `kind` tags to every AST node.** Each of the 28 concrete AST classes gets `readonly kind = '<tag>' as const`. The root `AstNode` declares `abstract readonly kind: string`, and the 5 intermediate abstract classes (`QueryAst`, `FromSource`, `Expression`, `WhereExpr`, `InsertOnConflictAction`) narrow it to their valid tag unions — so adding a new subclass with the wrong `kind` is a compile error. 2. **Export discriminated union types.** `AnyQueryAst`, `AnyFromSource`, `AnyExpression`, `AnyWhereExpr`, and `AnyInsertOnConflictAction` enable `switch (node.kind)` with exhaustive `never` defaults in dispatch sites that need full type narrowing. 3. **Migrate all ~112 production `instanceof` checks to `kind`-based dispatch.** Multi-branch chains become exhaustive `switch` statements. Single-branch guards become `if (node.kind === 'tag')`. Classes imported solely for `instanceof` are converted to type-only imports, reducing runtime coupling. 4. **Migrate all ~117 test `toBeInstanceOf` assertions.** Test assertions now validate the `kind` discriminant rather than class identity, ensuring tests exercise the structural dispatch mechanism. 5. **Clean up follow-on sites.** The `expressionKinds` Set in `predicate-builder.ts` (redundant now that `Expression.kind` is type-narrowed) is removed. The `whereExprKinds` Set in `collection.ts` is typed against `WhereExpr['kind']`. A dead branch in `JoinAst.rewrite` (predating this migration) is removed. ### Phase 2: Union-typed fields 6. **Replace abstract class types with discriminated unions everywhere.** All ~15 field declarations, constructor parameters, composite type aliases (`ProjectionExpr`, `JoinOnExpr`, `WhereArg`), `ExpressionRewriter`/`AstRewriter` interface return types, and abstract method return types (`rewrite()`, `not()`, `toExpr()`) are updated to reference union types instead of abstract base classes. `SqlComparable` is removed in favor of `AnySqlComparable`. 7. **Propagate union types to all downstream packages.** 24 files across `kysely-lane`, `sql-lane`, `sql-runtime`, `sql-orm-client`, and `postgres` are updated. All imports of abstract base classes for type annotations are replaced with union type imports. Casts that existed solely for `kind`-narrowing (e.g., `const node = ast as AnyQueryAst`) are removed. 8. **Un-export all 6 abstract base classes.** `AstNode`, `QueryAst`, `FromSource`, `Expression`, `WhereExpr`, and `InsertOnConflictAction` become module-private. They remain in the file for method inheritance but are no longer part of the public API surface. ## Behavior changes & evidence - **Every concrete AST class exposes a `readonly kind` discriminant with a unique kebab-case literal type.** Dispatch sites can structurally identify nodes without relying on class identity. - **Why**: `instanceof` fails silently when multiple copies of a package coexist in `node_modules`. Structural `kind` tags work regardless of module resolution. - **Implementation**: [types.ts](packages/2-sql/4-lanes/relational-core/src/ast/types.ts) - **Tests**: [kind-discriminants.test.ts](packages/2-sql/4-lanes/relational-core/test/ast/kind-discriminants.test.ts) — tests all 28 tags, uniqueness, and cross-module plain-object dispatch - **Intermediate abstract classes narrow `kind` to their valid tag union.** Adding a new `Expression` subclass with `kind = 'invalid'` is a compile error. - **Why**: Prevents silent dispatch gaps when the class hierarchy grows. - **Implementation**: [types.ts](packages/2-sql/4-lanes/relational-core/src/ast/types.ts) - **All production dispatch uses `kind`-based switching.** Zero `instanceof` on AST classes remain in production code. - **Implementation**: 13 source files across 6 packages (see change map) - **Tests**: Full test suite passes - **All polymorphic AST fields, parameters, and return types use discriminated union types.** `switch (node.kind)` narrows directly without casting. - **Why**: Abstract class types could not be narrowed by `kind` checks — consumers had to cast to the union first. Union types eliminate this friction. - **Implementation**: [types.ts](packages/2-sql/4-lanes/relational-core/src/ast/types.ts) + 24 downstream files - **Abstract base classes are no longer exported.** They serve only as internal implementation details for method inheritance and immutability (`freeze()`). - **Why**: With union types as the public API, there is no reason for consumers to reference the abstract classes. - **Implementation**: [types.ts](packages/2-sql/4-lanes/relational-core/src/ast/types.ts) - **Tests**: [adapter.test.ts](packages/3-targets/6-adapters/postgres/test/adapter.test.ts) — `UnsupportedAst` class replaced with plain-object cast - **No runtime behavior change**: the class hierarchy is preserved. Both phases are pure dispatch-mechanism and type-system migrations. ## Compatibility / migration / risk - **No breaking API changes for in-repo consumers.** The `kind` property is additive; fields gain more precise types (narrower unions instead of broad abstract class). - **External consumers that subclass abstract AST classes will break.** Since the abstract classes are no longer exported, code outside this repo that extends `QueryAst`, `Expression`, etc. will fail to compile. This is intentional. - **`toEqual` assertions against plain objects break** if the class now has a `kind` property that the plain object lacks. All affected tests were updated to use AST constructors (e.g. `ProjectionItem.of(...)` instead of `{ alias, expr }`). ## Follow-ups / open questions - ~90+ `toBeInstanceOf` calls on non-AST classes (e.g. `Collection`, `PostCollection`) remain untouched — those are outside the scope of this migration. ## Non-goals / intentionally out of scope - Removing the class hierarchy itself (classes are preserved for method inheritance, immutability via `freeze()`, and factory methods). - Adding generics (CRTP / F-bounded polymorphism) to the abstract classes (evaluated and rejected). - Migrating `instanceof` on non-AST types (e.g. `Error`, `Date`, `Collection`). --------- Co-authored-by: Alexey Orlenko <alex@aqrln.net>
1 parent 9fd4f0c commit 9345ca5

85 files changed

Lines changed: 1798 additions & 978 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/scripts/worktree-create.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/usr/bin/env node
22

33
import { execSync } from 'node:child_process';
4-
import { text } from 'node:stream/consumers';
54
import { resolve } from 'node:path';
5+
import { text } from 'node:stream/consumers';
66

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

packages/2-sql/4-lanes/kysely-lane/src/plan.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
22
import {
33
type BinaryExpr,
44
ParamRef,
5-
SelectAst,
5+
type SelectAst,
66
TableSource,
77
} from '@prisma-next/sql-relational-core/ast';
88
import type { CompiledQuery } from 'kysely';
@@ -99,7 +99,7 @@ describe('buildKyselyPlan', () => {
9999
it('assembles plan metadata with stable refs and limit annotations', () => {
100100
const plan = buildKyselyPlan(contract, createSelectCompiledQuery());
101101

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

277277
const plan = buildKyselyPlan(contract, query);
278-
expect(plan.ast).toBeInstanceOf(SelectAst);
278+
expect(plan.ast.kind).toBe('select');
279279
const ast = plan.ast as SelectAst;
280280
expect(ast.joins).toHaveLength(1);
281281
expect(ast.joins?.[0]?.joinType).toBe('left');

packages/2-sql/4-lanes/kysely-lane/src/transform/transform-context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { ParamDescriptor, PlanRefs } from '@prisma-next/contract/types';
22
import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
3-
import type { QueryAst } from '@prisma-next/sql-relational-core/ast';
3+
import type { AnyQueryAst } from '@prisma-next/sql-relational-core/ast';
44

55
export interface TransformResult {
6-
readonly ast: QueryAst;
6+
readonly ast: AnyQueryAst;
77
readonly metaAdditions: {
88
readonly refs: PlanRefs;
99
readonly paramDescriptors: ReadonlyArray<ParamDescriptor>;

packages/2-sql/4-lanes/kysely-lane/src/transform/transform-dml.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
InsertAst,
66
InsertOnConflict,
77
type InsertValue,
8-
ParamRef,
8+
type ParamRef,
99
UpdateAst,
1010
} from '@prisma-next/sql-relational-core/ast';
1111
import {
@@ -37,7 +37,7 @@ import { expandSelectAll } from './transform-select';
3737
import { resolveColumnRef, transformTableRef, validateColumn } from './transform-validate';
3838

3939
function assertParamRef(value: ReturnType<typeof transformValue>): ParamRef {
40-
if (!(value instanceof ParamRef)) {
40+
if (value.kind !== 'param-ref') {
4141
throw new KyselyTransformError(
4242
'Only parameterized VALUES are supported in Kysely transform lane',
4343
KYSELY_TRANSFORM_ERROR_CODES.UNSUPPORTED_NODE,

packages/2-sql/4-lanes/kysely-lane/src/transform/transform-expr.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { ParamDescriptor } from '@prisma-next/contract/types';
2-
import type { BinaryOp, JoinOnExpr, WhereExpr } from '@prisma-next/sql-relational-core/ast';
2+
import type { AnyWhereExpr, BinaryOp, JoinOnExpr } from '@prisma-next/sql-relational-core/ast';
33
import {
44
AndExpr,
55
BinaryExpr,
6-
ColumnRef,
6+
type ColumnRef,
77
EqColJoinOn,
88
ListLiteralExpr,
99
LiteralExpr,
@@ -113,7 +113,7 @@ function flattenLogical(
113113
logicalKind: 'and' | 'or',
114114
ctx: TransformContext,
115115
defaultTable: string | undefined,
116-
out: WhereExpr[],
116+
out: AnyWhereExpr[],
117117
): void {
118118
const current = ParensNode.is(node) ? node.node : node;
119119
if (logicalKind === 'and' && AndNode.is(current)) {
@@ -159,7 +159,7 @@ export function transformWhereExpr(
159159
node: unknown,
160160
ctx: TransformContext,
161161
defaultTable?: string,
162-
): WhereExpr | undefined {
162+
): AnyWhereExpr | undefined {
163163
if (!node) {
164164
return undefined;
165165
}
@@ -173,15 +173,15 @@ export function transformWhereExpr(
173173
}
174174

175175
if (AndNode.is(node)) {
176-
const exprs: WhereExpr[] = [];
176+
const exprs: AnyWhereExpr[] = [];
177177
flattenLogical(node, 'and', ctx, defaultTable, exprs);
178178
if (exprs.length === 0) return undefined;
179179
if (exprs.length === 1) return exprs[0];
180180
return AndExpr.of(exprs);
181181
}
182182

183183
if (OrNode.is(node)) {
184-
const exprs: WhereExpr[] = [];
184+
const exprs: AnyWhereExpr[] = [];
185185
flattenLogical(node, 'or', ctx, defaultTable, exprs);
186186
if (exprs.length === 0) return undefined;
187187
if (exprs.length === 1) return exprs[0];
@@ -252,10 +252,10 @@ export function transformJoinOn(
252252
}
253253

254254
if (
255-
expr instanceof BinaryExpr &&
255+
expr.kind === 'binary' &&
256256
expr.op === 'eq' &&
257-
expr.left instanceof ColumnRef &&
258-
expr.right instanceof ColumnRef
257+
expr.left.kind === 'column-ref' &&
258+
expr.right.kind === 'column-ref'
259259
) {
260260
return EqColJoinOn.of(expr.left, expr.right);
261261
}

packages/2-sql/4-lanes/kysely-lane/src/transform/transform.ts

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
* - Ambiguous selectAll in multi-table scope → AMBIGUOUS_SELECT_ALL
88
* - Unsupported node kinds → UNSUPPORTED_NODE
99
*/
10-
import type { PlanRefs } from '@prisma-next/contract/types';
1110
import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
12-
import { ColumnRef, type QueryAst, SelectAst } from '@prisma-next/sql-relational-core/ast';
11+
import type { AnyQueryAst } from '@prisma-next/sql-relational-core/ast';
1312
import { ifDefined } from '@prisma-next/utils/defined';
1413
import { DeleteQueryNode, InsertQueryNode, SelectQueryNode, UpdateQueryNode } from 'kysely';
1514
import { KYSELY_TRANSFORM_ERROR_CODES, KyselyTransformError } from './errors';
@@ -21,10 +20,6 @@ import { transformSelect } from './transform-select';
2120

2221
export type { TransformResult };
2322

24-
function extractRefsFromAst(ast: QueryAst): PlanRefs {
25-
return ast.collectRefs();
26-
}
27-
2823
export interface TransformResultWithParams extends TransformResult {
2924
readonly params: readonly unknown[];
3025
}
@@ -49,7 +44,7 @@ export function transformKyselyToPnAst(
4944

5045
const ctx = createContext(contract, parameters);
5146

52-
let ast: QueryAst;
47+
let ast: AnyQueryAst;
5348
if (SelectQueryNode.is(query)) {
5449
ast = transformSelect(query, ctx);
5550
} else if (InsertQueryNode.is(query)) {
@@ -68,7 +63,7 @@ export function transformKyselyToPnAst(
6863
);
6964
}
7065

71-
const refs = extractRefsFromAst(ast);
66+
const refs = ast.collectRefs();
7267

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

7873
let projection: Record<string, string> | undefined;
7974
let projectionTypes: Record<string, string> | undefined;
80-
if (ast instanceof SelectAst) {
75+
const select = ast.kind === 'select' ? ast : undefined;
76+
if (select) {
8177
projection = Object.fromEntries(
82-
ast.projection.map((projected) => [
83-
projected.alias,
84-
projected.expr instanceof ColumnRef ? projected.expr.column : projected.alias,
85-
]),
78+
select.projection.map((projected) => {
79+
const col = projected.expr.kind === 'column-ref' ? projected.expr : undefined;
80+
return [projected.alias, col?.column ?? projected.alias];
81+
}),
8682
);
8783

8884
projectionTypes = {};
89-
for (const projected of ast.projection) {
90-
if (projected.expr instanceof ColumnRef) {
91-
const column =
92-
ctx.contract.storage.tables[projected.expr.table]?.columns[projected.expr.column];
85+
for (const projected of select.projection) {
86+
const col = projected.expr.kind === 'column-ref' ? projected.expr : undefined;
87+
if (col) {
88+
const column = ctx.contract.storage.tables[col.table]?.columns[col.column];
9389
if (column) {
9490
projectionTypes[projected.alias] = column.codecId;
9591
}
@@ -105,8 +101,8 @@ export function transformKyselyToPnAst(
105101
'projectionTypes',
106102
projectionTypes && Object.keys(projectionTypes).length > 0 ? projectionTypes : undefined,
107103
),
108-
...ifDefined('selectAllIntent', ast instanceof SelectAst ? ast.selectAllIntent : undefined),
109-
...ifDefined('limit', ast instanceof SelectAst ? ast.limit : undefined),
104+
...ifDefined('selectAllIntent', select?.selectAllIntent),
105+
...ifDefined('limit', select?.limit),
110106
};
111107

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

132128
const ctx = createContext(contract);
133129

134-
let ast: QueryAst;
130+
let ast: AnyQueryAst;
135131
if (SelectQueryNode.is(query)) {
136132
ast = transformSelect(query, ctx);
137133
} else if (InsertQueryNode.is(query)) {
@@ -150,7 +146,7 @@ export function transformKyselyToPnAstCollectingParams(
150146
);
151147
}
152148

153-
const refs = extractRefsFromAst(ast);
149+
const refs = ast.collectRefs();
154150

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

160156
let projection: Record<string, string> | undefined;
161157
let projectionTypes: Record<string, string> | undefined;
162-
if (ast instanceof SelectAst) {
158+
const select = ast.kind === 'select' ? ast : undefined;
159+
if (select) {
163160
projection = Object.fromEntries(
164-
ast.projection.map((projected) => [
165-
projected.alias,
166-
projected.expr instanceof ColumnRef ? projected.expr.column : projected.alias,
167-
]),
161+
select.projection.map((projected) => {
162+
const col = projected.expr.kind === 'column-ref' ? projected.expr : undefined;
163+
return [projected.alias, col?.column ?? projected.alias];
164+
}),
168165
);
169166

170167
projectionTypes = {};
171-
for (const projected of ast.projection) {
172-
if (projected.expr instanceof ColumnRef) {
173-
const column =
174-
ctx.contract.storage.tables[projected.expr.table]?.columns[projected.expr.column];
168+
for (const projected of select.projection) {
169+
const col = projected.expr.kind === 'column-ref' ? projected.expr : undefined;
170+
if (col) {
171+
const column = ctx.contract.storage.tables[col.table]?.columns[col.column];
175172
if (column) {
176173
projectionTypes[projected.alias] = column.codecId;
177174
}
@@ -187,8 +184,8 @@ export function transformKyselyToPnAstCollectingParams(
187184
'projectionTypes',
188185
projectionTypes && Object.keys(projectionTypes).length > 0 ? projectionTypes : undefined,
189186
),
190-
...ifDefined('selectAllIntent', ast instanceof SelectAst ? ast.selectAllIntent : undefined),
191-
...ifDefined('limit', ast instanceof SelectAst ? ast.limit : undefined),
187+
...ifDefined('selectAllIntent', select?.selectAllIntent),
188+
...ifDefined('limit', select?.limit),
192189
};
193190

194191
return { ast, params: ctx.params, metaAdditions };

packages/2-sql/4-lanes/kysely-lane/src/where-expr.ast.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,13 @@ describe('buildKyselyWhereExpr nested AST traversal', () => {
156156
expect(bound.expr).toBeInstanceOf(ExistsExpr);
157157
const exists = bound.expr as ExistsExpr;
158158
const subquery = exists.subquery;
159-
expect(subquery.from).toBeInstanceOf(DerivedTableSource);
159+
expect(subquery.from.kind).toEqual('derived-table-source');
160160
expect(((subquery.from as DerivedTableSource).query.where as BinaryExpr).right).toEqual(
161161
ParamRef.of(3, 'kind'),
162162
);
163163

164164
const join = subquery.joins?.[0];
165-
expect(join?.source).toBeInstanceOf(DerivedTableSource);
165+
expect(join?.source.kind).toEqual('derived-table-source');
166166
expect((join?.on as BinaryExpr).right).toEqual(ParamRef.of(1, 'userId'));
167167
expect(((join?.source as DerivedTableSource).query.where as BinaryExpr).right).toEqual(
168168
ParamRef.of(4, 'title'),

packages/2-sql/4-lanes/kysely-lane/src/where-expr.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
33
import {
44
type BoundWhereExpr,
55
ListLiteralExpr,
6-
ParamRef,
7-
SelectAst,
86
type ToWhereExpr,
97
} from '@prisma-next/sql-relational-core/ast';
108
import type { CompiledQuery } from 'kysely';
@@ -29,7 +27,7 @@ export function buildKyselyWhereExpr<Row>(
2927
options: BuildKyselyPlanOptions = {},
3028
): ToWhereExpr {
3129
const plan = buildKyselyPlan(contract, compiledQuery, options);
32-
if (!(plan.ast instanceof SelectAst) || !plan.ast.where) {
30+
if (plan.ast.kind !== 'select' || !plan.ast.where) {
3331
throw new Error('whereExpr(...) requires a select query with a where clause');
3432
}
3533

@@ -97,7 +95,7 @@ function remapParamIndexes(
9795
listLiteral: (list) =>
9896
new ListLiteralExpr(
9997
list.values.map((value) => {
100-
if (!(value instanceof ParamRef)) {
98+
if (value.kind !== 'param-ref') {
10199
return value;
102100
}
103101
const newIndex = remap.get(value.index);

packages/2-sql/4-lanes/relational-core/src/ast/join.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { planInvalid } from '@prisma-next/plan';
22
import type { AnyColumnBuilder, JoinOnBuilder, JoinOnPredicate } from '../types';
33
import { isColumnBuilder } from '../types';
4-
import { EqColJoinOn, type FromSource, JoinAst } from './types';
4+
import { EqColJoinOn, JoinAst } from './types';
55

66
class JoinOnBuilderImpl implements JoinOnBuilder {
77
eqCol(left: AnyColumnBuilder, right: AnyColumnBuilder): JoinOnPredicate {
@@ -27,7 +27,7 @@ class JoinOnBuilderImpl implements JoinOnBuilder {
2727
}
2828
}
2929

30-
export { EqColJoinOn, JoinAst, type FromSource };
30+
export { EqColJoinOn, JoinAst };
3131

3232
export function createJoinOnBuilder(): JoinOnBuilder {
3333
return new JoinOnBuilderImpl();

0 commit comments

Comments
 (0)