v4 / vlib / db / pg / pg_timestamp_test.v
221 lines · 195 sloc · 6.41 KB · cc39afe7d730cd7b4edc16a2802f3430495fa4a3
Raw
1module pg
2
3import orm
4import time
5
6// These tests exercise the PostgreSQL TIMESTAMP/TIMESTAMPTZ text decoder
7// directly, so they do not require a running PostgreSQL server.
8
9fn test_plain_timestamp() {
10 t := pg_parse_timestamp('2024-01-15 14:00:00')!
11 assert t.year == 2024
12 assert t.month == 1
13 assert t.day == 15
14 assert t.hour == 14
15 assert t.minute == 0
16 assert t.second == 0
17 assert t.nanosecond == 0
18}
19
20fn test_fractional_seconds_microsecond_precision() {
21 t := pg_parse_timestamp('2024-01-15 14:00:00.123456')!
22 assert t.hour == 14
23 assert t.second == 0
24 // 123456 microseconds preserved as nanoseconds
25 assert t.nanosecond == 123456000
26}
27
28fn test_timestamptz_utc_offset() {
29 // `+00` offset -> already UTC, value unchanged.
30 t := pg_parse_timestamp('2024-01-15 13:00:00.123456+00')!
31 assert t.year == 2024
32 assert t.month == 1
33 assert t.day == 15
34 assert t.hour == 13
35 assert t.minute == 0
36 assert t.second == 0
37 assert t.nanosecond == 123456000
38}
39
40fn test_timestamptz_positive_offset_normalized_to_utc() {
41 // 14:00 at +01:00 -> 13:00 UTC
42 t := pg_parse_timestamp('2024-01-15 14:00:00.123456+01:00')!
43 assert t.hour == 13
44 assert t.minute == 0
45 assert t.nanosecond == 123456000
46}
47
48fn test_timestamptz_positive_short_offset() {
49 // 14:00 at +02 -> 12:00 UTC
50 t := pg_parse_timestamp('2024-01-15 14:00:00+02')!
51 assert t.hour == 12
52 assert t.minute == 0
53}
54
55fn test_timestamptz_offset_with_minutes() {
56 // 14:00 at +02:30 -> 11:30 UTC
57 t := pg_parse_timestamp('2024-01-15 14:00:00+02:30')!
58 assert t.hour == 11
59 assert t.minute == 30
60}
61
62fn test_timestamptz_negative_offset_normalized_to_utc() {
63 // 14:00 at -05:00 -> 19:00 UTC
64 t := pg_parse_timestamp('2024-01-15 14:00:00-05')!
65 assert t.hour == 19
66 assert t.minute == 0
67}
68
69fn test_timestamptz_negative_offset_with_minutes_day_rollover() {
70 // 23:00 at -05:30 -> 04:30 UTC next day
71 t := pg_parse_timestamp('2024-01-15 23:00:00-05:30')!
72 assert t.day == 16
73 assert t.hour == 4
74 assert t.minute == 30
75}
76
77fn test_timestamptz_z_suffix() {
78 t := pg_parse_timestamp('2024-01-15 14:00:00.5Z')!
79 assert t.hour == 14
80 assert t.nanosecond == 500000000
81}
82
83fn test_infinity_returns_clear_error() {
84 pg_parse_timestamp('infinity') or {
85 assert err.msg().contains('infinity')
86 return
87 }
88 assert false, 'expected an error for the special value `infinity`'
89}
90
91fn test_negative_infinity_returns_clear_error() {
92 pg_parse_timestamp('-infinity') or {
93 assert err.msg().contains('infinity')
94 return
95 }
96 assert false, 'expected an error for the special value `-infinity`'
97}
98
99fn test_bc_suffix_returns_clear_error() {
100 pg_parse_timestamp('0001-01-01 00:00:00 BC') or {
101 assert err.msg().contains('BC')
102 return
103 }
104 assert false, 'expected an error for a BC timestamp value'
105}
106
107fn test_bc_suffix_with_offset_returns_clear_error() {
108 pg_parse_timestamp('0044-03-15 12:00:00+00 BC') or {
109 assert err.msg().contains('BC')
110 return
111 }
112 assert false, 'expected an error for a BC TIMESTAMPTZ value'
113}
114
115fn test_garbage_seconds_suffix_is_rejected() {
116 // `string.int()` would silently keep the `00` prefix; strict parsing must reject it.
117 pg_parse_timestamp('2024-01-15 14:00:00 XY') or { return }
118 assert false, 'expected an error for a trailing non-numeric suffix'
119}
120
121fn test_unix_timestamp_path_decodes_integer() {
122 // Mirror the ORM fallback: a bare integer string is a Unix timestamp.
123 p := val_to_primitive('1700000000', orm.time_)!
124 t := p as time.Time
125 assert t.unix() == 1700000000
126}
127
128fn test_infinity_via_val_to_primitive_errors() {
129 // `infinity` has no date/time punctuation; it must still reach the parser and
130 // error instead of decoding to the Unix epoch.
131 val_to_primitive('infinity', orm.time_) or {
132 assert err.msg().contains('infinity')
133 return
134 }
135 assert false, 'expected an error decoding `infinity` via val_to_primitive'
136}
137
138fn test_year_out_of_range_returns_error_not_panic() {
139 // PostgreSQL accepts years past 9999, which `time.new` cannot represent. The
140 // decoder must return an error rather than panicking.
141 pg_parse_timestamp('10000-01-01 00:00:00') or {
142 assert err.msg().contains('out of range')
143 return
144 }
145 assert false, "expected an error for a year beyond V's supported range"
146}
147
148fn test_out_of_range_via_val_to_primitive_errors() {
149 val_to_primitive('10000-01-01 00:00:00', orm.time_) or {
150 assert err.msg().contains('out of range')
151 return
152 }
153 assert false, 'expected an error decoding an out-of-range timestamp'
154}
155
156fn test_offset_pushes_year_out_of_range_returns_error() {
157 // A valid local timestamp that normalizes to year 10000 in UTC must still error,
158 // not silently bypass the range guard via the offset shift.
159 pg_parse_timestamp('9999-12-31 23:30:00-01') or {
160 assert err.msg().contains('out of range')
161 return
162 }
163 assert false, 'expected an error when the UTC offset pushes the year out of range'
164}
165
166fn test_offset_within_range_still_decodes() {
167 // A near-boundary value that stays in range after normalization must succeed.
168 t := pg_parse_timestamp('9999-12-31 22:30:00-01')!
169 assert t.year == 9999
170 assert t.month == 12
171 assert t.day == 31
172 assert t.hour == 23
173 assert t.minute == 30
174}
175
176fn test_offset_normalizes_into_bc_returns_error() {
177 // `0001-01-01 00:30:00+01` normalizes to `0000-12-31 23:30:00` (year 0 == 1 BC),
178 // which is unrepresentable and must be rejected rather than silently accepted.
179 pg_parse_timestamp('0001-01-01 00:30:00+01') or {
180 assert err.msg().contains('BC')
181 return
182 }
183 assert false, 'expected an error when the UTC offset normalizes into a BC/year-0 date'
184}
185
186fn test_lower_boundary_offset_within_range_still_decodes() {
187 // `0001-01-01 01:30:00+01` normalizes to `0001-01-01 00:30:00` (year 1), still valid.
188 t := pg_parse_timestamp('0001-01-01 01:30:00+01')!
189 assert t.year == 1
190 assert t.month == 1
191 assert t.day == 1
192 assert t.hour == 0
193 assert t.minute == 30
194}
195
196fn test_year_zero_literal_returns_bc_error() {
197 // A direct year-0 value (no ` BC` suffix) is still unrepresentable.
198 pg_parse_timestamp('0000-01-01 00:00:00') or {
199 assert err.msg().contains('BC')
200 return
201 }
202 assert false, 'expected a BC/year-0 error for a literal year-0 timestamp'
203}
204
205fn test_issue_27556_example() {
206 // The exact value from the issue: stored as '2024-01-15 14:00:00.123456+01:00'
207 // with the session in UTC, PostgreSQL returns it as below.
208 t := pg_parse_timestamp('2024-01-15 13:00:00.123456+00')!
209 expected := time.new(
210 year: 2024
211 month: 1
212 day: 15
213 hour: 13
214 minute: 0
215 second: 0
216 nanosecond: 123456000
217 is_local: false
218 )
219 assert t.unix() == expected.unix()
220 assert t.nanosecond == expected.nanosecond
221}
222