Skip to content

Commit 0bbdcc4

Browse files
committed
fix: enhance SQL expression handling for JSON fields and improve query logging
1 parent 2a6fed4 commit 0bbdcc4

File tree

5 files changed

+30
-33
lines changed

5 files changed

+30
-33
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ export class FieldService implements IReadonlyAdapterService {
307307

308308
// Execute all queries (main table alteration + any additional queries like junction tables)
309309
for (const query of alterTableQueries) {
310+
this.logger.debug(`Executing alter table query: ${query}`);
310311
await this.prismaService.txClient().$executeRawUnsafe(query);
311312
}
312313

‎apps/nestjs-backend/src/features/record/query-builder/field-cte-visitor.ts‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -469,13 +469,14 @@ class FieldCteSelectionVisitor implements IFieldVisitor<IFieldSelectName> {
469469
}
470470
} else {
471471
const targetFieldResult = targetLookupField.accept(selectVisitor);
472-
expression =
473-
typeof targetFieldResult === 'string' ? targetFieldResult : targetFieldResult.toSQL().sql;
474-
// Self-join: ensure expression uses the foreign alias override
475472
const defaultForeignAlias = getTableAliasFromTable(this.foreignTable);
476-
if (defaultForeignAlias !== foreignAlias) {
477-
expression = expression.replaceAll(`"${defaultForeignAlias}"`, `"${foreignAlias}"`);
478-
}
473+
const baseExpression =
474+
typeof targetFieldResult === 'string' ? targetFieldResult : targetFieldResult.toSQL().sql;
475+
const normalizedBaseExpression =
476+
defaultForeignAlias !== foreignAlias
477+
? baseExpression.replaceAll(`"${defaultForeignAlias}"`, `"${foreignAlias}"`)
478+
: baseExpression;
479+
expression = normalizedBaseExpression;
479480

480481
// For Postgres multi-value lookups targeting datetime-like fields, normalize the
481482
// element expression to an ISO8601 UTC string so downstream JSON comparisons using
@@ -486,8 +487,9 @@ class FieldCteSelectionVisitor implements IFieldVisitor<IFieldSelectName> {
486487
field.isMultipleCellValue &&
487488
isDateLikeField(targetLookupField)
488489
) {
489-
// Format: 2020-01-10T16:00:00.000Z
490-
expression = `to_char(${expression} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`;
490+
// Format: 2020-01-10T16:00:00.000Z, wrap as jsonb so downstream aggregation remains valid JSON.
491+
const isoUtcExpr = `to_char(${normalizedBaseExpression} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`;
492+
expression = `to_jsonb(${isoUtcExpr})`;
491493
}
492494
}
493495
// Build deterministic order-by for multi-value lookups using the link field configuration

‎apps/nestjs-backend/src/features/record/query-builder/field-select-visitor.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import type {
2323
ButtonFieldCore,
2424
TableDomain,
2525
} from '@teable/core';
26-
import { FieldType, isLinkLookupOptions } from '@teable/core';
26+
import { DbFieldType, FieldType, isLinkLookupOptions } from '@teable/core';
2727
// no driver-specific logic here; use dialect for differences
2828
import type { Knex } from 'knex';
2929
import type { IDbProvider } from '../../../db-provider/db.provider.interface';
@@ -226,7 +226,7 @@ export class FieldSelectVisitor implements IFieldVisitor<IFieldSelectName> {
226226
// always emit the computed expression which degrades to NULL when references
227227
// are unresolved.
228228
if (this.rawProjection) {
229-
return this.dbProvider.convertFormulaToSelectQuery(expression, {
229+
const formulaSql = this.dbProvider.convertFormulaToSelectQuery(expression, {
230230
table: this.table,
231231
tableAlias: this.tableAlias,
232232
selectionMap: this.getSelectionMap(),
@@ -235,6 +235,10 @@ export class FieldSelectVisitor implements IFieldVisitor<IFieldSelectName> {
235235
preferRawFieldReferences: this.preferRawFieldReferences,
236236
targetDbFieldType: field.dbFieldType,
237237
});
238+
const normalized =
239+
field.dbFieldType === DbFieldType.Json ? `to_jsonb(${formulaSql})` : formulaSql;
240+
this.state.setSelection(field.id, normalized);
241+
return normalized;
238242
}
239243

240244
// For non-raw contexts where the generated column exists, select it directly

‎apps/nestjs-backend/src/features/record/query-builder/record-query-builder.service.ts‎

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { ID_FIELD_NAME, preservedDbFieldNames } from '../../field/constant';
99
import { TableDomainQueryService } from '../../table-domain/table-domain-query.service';
1010
import { FieldCteVisitor } from './field-cte-visitor';
1111
import { FieldSelectVisitor } from './field-select-visitor';
12-
import type { IFieldSelectName } from './field-select.type';
1312
import type {
1413
ICreateRecordAggregateBuilderOptions,
1514
ICreateRecordQueryBuilderOptions,
@@ -262,32 +261,17 @@ export class RecordQueryBuilderService implements IRecordQueryBuilder {
262261

263262
// Apply grouping if specified
264263
if (groupBy && groupBy.length > 0) {
265-
const groupingBuilder = this.dbProvider
264+
this.dbProvider
266265
.groupQuery(qb, fieldMap, groupByFieldIds, undefined, { selectionMap })
267266
.appendGroupBuilder();
268267

269-
// After grouping, the select list aliases each grouped column to its dbFieldName.
270-
// Normalize the selectionMap accordingly so subsequent sort builders reference the
271-
// projected alias instead of the original table-qualified column, avoiding
272-
// "must appear in GROUP BY" errors when ordering grouped aggregates.
273-
const mutableSelectionMap = selectionMap as Map<string, IFieldSelectName>;
274-
groupBy.forEach(({ fieldId }) => {
275-
const groupedField = fieldMap[fieldId];
276-
if (!groupedField) {
277-
return;
278-
}
268+
for (const groupItem of groupBy) {
269+
const groupedField = fieldMap[groupItem.fieldId];
270+
if (!groupedField) continue;
271+
const direction = groupItem.order === SortFunc.Desc ? 'DESC' : 'ASC';
272+
const nullOrdering = direction === 'DESC' ? 'NULLS LAST' : 'NULLS FIRST';
279273
const quotedAlias = `"${groupedField.dbFieldName.replace(/"/g, '""')}"`;
280-
mutableSelectionMap.set(fieldId, quotedAlias);
281-
});
282-
283-
const groupSortItems: ISortItem[] = groupBy.map((item) => ({
284-
fieldId: item.fieldId,
285-
order: item.order ?? SortFunc.Asc,
286-
}));
287-
if (groupSortItems.length > 0) {
288-
this.dbProvider
289-
.sortQuery(qb, fieldMap, groupSortItems, undefined, { selectionMap })
290-
.appendSortBuilder();
274+
qb.orderByRaw(`${quotedAlias} ${direction} ${nullOrdering}`);
291275
}
292276
}
293277

‎apps/nestjs-backend/src/features/record/query-builder/sql-conversion.visitor.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,6 +1321,9 @@ export class SelectColumnSqlConversionVisitor extends BaseSqlConversionVisitor<I
13211321
columnRef = adjusted;
13221322
}
13231323
if (fieldInfo.isLookup && isLinkLookupOptions(fieldInfo.lookupOptions)) {
1324+
if (preferRaw) {
1325+
return columnRef;
1326+
}
13241327
if (fieldInfo.dbFieldType !== DbFieldType.Json) {
13251328
return columnRef;
13261329
}
@@ -1427,6 +1430,9 @@ export class SelectColumnSqlConversionVisitor extends BaseSqlConversionVisitor<I
14271430
}
14281431

14291432
const preferRaw = !!selectContext.preferRawFieldReferences;
1433+
if (preferRaw) {
1434+
return expr;
1435+
}
14301436
if (preferRaw && selectContext.targetDbFieldType === DbFieldType.Json) {
14311437
return expr;
14321438
}

0 commit comments

Comments
 (0)