Skip to content

Commit 040bbd6

Browse files
authored
fix: fix link lookup a formula field reference a lookup field reference a link field (#2002)
* fix: fix link lookup a formula field reference a lookup field reference a link field * chore: aggregate should use query model * fix: fix date to char * fix: fix sum formula cast issue
1 parent bad27ed commit 040bbd6

25 files changed

Lines changed: 589 additions & 109 deletions

‎apps/nestjs-backend/src/db-provider/select-query/postgres/select-query.postgres.spec.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,20 @@ describe('SelectQueryPostgres unit-aware date helpers', () => {
158158
`DATE_TRUNC('${expectedUnit}', date_a::timestamp) = DATE_TRUNC('${expectedUnit}', date_b::timestamp)`
159159
);
160160
});
161+
162+
describe('numeric aggregate rewrites', () => {
163+
it('sum rewrites multiple params to addition with numeric coercion', () => {
164+
const sql = query.sum(['column_a', 'column_b', '10']);
165+
expect(sql).toBe(
166+
"(COALESCE(NULLIF(REGEXP_REPLACE((column_a)::text, '[^0-9.+-]', '', 'g'), '')::double precision, 0) + COALESCE(NULLIF(REGEXP_REPLACE((column_b)::text, '[^0-9.+-]', '', 'g'), '')::double precision, 0) + COALESCE(NULLIF(REGEXP_REPLACE((10)::text, '[^0-9.+-]', '', 'g'), '')::double precision, 0))"
167+
);
168+
});
169+
170+
it('average divides the rewritten sum by parameter count', () => {
171+
const sql = query.average(['column_a', '10']);
172+
expect(sql).toBe(
173+
"((COALESCE(NULLIF(REGEXP_REPLACE((column_a)::text, '[^0-9.+-]', '', 'g'), '')::double precision, 0) + COALESCE(NULLIF(REGEXP_REPLACE((10)::text, '[^0-9.+-]', '', 'g'), '')::double precision, 0))) / 2"
174+
);
175+
});
176+
});
161177
});

‎apps/nestjs-backend/src/db-provider/select-query/postgres/select-query.postgres.ts‎

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DbFieldType } from '@teable/core';
2+
import type { ISelectFormulaConversionContext } from '../../../features/record/query-builder/sql-conversion.visitor';
23
import { SelectQueryAbstract } from '../select-query.abstract';
34

45
/**
@@ -8,6 +9,17 @@ import { SelectQueryAbstract } from '../select-query.abstract';
89
* mutable functions and have different optimization strategies.
910
*/
1011
export class SelectQueryPostgres extends SelectQueryAbstract {
12+
private get tableAlias(): string | undefined {
13+
const ctx = this.context as ISelectFormulaConversionContext | undefined;
14+
return ctx?.tableAlias;
15+
}
16+
17+
private qualifySystemColumn(column: string): string {
18+
const quoted = `"${column}"`;
19+
const alias = this.tableAlias;
20+
return alias ? `"${alias}".${quoted}` : quoted;
21+
}
22+
1123
private toNumericSafe(expr: string): string {
1224
// Safely coerce any scalar to a floating-point number:
1325
// - Strip everything except digits, sign, decimal point
@@ -16,6 +28,10 @@ export class SelectQueryPostgres extends SelectQueryAbstract {
1628
return `NULLIF(REGEXP_REPLACE((${expr})::text, '[^0-9.+-]', '', 'g'), '')::double precision`;
1729
}
1830

31+
private coalesceNumeric(expr: string): string {
32+
return `COALESCE(${this.toNumericSafe(expr)}, 0)`;
33+
}
34+
1935
private isEmptyStringLiteral(value: string): boolean {
2036
return value.trim() === "''";
2137
}
@@ -220,12 +236,23 @@ export class SelectQueryPostgres extends SelectQueryAbstract {
220236
}
221237
// Numeric Functions
222238
sum(params: string[]): string {
223-
// In SELECT context, we can use window functions and aggregates more freely
224-
return `SUM(${this.joinParams(params)})`;
239+
if (params.length === 0) {
240+
return '0';
241+
}
242+
243+
const terms = params.map((param) => this.coalesceNumeric(param));
244+
if (terms.length === 1) {
245+
return terms[0];
246+
}
247+
return `(${terms.join(' + ')})`;
225248
}
226249

227250
average(params: string[]): string {
228-
return `AVG(${this.joinParams(params)})`;
251+
if (params.length === 0) {
252+
return '0';
253+
}
254+
const numerator = this.sum(params);
255+
return `(${numerator}) / ${params.length}`;
229256
}
230257

231258
max(params: string[]): string {
@@ -488,7 +515,7 @@ export class SelectQueryPostgres extends SelectQueryAbstract {
488515

489516
lastModifiedTime(): string {
490517
// This would typically reference a system column
491-
return `"__last_modified_time"`;
518+
return this.qualifySystemColumn('__last_modified_time');
492519
}
493520

494521
minute(date: string): string {
@@ -539,7 +566,7 @@ export class SelectQueryPostgres extends SelectQueryAbstract {
539566

540567
createdTime(): string {
541568
// This would typically reference a system column
542-
return `"__created_time"`;
569+
return this.qualifySystemColumn('__created_time');
543570
}
544571

545572
// Logical Functions
@@ -680,12 +707,12 @@ export class SelectQueryPostgres extends SelectQueryAbstract {
680707
// System Functions
681708
recordId(): string {
682709
// This would typically reference the primary key
683-
return `__id`;
710+
return this.qualifySystemColumn('__id');
684711
}
685712

686713
autoNumber(): string {
687714
// This would typically reference an auto-increment column
688-
return `__auto_number`;
715+
return this.qualifySystemColumn('__auto_number');
689716
}
690717

691718
textAll(value: string): string {

‎apps/nestjs-backend/src/db-provider/select-query/sqlite/select-query.sqlite.spec.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,20 @@ describe('SelectQuerySqlite unit-aware date helpers', () => {
151151
const sql = query.isSame('date_a', 'date_b', `'${literal}'`);
152152
expect(sql).toBe(`STRFTIME('${format}', date_a) = STRFTIME('${format}', date_b)`);
153153
});
154+
155+
describe('numeric aggregate rewrites', () => {
156+
it('sum rewrites multiple params to addition with numeric coercion', () => {
157+
const sql = query.sum(['column_a', 'column_b', '10']);
158+
expect(sql).toBe(
159+
'(COALESCE(CAST((column_a) AS REAL), 0) + COALESCE(CAST((column_b) AS REAL), 0) + COALESCE(CAST((10) AS REAL), 0))'
160+
);
161+
});
162+
163+
it('average divides the rewritten sum by parameter count', () => {
164+
const sql = query.average(['column_a', '10']);
165+
expect(sql).toBe(
166+
'((COALESCE(CAST((column_a) AS REAL), 0) + COALESCE(CAST((10) AS REAL), 0))) / 2'
167+
);
168+
});
169+
});
154170
});

‎apps/nestjs-backend/src/db-provider/select-query/sqlite/select-query.sqlite.ts‎

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ISelectFormulaConversionContext } from '../../../features/record/query-builder/sql-conversion.visitor';
12
import { SelectQueryAbstract } from '../select-query.abstract';
23

34
/**
@@ -7,6 +8,17 @@ import { SelectQueryAbstract } from '../select-query.abstract';
78
* more functions and have different optimization strategies.
89
*/
910
export class SelectQuerySqlite extends SelectQueryAbstract {
11+
private get tableAlias(): string | undefined {
12+
const ctx = this.context as ISelectFormulaConversionContext | undefined;
13+
return ctx?.tableAlias;
14+
}
15+
16+
private qualifySystemColumn(column: string): string {
17+
const quoted = `"${column}"`;
18+
const alias = this.tableAlias;
19+
return alias ? `"${alias}".${quoted}` : quoted;
20+
}
21+
1022
private isEmptyStringLiteral(value: string): boolean {
1123
return value.trim() === "''";
1224
}
@@ -31,13 +43,28 @@ export class SelectQuerySqlite extends SelectQueryAbstract {
3143
return `(${normalizedLeft} ${operator} ${normalizedRight})`;
3244
}
3345

46+
private coalesceNumeric(expr: string): string {
47+
return `COALESCE(CAST((${expr}) AS REAL), 0)`;
48+
}
49+
3450
// Numeric Functions
3551
sum(params: string[]): string {
36-
return `SUM(${this.joinParams(params)})`;
52+
if (params.length === 0) {
53+
return '0';
54+
}
55+
const terms = params.map((param) => this.coalesceNumeric(param));
56+
if (terms.length === 1) {
57+
return terms[0];
58+
}
59+
return `(${terms.join(' + ')})`;
3760
}
3861

3962
average(params: string[]): string {
40-
return `AVG(${this.joinParams(params)})`;
63+
if (params.length === 0) {
64+
return '0';
65+
}
66+
const numerator = this.sum(params);
67+
return `(${numerator}) / ${params.length}`;
4168
}
4269

4370
max(params: string[]): string {
@@ -399,7 +426,7 @@ export class SelectQuerySqlite extends SelectQueryAbstract {
399426
}
400427

401428
lastModifiedTime(): string {
402-
return `"__last_modified_time"`;
429+
return this.qualifySystemColumn('__last_modified_time');
403430
}
404431

405432
minute(date: string): string {
@@ -445,7 +472,7 @@ export class SelectQuerySqlite extends SelectQueryAbstract {
445472
}
446473

447474
createdTime(): string {
448-
return `"__created_time"`;
475+
return this.qualifySystemColumn('__created_time');
449476
}
450477

451478
// Logical Functions
@@ -546,11 +573,11 @@ export class SelectQuerySqlite extends SelectQueryAbstract {
546573

547574
// System Functions
548575
recordId(): string {
549-
return `__id`;
576+
return this.qualifySystemColumn('__id');
550577
}
551578

552579
autoNumber(): string {
553-
return `__auto_number`;
580+
return this.qualifySystemColumn('__auto_number');
554581
}
555582

556583
textAll(value: string): string {

‎apps/nestjs-backend/src/features/aggregation/aggregation.service.interface.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface IAggregationService {
2929
withFieldIds?: string[];
3030
withView?: IWithView;
3131
search?: [string, string?, boolean?];
32+
useQueryModel?: boolean;
3233
}): Promise<IRawAggregationValue>;
3334

3435
/**

‎apps/nestjs-backend/src/features/aggregation/aggregation.service.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,9 @@ export class AggregationService implements IAggregationService {
8989
withFieldIds?: string[];
9090
withView?: IWithView;
9191
search?: [string, string?, boolean?];
92+
useQueryModel?: boolean;
9293
}): Promise<IRawAggregationValue> {
93-
const { tableId, withFieldIds, withView, search } = params;
94+
const { tableId, withFieldIds, withView, search, useQueryModel } = params;
9495
// Retrieve the current user's ID to build user-related query conditions
9596
const currentUserId = this.cls.get('user.id');
9697

@@ -113,6 +114,7 @@ export class AggregationService implements IAggregationService {
113114
statisticFields,
114115
withUserId: currentUserId,
115116
withView,
117+
useQueryModel,
116118
});
117119

118120
const aggregationResult = rawAggregationData && rawAggregationData[0];
@@ -150,6 +152,7 @@ export class AggregationService implements IAggregationService {
150152
dbTableName,
151153
fieldInstanceMap,
152154
withView,
155+
useQueryModel,
153156
});
154157

155158
return { aggregations: aggregationsWithGroup };
@@ -204,6 +207,7 @@ export class AggregationService implements IAggregationService {
204207
statisticFields?: IAggregationField[];
205208
withUserId?: string;
206209
withView?: IWithView;
210+
useQueryModel?: boolean;
207211
}) {
208212
const {
209213
dbTableName,
@@ -215,6 +219,7 @@ export class AggregationService implements IAggregationService {
215219
groupBy,
216220
withView,
217221
tableId,
222+
useQueryModel,
218223
} = params;
219224

220225
if (!statisticFields?.length) {
@@ -244,6 +249,7 @@ export class AggregationService implements IAggregationService {
244249
currentUserId: withUserId,
245250
// Limit link/lookup CTEs to enabled fields so denied fields resolve to NULL
246251
projection,
252+
useQueryModel,
247253
});
248254

249255
// Attach permission CTE and switch FROM to the CTE alias
@@ -273,6 +279,7 @@ export class AggregationService implements IAggregationService {
273279
dbTableName: string;
274280
fieldInstanceMap: Record<string, IFieldInstance>;
275281
withView?: IWithView;
282+
useQueryModel?: boolean;
276283
}) {
277284
const {
278285
dbTableName,
@@ -284,6 +291,7 @@ export class AggregationService implements IAggregationService {
284291
fieldInstanceMap,
285292
withView,
286293
tableId,
294+
useQueryModel,
287295
} = params;
288296

289297
if (!groupBy || !statisticFields) return aggregations;
@@ -309,6 +317,7 @@ export class AggregationService implements IAggregationService {
309317
statisticFields,
310318
withUserId: currentUserId,
311319
withView,
320+
useQueryModel,
312321
}))!;
313322

314323
const currentGroupFieldId = groupByFields[i].fieldId;

‎apps/nestjs-backend/src/features/aggregation/open-api/aggregation-open-api.service.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export class AggregationOpenApiService {
5959
tableId: tableId,
6060
withView,
6161
search: query?.search,
62+
useQueryModel: true,
6263
});
6364
return { aggregations: result?.aggregations };
6465
}

‎apps/nestjs-backend/src/features/calculation/link.service.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,8 @@ export class LinkService {
818818
{
819819
tableIdOrDbTableName: tableId,
820820
viewId: undefined,
821+
rawProjection: true,
822+
preferRawFieldReferences: true,
821823
}
822824
);
823825

‎apps/nestjs-backend/src/features/field/field-calculate/field-supplement.service.ts‎

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,26 @@ export class FieldSupplementService {
230230
const foreignTableName = await this.getDbTableName(foreignTableId);
231231

232232
if (!lookupFieldId) {
233-
const { id: defaultLookupFieldId } = await this.prismaService
234-
.txClient()
235-
.field.findFirstOrThrow({
236-
where: { tableId: foreignTableId, isPrimary: true },
237-
select: { id: true },
238-
});
239-
lookupFieldId = defaultLookupFieldId;
233+
const labelField = await this.prismaService.txClient().field.findFirst({
234+
where: {
235+
tableId: foreignTableId,
236+
name: 'Label',
237+
deletedTime: null,
238+
},
239+
select: { id: true },
240+
});
241+
242+
if (labelField?.id) {
243+
lookupFieldId = labelField.id;
244+
} else {
245+
const { id: defaultLookupFieldId } = await this.prismaService
246+
.txClient()
247+
.field.findFirstOrThrow({
248+
where: { tableId: foreignTableId, isPrimary: true },
249+
select: { id: true },
250+
});
251+
lookupFieldId = defaultLookupFieldId;
252+
}
240253
}
241254

242255
if (baseId) {
@@ -305,13 +318,21 @@ export class FieldSupplementService {
305318
}
306319
}
307320
if (!lookupFieldId) {
308-
const { id: defaultLookupFieldId } = await this.prismaService
309-
.txClient()
310-
.field.findFirstOrThrow({
311-
where: { tableId: foreignTableId, isPrimary: true, deletedTime: null },
312-
select: { id: true },
313-
});
314-
lookupFieldId = defaultLookupFieldId;
321+
const labelField = await this.prismaService.txClient().field.findFirst({
322+
where: { tableId: foreignTableId, name: 'Label', deletedTime: null },
323+
select: { id: true },
324+
});
325+
if (labelField?.id) {
326+
lookupFieldId = labelField.id;
327+
} else {
328+
const { id: defaultLookupFieldId } = await this.prismaService
329+
.txClient()
330+
.field.findFirstOrThrow({
331+
where: { tableId: foreignTableId, isPrimary: true, deletedTime: null },
332+
select: { id: true },
333+
});
334+
lookupFieldId = defaultLookupFieldId;
335+
}
315336
}
316337

317338
if (baseId) {

‎apps/nestjs-backend/src/features/record/computed/services/computed-evaluator.service.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export class ComputedEvaluatorService {
8888
tableIdOrDbTableName: tableId,
8989
projection: Array.from(validFieldIdSet),
9090
rawProjection: true,
91+
preferRawFieldReferences: true,
9192
});
9293

9394
const idCol = alias ? `${alias}.__id` : '__id';

0 commit comments

Comments
 (0)