Skip to content

Commit 33ac729

Browse files
committed
fix: update field visitor methods to handle lookup fields correctly
1 parent 74b7c19 commit 33ac729

File tree

6 files changed

+210
-163
lines changed

6 files changed

+210
-163
lines changed

‎apps/nestjs-backend/src/db-provider/create-database-column-query/create-database-column-field-visitor.postgres.ts‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,11 @@ export class CreatePostgresDatabaseColumnFieldVisitor implements IFieldVisitor<v
379379
this.createFormulaColumns(field);
380380
}
381381

382-
visitCreatedTimeField(_field: CreatedTimeFieldCore): void {
382+
visitCreatedTimeField(field: CreatedTimeFieldCore): void {
383+
if (field.isLookup) {
384+
this.createStandardColumn(field);
385+
return;
386+
}
383387
this.context.table.specificType(
384388
this.context.dbFieldName,
385389
'TIMESTAMP GENERATED ALWAYS AS (__created_time) STORED'
@@ -389,7 +393,11 @@ export class CreatePostgresDatabaseColumnFieldVisitor implements IFieldVisitor<v
389393
});
390394
}
391395

392-
visitLastModifiedTimeField(_field: LastModifiedTimeFieldCore): void {
396+
visitLastModifiedTimeField(field: LastModifiedTimeFieldCore): void {
397+
if (field.isLookup) {
398+
this.createStandardColumn(field);
399+
return;
400+
}
393401
this.context.table.specificType(
394402
this.context.dbFieldName,
395403
'TIMESTAMP GENERATED ALWAYS AS (__last_modified_time) STORED'
@@ -404,7 +412,11 @@ export class CreatePostgresDatabaseColumnFieldVisitor implements IFieldVisitor<v
404412
this.createStandardColumn(field);
405413
}
406414

407-
visitCreatedByField(_field: CreatedByFieldCore): void {
415+
visitCreatedByField(field: CreatedByFieldCore): void {
416+
if (field.isLookup) {
417+
this.createStandardColumn(field);
418+
return;
419+
}
408420
// Persist as generated column that mirrors __created_by (TEXT)
409421
this.context.table.specificType(
410422
this.context.dbFieldName,
@@ -415,7 +427,11 @@ export class CreatePostgresDatabaseColumnFieldVisitor implements IFieldVisitor<v
415427
});
416428
}
417429

418-
visitLastModifiedByField(_field: LastModifiedByFieldCore): void {
430+
visitLastModifiedByField(field: LastModifiedByFieldCore): void {
431+
if (field.isLookup) {
432+
this.createStandardColumn(field);
433+
return;
434+
}
419435
// Persist as generated column that mirrors __last_modified_by (TEXT)
420436
this.context.table.specificType(
421437
this.context.dbFieldName,

‎apps/nestjs-backend/src/db-provider/create-database-column-query/create-database-column-field-visitor.sqlite.ts‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,11 @@ export class CreateSqliteDatabaseColumnFieldVisitor implements IFieldVisitor<voi
377377
this.createStandardColumn(field);
378378
}
379379

380-
visitCreatedTimeField(_field: CreatedTimeFieldCore): void {
380+
visitCreatedTimeField(field: CreatedTimeFieldCore): void {
381+
if (field.isLookup) {
382+
this.createStandardColumn(field);
383+
return;
384+
}
381385
const storageType = this.context.isNewTable ? 'STORED' : 'VIRTUAL';
382386
this.context.table.specificType(
383387
this.context.dbFieldName,
@@ -388,7 +392,11 @@ export class CreateSqliteDatabaseColumnFieldVisitor implements IFieldVisitor<voi
388392
});
389393
}
390394

391-
visitLastModifiedTimeField(_field: LastModifiedTimeFieldCore): void {
395+
visitLastModifiedTimeField(field: LastModifiedTimeFieldCore): void {
396+
if (field.isLookup) {
397+
this.createStandardColumn(field);
398+
return;
399+
}
392400
const storageType = this.context.isNewTable ? 'STORED' : 'VIRTUAL';
393401
this.context.table.specificType(
394402
this.context.dbFieldName,
@@ -404,7 +412,11 @@ export class CreateSqliteDatabaseColumnFieldVisitor implements IFieldVisitor<voi
404412
this.createStandardColumn(field);
405413
}
406414

407-
visitCreatedByField(_field: CreatedByFieldCore): void {
415+
visitCreatedByField(field: CreatedByFieldCore): void {
416+
if (field.isLookup) {
417+
this.createStandardColumn(field);
418+
return;
419+
}
408420
// Persist as generated column that mirrors __created_by (TEXT)
409421
const storageType = this.context.isNewTable ? 'STORED' : 'VIRTUAL';
410422
this.context.table.specificType(
@@ -416,7 +428,11 @@ export class CreateSqliteDatabaseColumnFieldVisitor implements IFieldVisitor<voi
416428
});
417429
}
418430

419-
visitLastModifiedByField(_field: LastModifiedByFieldCore): void {
431+
visitLastModifiedByField(field: LastModifiedByFieldCore): void {
432+
if (field.isLookup) {
433+
this.createStandardColumn(field);
434+
return;
435+
}
420436
// Persist as generated column that mirrors __last_modified_by (TEXT)
421437
const storageType = this.context.isNewTable ? 'STORED' : 'VIRTUAL';
422438
this.context.table.specificType(

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

Lines changed: 52 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ describe('SelectQueryPostgres unit-aware date helpers', () => {
2222
timeZone,
2323
});
2424

25+
const sanitizeTimestampInput = (expr: string) => `NULLIF(BTRIM((${expr})::text), '')`;
26+
const tzWrap = (expr: string, timeZone: string) => {
27+
const safeTz = timeZone.replace(/'/g, "''");
28+
return `(${sanitizeTimestampInput(expr)})::timestamptz AT TIME ZONE '${safeTz}'`;
29+
};
30+
const localWrap = (expr: string) => `(${sanitizeTimestampInput(expr)})::timestamp`;
31+
2532
it('left casts expressions to text before truncation', () => {
2633
expect(query.left('raw_expr', '5')).toBe(`LEFT((raw_expr)::text, 5::integer)`);
2734
});
@@ -38,123 +45,99 @@ describe('SelectQueryPostgres unit-aware date helpers', () => {
3845

3946
describe('timezone-aware wrappers', () => {
4047
let tzQuery: SelectQueryPostgres;
48+
const timeZone = 'Asia/Shanghai';
49+
const tz = (expr: string) => tzWrap(expr, timeZone);
4150

4251
beforeEach(() => {
4352
tzQuery = new SelectQueryPostgres();
44-
tzQuery.setContext(createTimezoneContext('Asia/Shanghai'));
53+
tzQuery.setContext(createTimezoneContext(timeZone));
4554
});
4655

4756
it('datestr wraps timezone-adjusted expressions before casting', () => {
48-
expect(tzQuery.datestr('date_col')).toBe(
49-
`((date_col)::timestamptz AT TIME ZONE 'Asia/Shanghai')::date::text`
50-
);
57+
expect(tzQuery.datestr('date_col')).toBe(`(${tz('date_col')})::date::text`);
5158
});
5259

5360
it('timestr wraps timezone-adjusted expressions before casting', () => {
54-
expect(tzQuery.timestr('date_col')).toBe(
55-
`((date_col)::timestamptz AT TIME ZONE 'Asia/Shanghai')::time::text`
56-
);
61+
expect(tzQuery.timestr('date_col')).toBe(`(${tz('date_col')})::time::text`);
5762
});
5863

5964
it('workday casts after timezone normalization', () => {
6065
expect(tzQuery.workday('start_col', '5')).toBe(
61-
`((start_col)::timestamptz AT TIME ZONE 'Asia/Shanghai')::date + INTERVAL '5 days'`
66+
`(${tz('start_col')})::date + INTERVAL '5 days'`
6267
);
6368
});
6469

6570
it('dateAdd uses timezone-normalized base expression', () => {
6671
expect(tzQuery.dateAdd('date_col', '2', `'day'`)).toBe(
67-
`(date_col)::timestamptz AT TIME ZONE 'Asia/Shanghai' + ((2)) * INTERVAL '1 day'`
72+
`${tz('date_col')} + ((2)) * INTERVAL '1 day'`
6873
);
6974
});
7075

7176
it('day extracts day after timezone normalization', () => {
72-
expect(tzQuery.day('date_col')).toBe(
73-
`EXTRACT(DAY FROM (date_col)::timestamptz AT TIME ZONE 'Asia/Shanghai')::int`
74-
);
77+
expect(tzQuery.day('date_col')).toBe(`EXTRACT(DAY FROM ${tz('date_col')})::int`);
7578
});
7679

7780
it('datetimeFormat formats timezone-normalized timestamp', () => {
78-
expect(tzQuery.datetimeFormat('date_col', `'%Y'`)).toBe(
79-
`TO_CHAR((date_col)::timestamptz AT TIME ZONE 'Asia/Shanghai', '%Y')`
80-
);
81+
expect(tzQuery.datetimeFormat('date_col', `'%Y'`)).toBe(`TO_CHAR(${tz('date_col')}, '%Y')`);
8182
});
8283

8384
it('isAfter compares timezone-normalized expressions', () => {
84-
expect(tzQuery.isAfter('date_a', 'date_b')).toBe(
85-
`(date_a)::timestamptz AT TIME ZONE 'Asia/Shanghai' > (date_b)::timestamptz AT TIME ZONE 'Asia/Shanghai'`
86-
);
85+
expect(tzQuery.isAfter('date_a', 'date_b')).toBe(`${tz('date_a')} > ${tz('date_b')}`);
8786
});
8887

8988
it('isBefore compares timezone-normalized expressions', () => {
90-
expect(tzQuery.isBefore('date_a', 'date_b')).toBe(
91-
`(date_a)::timestamptz AT TIME ZONE 'Asia/Shanghai' < (date_b)::timestamptz AT TIME ZONE 'Asia/Shanghai'`
92-
);
89+
expect(tzQuery.isBefore('date_a', 'date_b')).toBe(`${tz('date_a')} < ${tz('date_b')}`);
9390
});
9491

9592
it('isSame normalizes unit comparisons after timezone conversion', () => {
9693
expect(tzQuery.isSame('date_a', 'date_b', `'hour'`)).toBe(
97-
`DATE_TRUNC('hour', (date_a)::timestamptz AT TIME ZONE 'Asia/Shanghai') = DATE_TRUNC('hour', (date_b)::timestamptz AT TIME ZONE 'Asia/Shanghai')`
94+
`DATE_TRUNC('hour', ${tz('date_a')}) = DATE_TRUNC('hour', ${tz('date_b')})`
9895
);
9996
});
10097

10198
it('hour extracts hour after timezone normalization', () => {
102-
expect(tzQuery.hour('date_col')).toBe(
103-
`EXTRACT(HOUR FROM (date_col)::timestamptz AT TIME ZONE 'Asia/Shanghai')::int`
104-
);
99+
expect(tzQuery.hour('date_col')).toBe(`EXTRACT(HOUR FROM ${tz('date_col')})::int`);
105100
});
106101

107102
it('minute extracts minute after timezone normalization', () => {
108-
expect(tzQuery.minute('date_col')).toBe(
109-
`EXTRACT(MINUTE FROM (date_col)::timestamptz AT TIME ZONE 'Asia/Shanghai')::int`
110-
);
103+
expect(tzQuery.minute('date_col')).toBe(`EXTRACT(MINUTE FROM ${tz('date_col')})::int`);
111104
});
112105

113106
it('second extracts second after timezone normalization', () => {
114-
expect(tzQuery.second('date_col')).toBe(
115-
`EXTRACT(SECOND FROM (date_col)::timestamptz AT TIME ZONE 'Asia/Shanghai')::int`
116-
);
107+
expect(tzQuery.second('date_col')).toBe(`EXTRACT(SECOND FROM ${tz('date_col')})::int`);
117108
});
118109

119110
it('month extracts month after timezone normalization', () => {
120-
expect(tzQuery.month('date_col')).toBe(
121-
`EXTRACT(MONTH FROM (date_col)::timestamptz AT TIME ZONE 'Asia/Shanghai')::int`
122-
);
111+
expect(tzQuery.month('date_col')).toBe(`EXTRACT(MONTH FROM ${tz('date_col')})::int`);
123112
});
124113

125114
it('year extracts year after timezone normalization', () => {
126-
expect(tzQuery.year('date_col')).toBe(
127-
`EXTRACT(YEAR FROM (date_col)::timestamptz AT TIME ZONE 'Asia/Shanghai')::int`
128-
);
115+
expect(tzQuery.year('date_col')).toBe(`EXTRACT(YEAR FROM ${tz('date_col')})::int`);
129116
});
130117

131118
it('weekNum extracts week number after timezone normalization', () => {
132-
expect(tzQuery.weekNum('date_col')).toBe(
133-
`EXTRACT(WEEK FROM (date_col)::timestamptz AT TIME ZONE 'Asia/Shanghai')::int`
134-
);
119+
expect(tzQuery.weekNum('date_col')).toBe(`EXTRACT(WEEK FROM ${tz('date_col')})::int`);
135120
});
136121

137122
it('weekday extracts day of week after timezone normalization', () => {
138-
expect(tzQuery.weekday('date_col')).toBe(
139-
`EXTRACT(DOW FROM (date_col)::timestamptz AT TIME ZONE 'Asia/Shanghai')::int`
140-
);
123+
expect(tzQuery.weekday('date_col')).toBe(`EXTRACT(DOW FROM ${tz('date_col')})::int`);
141124
});
142125

143126
it('toNow computes epoch difference using timezone context', () => {
144127
expect(tzQuery.toNow('date_col')).toBe(
145-
`EXTRACT(EPOCH FROM ((date_col)::timestamptz AT TIME ZONE 'Asia/Shanghai' - (NOW() AT TIME ZONE 'Asia/Shanghai')))`
128+
`EXTRACT(EPOCH FROM (${tz('date_col')} - (NOW() AT TIME ZONE '${timeZone}')))`
146129
);
147130
});
148131

149132
it('datetimeDiff subtracts timezone-normalized expressions', () => {
150133
expect(tzQuery.datetimeDiff('start_col', 'end_col', `'day'`)).toBe(
151-
`(EXTRACT(EPOCH FROM ((end_col)::timestamptz AT TIME ZONE 'Asia/Shanghai' - (start_col)::timestamptz AT TIME ZONE 'Asia/Shanghai'))) / 86400`
134+
`(EXTRACT(EPOCH FROM (${tz('end_col')} - ${tz('start_col')}))) / 86400`
152135
);
153136
});
154137

155138
it('fromNow uses timezone-aware current timestamp', () => {
156139
expect(tzQuery.fromNow('date_col')).toBe(
157-
`EXTRACT(EPOCH FROM ((NOW() AT TIME ZONE 'Asia/Shanghai') - (date_col)::timestamptz AT TIME ZONE 'Asia/Shanghai'))`
140+
`EXTRACT(EPOCH FROM ((NOW() AT TIME ZONE '${timeZone}') - ${tz('date_col')}))`
158141
);
159142
});
160143

@@ -163,7 +146,7 @@ describe('SelectQueryPostgres unit-aware date helpers', () => {
163146
customTzQuery.setContext(createTimezoneContext("America/St_John's"));
164147

165148
expect(customTzQuery.datestr('date_col')).toBe(
166-
`((date_col)::timestamptz AT TIME ZONE 'America/St_John''s')::date::text`
149+
`(${tzWrap('date_col', "America/St_John's")})::date::text`
167150
);
168151
});
169152
});
@@ -199,88 +182,30 @@ describe('SelectQueryPostgres unit-aware date helpers', () => {
199182
it.each(dateAddCases)('dateAdd normalizes unit "%s" to "%s"', ({ literal, unit, factor }) => {
200183
const sql = query.dateAdd('date_col', 'count_expr', `'${literal}'`);
201184
const scaled = factor === 1 ? '(count_expr)' : `(count_expr) * ${factor}`;
202-
expect(sql).toBe(`(date_col)::timestamp + (${scaled}) * INTERVAL '1 ${unit}'`);
185+
expect(sql).toBe(`${localWrap('date_col')} + (${scaled}) * INTERVAL '1 ${unit}'`);
203186
});
204187

188+
const localDiffBase = `(EXTRACT(EPOCH FROM (${localWrap('date_end')} - ${localWrap('date_start')})))`;
205189
const datetimeDiffCases: Array<{ literal: string; expected: string }> = [
206-
{
207-
literal: 'millisecond',
208-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp))) * 1000',
209-
},
210-
{
211-
literal: 'milliseconds',
212-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp))) * 1000',
213-
},
214-
{
215-
literal: 'ms',
216-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp))) * 1000',
217-
},
218-
{
219-
literal: 'second',
220-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp)))',
221-
},
222-
{
223-
literal: 'seconds',
224-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp)))',
225-
},
226-
{
227-
literal: 'sec',
228-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp)))',
229-
},
230-
{
231-
literal: 'secs',
232-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp)))',
233-
},
234-
{
235-
literal: 'minute',
236-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp))) / 60',
237-
},
238-
{
239-
literal: 'minutes',
240-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp))) / 60',
241-
},
242-
{
243-
literal: 'min',
244-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp))) / 60',
245-
},
246-
{
247-
literal: 'mins',
248-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp))) / 60',
249-
},
250-
{
251-
literal: 'hour',
252-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp))) / 3600',
253-
},
254-
{
255-
literal: 'hours',
256-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp))) / 3600',
257-
},
258-
{
259-
literal: 'hr',
260-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp))) / 3600',
261-
},
262-
{
263-
literal: 'hrs',
264-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp))) / 3600',
265-
},
266-
{
267-
literal: 'week',
268-
expected:
269-
'(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp))) / (86400 * 7)',
270-
},
271-
{
272-
literal: 'weeks',
273-
expected:
274-
'(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp))) / (86400 * 7)',
275-
},
276-
{
277-
literal: 'day',
278-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp))) / 86400',
279-
},
280-
{
281-
literal: 'days',
282-
expected: '(EXTRACT(EPOCH FROM ((date_end)::timestamp - (date_start)::timestamp))) / 86400',
283-
},
190+
{ literal: 'millisecond', expected: `${localDiffBase} * 1000` },
191+
{ literal: 'milliseconds', expected: `${localDiffBase} * 1000` },
192+
{ literal: 'ms', expected: `${localDiffBase} * 1000` },
193+
{ literal: 'second', expected: `${localDiffBase}` },
194+
{ literal: 'seconds', expected: `${localDiffBase}` },
195+
{ literal: 'sec', expected: `${localDiffBase}` },
196+
{ literal: 'secs', expected: `${localDiffBase}` },
197+
{ literal: 'minute', expected: `${localDiffBase} / 60` },
198+
{ literal: 'minutes', expected: `${localDiffBase} / 60` },
199+
{ literal: 'min', expected: `${localDiffBase} / 60` },
200+
{ literal: 'mins', expected: `${localDiffBase} / 60` },
201+
{ literal: 'hour', expected: `${localDiffBase} / 3600` },
202+
{ literal: 'hours', expected: `${localDiffBase} / 3600` },
203+
{ literal: 'hr', expected: `${localDiffBase} / 3600` },
204+
{ literal: 'hrs', expected: `${localDiffBase} / 3600` },
205+
{ literal: 'week', expected: `${localDiffBase} / (86400 * 7)` },
206+
{ literal: 'weeks', expected: `${localDiffBase} / (86400 * 7)` },
207+
{ literal: 'day', expected: `${localDiffBase} / 86400` },
208+
{ literal: 'days', expected: `${localDiffBase} / 86400` },
284209
];
285210

286211
it.each(datetimeDiffCases)('datetimeDiff normalizes unit "%s"', ({ literal, expected }) => {
@@ -319,7 +244,7 @@ describe('SelectQueryPostgres unit-aware date helpers', () => {
319244
it.each(isSameCases)('isSame normalizes unit "%s"', ({ literal, expectedUnit }) => {
320245
const sql = query.isSame('date_a', 'date_b', `'${literal}'`);
321246
expect(sql).toBe(
322-
`DATE_TRUNC('${expectedUnit}', (date_a)::timestamp) = DATE_TRUNC('${expectedUnit}', (date_b)::timestamp)`
247+
`DATE_TRUNC('${expectedUnit}', ${localWrap('date_a')}) = DATE_TRUNC('${expectedUnit}', ${localWrap('date_b')})`
323248
);
324249
});
325250

0 commit comments

Comments
 (0)