@@ -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