Skip to content

Commit ca8f437

Browse files
authored
fix(orm): use compact alias names when transforming ORM queries to Kysely (#2406)
1 parent 17922f0 commit ca8f437

File tree

14 files changed

+195
-26
lines changed

14 files changed

+195
-26
lines changed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
2020
### Testing
2121

2222
- E2E tests are in `tests/e2e/` directory
23+
- Regression tests for GitHub issues go in `tests/regression/test/` as `issue-{number}.test.ts`
2324

2425
### ZenStack CLI Commands
2526

packages/orm/src/client/crud/dialects/base-dialect.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
requireIdFields,
3535
requireModel,
3636
requireTypeDef,
37+
tmpAlias,
3738
} from '../../query-utils';
3839

3940
export abstract class BaseCrudDialect<Schema extends SchemaDef> {
@@ -298,7 +299,7 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
298299
}
299300
}
300301

301-
const joinAlias = `${modelAlias}$${field}`;
302+
const joinAlias = tmpAlias(`${modelAlias}$${field}`);
302303
const joinPairs = buildJoinPairs(
303304
this.schema,
304305
model,
@@ -307,7 +308,7 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
307308
field,
308309
joinAlias,
309310
);
310-
const filterResultField = `${field}$filter`;
311+
const filterResultField = tmpAlias(`${field}$flt`);
311312

312313
const joinSelect = this.eb
313314
.selectFrom(`${fieldDef.type} as ${joinAlias}`)
@@ -383,7 +384,7 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
383384

384385
// evaluating the filter involves creating an inner select,
385386
// give it an alias to avoid conflict
386-
const relationFilterSelectAlias = `${modelAlias}$${field}$filter`;
387+
const relationFilterSelectAlias = tmpAlias(`${modelAlias}$${field}$flt`);
387388

388389
const buildPkFkWhereRefs = (eb: ExpressionBuilder<any, any>) => {
389390
const m2m = getManyToManyRelation(this.schema, model, field);
@@ -1083,7 +1084,7 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
10831084
);
10841085
const sort = this.negateSort(value._count, negated);
10851086
result = result.orderBy((eb) => {
1086-
const subQueryAlias = `${modelAlias}$orderBy$${field}$count`;
1087+
const subQueryAlias = tmpAlias(`${modelAlias}$ob$${field}$ct`);
10871088
let subQuery = this.buildSelectModel(relationModel, subQueryAlias);
10881089
const joinPairs = buildJoinPairs(this.schema, model, modelAlias, field, subQueryAlias);
10891090
subQuery = subQuery.where(() =>
@@ -1099,7 +1100,7 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
10991100
}
11001101
} else {
11011102
// order by to-one relation
1102-
const joinAlias = `${modelAlias}$orderBy$${index}`;
1103+
const joinAlias = tmpAlias(`${modelAlias}$ob$${index}`);
11031104
result = result.leftJoin(`${relationModel} as ${joinAlias}`, (join) => {
11041105
const joinPairs = buildJoinPairs(this.schema, model, modelAlias, field, joinAlias);
11051106
return join.on((eb) =>

packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
requireField,
1212
requireIdFields,
1313
requireModel,
14+
tmpAlias,
1415
} from '../../query-utils';
1516
import { BaseCrudDialect } from './base-dialect';
1617

@@ -31,7 +32,7 @@ export abstract class LateralJoinDialectBase<Schema extends SchemaDef> extends B
3132
parentAlias: string,
3233
payload: true | FindArgs<Schema, GetModels<Schema>, any, true>,
3334
): SelectQueryBuilder<any, any, any> {
34-
const relationResultName = `${parentAlias}$${relationField}`;
35+
const relationResultName = tmpAlias(`${parentAlias}$${relationField}`);
3536
const joinedQuery = this.buildRelationJSON(
3637
model,
3738
query,
@@ -56,7 +57,7 @@ export abstract class LateralJoinDialectBase<Schema extends SchemaDef> extends B
5657

5758
return qb.leftJoinLateral(
5859
(eb) => {
59-
const relationSelectName = `${resultName}$sub`;
60+
const relationSelectName = tmpAlias(`${resultName}$sub`);
6061
const relationModelDef = requireModel(this.schema, relationModel);
6162

6263
let tbl: SelectQueryBuilder<any, any, any>;

packages/orm/src/client/crud/dialects/mysql.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ export class MySqlCrudDialect<Schema extends SchemaDef> extends LateralJoinDiale
306306
return this.eb.exists(
307307
this.eb
308308
.selectFrom(sql`JSON_TABLE(${receiver}, '$[*]' COLUMNS(value JSON PATH '$'))`.as('$items'))
309-
.select(this.eb.lit(1).as('$t'))
309+
.select(this.eb.lit(1).as('_'))
310310
.where(buildFilter(this.eb.ref('$items.value'))),
311311
);
312312
}

packages/orm/src/client/crud/dialects/postgresql.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends LateralJoinDi
352352
return this.eb.exists(
353353
this.eb
354354
.selectFrom(this.eb.fn('jsonb_array_elements', [receiver]).as('$items'))
355-
.select(this.eb.lit(1).as('$t'))
355+
.select(this.eb.lit(1).as('_'))
356356
.where(buildFilter(this.eb.ref('$items.value'))),
357357
);
358358
}

packages/orm/src/client/crud/dialects/sqlite.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
requireField,
2626
requireIdFields,
2727
requireModel,
28+
tmpAlias,
2829
} from '../../query-utils';
2930
import { BaseCrudDialect } from './base-dialect';
3031

@@ -201,7 +202,7 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
201202
const relationModel = relationFieldDef.type as GetModels<Schema>;
202203
const relationModelDef = requireModel(this.schema, relationModel);
203204

204-
const subQueryName = `${parentAlias}$${relationField}`;
205+
const subQueryName = tmpAlias(`${parentAlias}$${relationField}`);
205206
let tbl: SelectQueryBuilder<any, any, any>;
206207

207208
if (this.canJoinWithoutNestedSelect(relationModelDef, payload)) {
@@ -214,7 +215,7 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
214215
// need to make a nested select on relation model
215216
tbl = eb.selectFrom(() => {
216217
// nested query name
217-
const selectModelAlias = `${parentAlias}$${relationField}$sub`;
218+
const selectModelAlias = tmpAlias(`${parentAlias}$${relationField}$sub`);
218219

219220
// select all fields
220221
let selectModelQuery = this.buildModelSelect(relationModel, selectModelAlias, payload, true);
@@ -268,7 +269,7 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
268269
const subJson = this.buildCountJson(
269270
relationModel,
270271
eb,
271-
`${parentAlias}$${relationField}`,
272+
tmpAlias(`${parentAlias}$${relationField}`),
272273
value,
273274
);
274275
return [sql.lit(field), subJson];
@@ -279,7 +280,7 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
279280
relationModel,
280281
eb,
281282
field,
282-
`${parentAlias}$${relationField}`,
283+
tmpAlias(`${parentAlias}$${relationField}`),
283284
value,
284285
);
285286
return [sql.lit(field), subJson];
@@ -305,7 +306,7 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
305306
relationModel,
306307
eb,
307308
field,
308-
`${parentAlias}$${relationField}`,
309+
tmpAlias(`${parentAlias}$${relationField}`),
309310
value,
310311
);
311312
return [sql.lit(field), subJson];
@@ -440,7 +441,7 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
440441
return this.eb.exists(
441442
this.eb
442443
.selectFrom(this.eb.fn('json_each', [receiver]).as('$items'))
443-
.select(this.eb.lit(1).as('$t'))
444+
.select(this.eb.lit(1).as('_'))
444445
.where(buildFilter(this.eb.ref('$items.value'))),
445446
);
446447
}

packages/orm/src/client/crud/operations/base.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,23 +260,23 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
260260
.exists(
261261
this.dialect
262262
.buildSelectModel(model, model)
263-
.select(sql.lit(1).as('$t'))
263+
.select(sql.lit(1).as('_'))
264264
.where(() => this.dialect.buildFilter(model, model, filter)),
265265
)
266-
.as('exists'),
266+
.as('$exists'),
267267
)
268268
.modifyEnd(this.makeContextComment({ model, operation: 'read' }));
269269

270-
let result: { exists: number | boolean }[] = [];
270+
let result: { $exists: number | boolean }[] = [];
271271
const compiled = kysely.getExecutor().compileQuery(query.toOperationNode(), createQueryId());
272272
try {
273273
const r = await kysely.getExecutor().executeQuery(compiled);
274-
result = r.rows as { exists: number | boolean }[];
274+
result = r.rows as { $exists: number | boolean }[];
275275
} catch (err) {
276276
throw createDBQueryError(`Failed to execute query: ${err}`, err, compiled.sql, compiled.parameters);
277277
}
278278

279-
return !!result[0]?.exists;
279+
return !!result[0]?.$exists;
280280
}
281281

282282
protected async read(
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { IdentifierNode, OperationNodeTransformer, type OperationNode, type QueryId } from 'kysely';
2+
import { TEMP_ALIAS_PREFIX } from '../query-utils';
3+
4+
/**
5+
* Kysely node transformer that replaces temporary aliases created during query construction with
6+
* shorter names while ensuring the same temp alias gets replaced with the same name.
7+
*/
8+
export class TempAliasTransformer extends OperationNodeTransformer {
9+
private aliasMap = new Map<string, string>();
10+
11+
run<T extends OperationNode>(node: T): T {
12+
this.aliasMap.clear();
13+
return this.transformNode(node);
14+
}
15+
16+
protected override transformIdentifier(node: IdentifierNode, queryId?: QueryId): IdentifierNode {
17+
if (node.name.startsWith(TEMP_ALIAS_PREFIX)) {
18+
let mapped = this.aliasMap.get(node.name);
19+
if (!mapped) {
20+
mapped = `$$t${this.aliasMap.size + 1}`;
21+
this.aliasMap.set(node.name, mapped);
22+
}
23+
return IdentifierNode.create(mapped);
24+
}
25+
return super.transformIdentifier(node, queryId);
26+
}
27+
}

packages/orm/src/client/executor/zenstack-query-executor.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { createDBQueryError, createInternalError, ORMError } from '../errors';
3939
import type { AfterEntityMutationCallback, OnKyselyQueryCallback } from '../plugin';
4040
import { requireIdFields, stripAlias } from '../query-utils';
4141
import { QueryNameMapper } from './name-mapper';
42+
import { TempAliasTransformer } from './temp-alias-transformer';
4243
import type { ZenStackDriver } from './zenstack-driver';
4344

4445
type MutationQueryNode = InsertQueryNode | UpdateQueryNode | DeleteQueryNode;
@@ -620,10 +621,24 @@ In such cases, ZenStack cannot reliably determine the IDs of the mutated entitie
620621
}) as string;
621622
}
622623

624+
private processQueryNode<Node extends RootOperationNode>(query: Node): Node {
625+
let result = query;
626+
result = this.processNameMapping(result);
627+
result = this.processTempAlias(result);
628+
return result;
629+
}
630+
623631
private processNameMapping<Node extends RootOperationNode>(query: Node): Node {
624632
return this.nameMapper?.transformNode(query) ?? query;
625633
}
626634

635+
private processTempAlias<Node extends RootOperationNode>(query: Node): Node {
636+
if (this.options.useCompactAliasNames === false) {
637+
return query;
638+
}
639+
return new TempAliasTransformer().run(query);
640+
}
641+
627642
private createClientForConnection(connection: DatabaseConnection, inTx: boolean) {
628643
const innerExecutor = this.withConnectionProvider(new SingleConnectionProvider(connection));
629644
innerExecutor.suppressMutationHooks = true;
@@ -650,8 +665,8 @@ In such cases, ZenStack cannot reliably determine the IDs of the mutated entitie
650665
queryId?: QueryId,
651666
parameters?: readonly unknown[],
652667
) {
653-
// no need to handle mutation hooks, just proceed
654-
const finalQuery = this.processNameMapping(query);
668+
// run query node processors: name mapping, temp alias renaming, etc.
669+
const finalQuery = this.processQueryNode(query);
655670

656671
// inherit the original queryId
657672
let compiledQuery = this.compileQuery(finalQuery, queryId ?? createQueryId());

packages/orm/src/client/options.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@ export type ClientOptions<Schema extends SchemaDef> = QueryOptions<Schema> & {
197197
* `@@validate`, etc. Defaults to `true`.
198198
*/
199199
validateInput?: boolean;
200+
201+
/**
202+
* Whether to use compact alias names (e.g., "$t1", "$t2") when transforming ORM queries to SQL.
203+
* Defaults to `true`.
204+
*/
205+
useCompactAliasNames?: boolean;
200206
} & (HasComputedFields<Schema> extends true
201207
? {
202208
/**

0 commit comments

Comments
 (0)