Skip to content

Commit 35de8e9

Browse files
authored
Merge pull request #2069 from teableio/fix/group
fix: enhance group sorting logic to respect database-sorted results in RecordService
2 parents b67a6cc + e4fc4aa commit 35de8e9

23 files changed

+706
-254
lines changed

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

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -70,79 +70,79 @@ describe('GeneratedColumnQueryPostgres unit-aware helpers', () => {
7070
const datetimeDiffCases: Array<{ literal: string; expected: string }> = [
7171
{
7272
literal: 'millisecond',
73-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp)) * 1000',
73+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp)) * 1000',
7474
},
7575
{
7676
literal: 'milliseconds',
77-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp)) * 1000',
77+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp)) * 1000',
7878
},
7979
{
8080
literal: 'ms',
81-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp)) * 1000',
81+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp)) * 1000',
8282
},
8383
{
8484
literal: 'second',
85-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp))',
85+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp))',
8686
},
8787
{
8888
literal: 'seconds',
89-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp))',
89+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp))',
9090
},
9191
{
9292
literal: 'sec',
93-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp))',
93+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp))',
9494
},
9595
{
9696
literal: 'secs',
97-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp))',
97+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp))',
9898
},
9999
{
100100
literal: 'minute',
101-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp)) / 60',
101+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp)) / 60',
102102
},
103103
{
104104
literal: 'minutes',
105-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp)) / 60',
105+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp)) / 60',
106106
},
107107
{
108108
literal: 'min',
109-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp)) / 60',
109+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp)) / 60',
110110
},
111111
{
112112
literal: 'mins',
113-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp)) / 60',
113+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp)) / 60',
114114
},
115115
{
116116
literal: 'hour',
117-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp)) / 3600',
117+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp)) / 3600',
118118
},
119119
{
120120
literal: 'hours',
121-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp)) / 3600',
121+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp)) / 3600',
122122
},
123123
{
124124
literal: 'hr',
125-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp)) / 3600',
125+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp)) / 3600',
126126
},
127127
{
128128
literal: 'hrs',
129-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp)) / 3600',
129+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp)) / 3600',
130130
},
131131
{
132132
literal: 'week',
133-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp)) / (86400 * 7)',
133+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp)) / (86400 * 7)',
134134
},
135135
{
136136
literal: 'weeks',
137-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp)) / (86400 * 7)',
137+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp)) / (86400 * 7)',
138138
},
139139
{
140140
literal: 'day',
141-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp)) / 86400',
141+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp)) / 86400',
142142
},
143143
{
144144
literal: 'days',
145-
expected: '(EXTRACT(EPOCH FROM date_end::timestamp - date_start::timestamp)) / 86400',
145+
expected: '(EXTRACT(EPOCH FROM date_start::timestamp - date_end::timestamp)) / 86400',
146146
},
147147
];
148148

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ export class GeneratedColumnQueryPostgres extends GeneratedColumnQueryAbstract {
474474

475475
datetimeDiff(startDate: string, endDate: string, unit: string): string {
476476
const diffUnit = this.normalizeDiffUnit(unit.replace(/^'|'$/g, ''));
477-
const diffSeconds = `EXTRACT(EPOCH FROM ${endDate}::timestamp - ${startDate}::timestamp)`;
477+
const diffSeconds = `EXTRACT(EPOCH FROM ${startDate}::timestamp - ${endDate}::timestamp)`;
478478
switch (diffUnit) {
479479
case 'millisecond':
480480
return `(${diffSeconds}) * 1000`;

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

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -56,74 +56,74 @@ describe('GeneratedColumnQuerySqlite unit-aware helpers', () => {
5656
const datetimeDiffCases: Array<{ literal: string; expected: string }> = [
5757
{
5858
literal: 'millisecond',
59-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) * 24.0 * 60 * 60 * 1000',
59+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) * 24.0 * 60 * 60 * 1000',
6060
},
6161
{
6262
literal: 'milliseconds',
63-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) * 24.0 * 60 * 60 * 1000',
63+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) * 24.0 * 60 * 60 * 1000',
6464
},
6565
{
6666
literal: 'ms',
67-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) * 24.0 * 60 * 60 * 1000',
67+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) * 24.0 * 60 * 60 * 1000',
6868
},
6969
{
7070
literal: 'second',
71-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) * 24.0 * 60 * 60',
71+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) * 24.0 * 60 * 60',
7272
},
7373
{
7474
literal: 'seconds',
75-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) * 24.0 * 60 * 60',
75+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) * 24.0 * 60 * 60',
7676
},
7777
{
7878
literal: 'sec',
79-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) * 24.0 * 60 * 60',
79+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) * 24.0 * 60 * 60',
8080
},
8181
{
8282
literal: 'secs',
83-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) * 24.0 * 60 * 60',
83+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) * 24.0 * 60 * 60',
8484
},
8585
{
8686
literal: 'minute',
87-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) * 24.0 * 60',
87+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) * 24.0 * 60',
8888
},
8989
{
9090
literal: 'minutes',
91-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) * 24.0 * 60',
91+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) * 24.0 * 60',
9292
},
9393
{
9494
literal: 'min',
95-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) * 24.0 * 60',
95+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) * 24.0 * 60',
9696
},
9797
{
9898
literal: 'mins',
99-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) * 24.0 * 60',
99+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) * 24.0 * 60',
100100
},
101101
{
102102
literal: 'hour',
103-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) * 24.0',
103+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) * 24.0',
104104
},
105105
{
106106
literal: 'hours',
107-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) * 24.0',
107+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) * 24.0',
108108
},
109109
{
110110
literal: 'hr',
111-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) * 24.0',
111+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) * 24.0',
112112
},
113113
{
114114
literal: 'hrs',
115-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) * 24.0',
115+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) * 24.0',
116116
},
117117
{
118118
literal: 'week',
119-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) / 7.0',
119+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) / 7.0',
120120
},
121121
{
122122
literal: 'weeks',
123-
expected: '((JULIANDAY(date_end) - JULIANDAY(date_start))) / 7.0',
123+
expected: '((JULIANDAY(date_start) - JULIANDAY(date_end))) / 7.0',
124124
},
125-
{ literal: 'day', expected: '(JULIANDAY(date_end) - JULIANDAY(date_start))' },
126-
{ literal: 'days', expected: '(JULIANDAY(date_end) - JULIANDAY(date_start))' },
125+
{ literal: 'day', expected: '(JULIANDAY(date_start) - JULIANDAY(date_end))' },
126+
{ literal: 'days', expected: '(JULIANDAY(date_start) - JULIANDAY(date_end))' },
127127
];
128128

129129
it.each(datetimeDiffCases)('datetimeDiff normalizes unit "%s"', ({ literal, expected }) => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ export class GeneratedColumnQuerySqlite extends GeneratedColumnQueryAbstract {
437437
}
438438

439439
datetimeDiff(startDate: string, endDate: string, unit: string): string {
440-
const baseDiffDays = `(JULIANDAY(${endDate}) - JULIANDAY(${startDate}))`;
440+
const baseDiffDays = `(JULIANDAY(${startDate}) - JULIANDAY(${endDate}))`;
441441
switch (this.normalizeDiffUnit(unit)) {
442442
case 'millisecond':
443443
return `(${baseDiffDays}) * 24.0 * 60 * 60 * 1000`;

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

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
/* eslint-disable sonarjs/no-duplicate-string */
2-
import { TableDomain } from '@teable/core';
2+
import { CellValueType, DbFieldType, FieldType, TableDomain } from '@teable/core';
3+
import type { IFieldVo } from '@teable/core';
4+
import knex from 'knex';
35
import { beforeEach, describe, expect, it } from 'vitest';
46

7+
import { createFieldInstanceByVo } from '../../../features/field/model/factory';
58
import type { IFieldSelectName } from '../../../features/record/query-builder/field-select.type';
69
import type { ISelectFormulaConversionContext } from '../../../features/record/query-builder/sql-conversion.visitor';
10+
import { PostgresProvider } from '../../postgres.provider';
711
import { SelectQueryPostgres } from './select-query.postgres';
812

913
describe('SelectQueryPostgres unit-aware date helpers', () => {
@@ -132,9 +136,9 @@ describe('SelectQueryPostgres unit-aware date helpers', () => {
132136
);
133137
});
134138

135-
it('datetimeDiff subtracts timezone-normalized expressions', () => {
136-
expect(tzQuery.datetimeDiff('start_col', 'end_col', `'day'`)).toBe(
137-
`(EXTRACT(EPOCH FROM (${tz('end_col')} - ${tz('start_col')}))) / 86400`
139+
it('datetimeDiff subtracts the second argument from the first after timezone normalization', () => {
140+
expect(tzQuery.datetimeDiff('first_col', 'second_col', `'day'`)).toBe(
141+
`(EXTRACT(EPOCH FROM (${tz('first_col')} - ${tz('second_col')}))) / 86400`
138142
);
139143
});
140144

@@ -188,7 +192,7 @@ describe('SelectQueryPostgres unit-aware date helpers', () => {
188192
expect(sql).toBe(`${localWrap('date_col')} + (${scaled}) * INTERVAL '1 ${unit}'`);
189193
});
190194

191-
const localDiffBase = `(EXTRACT(EPOCH FROM (${localWrap('date_end')} - ${localWrap('date_start')})))`;
195+
const localDiffBase = `(EXTRACT(EPOCH FROM (${localWrap('date_start')} - ${localWrap('date_end')})))`;
192196
const datetimeDiffCases: Array<{ literal: string; expected: string }> = [
193197
{ literal: 'millisecond', expected: `${localDiffBase} * 1000` },
194198
{ literal: 'milliseconds', expected: `${localDiffBase} * 1000` },
@@ -267,3 +271,69 @@ describe('SelectQueryPostgres unit-aware date helpers', () => {
267271
});
268272
});
269273
});
274+
275+
describe('Select formula boolean normalization', () => {
276+
const knexClient = knex({ client: 'pg' });
277+
const provider = new PostgresProvider(knexClient);
278+
279+
const booleanFieldVo: IFieldVo = {
280+
id: 'fldBoolean001',
281+
name: 'Boolean Flag',
282+
type: FieldType.Checkbox,
283+
options: {},
284+
dbFieldName: 'bool_col',
285+
dbFieldType: DbFieldType.Boolean,
286+
cellValueType: CellValueType.Boolean,
287+
isLookup: false,
288+
isComputed: false,
289+
isMultipleCellValue: false,
290+
};
291+
292+
const textFieldVo: IFieldVo = {
293+
id: 'fldText001',
294+
name: 'Text Field',
295+
type: FieldType.SingleLineText,
296+
options: {},
297+
dbFieldName: 'text_col',
298+
dbFieldType: DbFieldType.Text,
299+
cellValueType: CellValueType.String,
300+
isLookup: false,
301+
isComputed: false,
302+
isMultipleCellValue: false,
303+
};
304+
305+
const booleanField = createFieldInstanceByVo(booleanFieldVo);
306+
const textField = createFieldInstanceByVo(textFieldVo);
307+
308+
const table = new TableDomain({
309+
id: 'tblBoolean',
310+
name: 'Boolean Table',
311+
dbTableName: 'boolean_table',
312+
lastModifiedTime: '1970-01-01T00:00:00.000Z',
313+
fields: [booleanField, textField],
314+
});
315+
316+
const buildContext = (): ISelectFormulaConversionContext => ({
317+
table,
318+
tableAlias: 'main',
319+
selectionMap: new Map<string, IFieldSelectName>([
320+
[booleanField.id, '"main"."bool_col"'],
321+
[textField.id, '"main"."text_col"'],
322+
]),
323+
timeZone: 'UTC',
324+
preferRawFieldReferences: true,
325+
});
326+
327+
it('casts boolean field references before PostgreSQL COALESCE', () => {
328+
const sql = provider.convertFormulaToSelectQuery('AND({fldBoolean001})', buildContext());
329+
330+
expect(sql).toContain('(("main"."bool_col"))::boolean');
331+
expect(sql).toContain('COALESCE');
332+
});
333+
334+
it('does not cast non-boolean field references', () => {
335+
const sql = provider.convertFormulaToSelectQuery('AND({fldText001})', buildContext());
336+
337+
expect(sql).not.toContain('::boolean');
338+
});
339+
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ export class SelectQueryPostgres extends SelectQueryAbstract {
448448

449449
datetimeDiff(startDate: string, endDate: string, unit: string): string {
450450
const diffUnit = this.normalizeDiffUnit(unit.replace(/^'|'$/g, ''));
451-
const diffSeconds = `EXTRACT(EPOCH FROM (${this.tzWrap(endDate)} - ${this.tzWrap(startDate)}))`;
451+
const diffSeconds = `EXTRACT(EPOCH FROM (${this.tzWrap(startDate)} - ${this.tzWrap(endDate)}))`;
452452
switch (diffUnit) {
453453
case 'millisecond':
454454
return `(${diffSeconds}) * 1000`;

0 commit comments

Comments
 (0)