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