| 1 | // vtest build: !windows && !musl? |
| 2 | module pg |
| 3 | |
| 4 | import orm |
| 5 | import time |
| 6 | |
| 7 | // These tests exercise the PostgreSQL TIMESTAMP/TIMESTAMPTZ text decoder |
| 8 | // directly, so they do not require a running PostgreSQL server. |
| 9 | |
| 10 | fn 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 | |
| 21 | fn 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 | |
| 29 | fn 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 | |
| 41 | fn 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 | |
| 49 | fn 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 | |
| 56 | fn 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 | |
| 63 | fn 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 | |
| 70 | fn 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 | |
| 78 | fn 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 | |
| 84 | fn 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 | |
| 92 | fn 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 | |
| 100 | fn 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 | |
| 108 | fn 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 | |
| 116 | fn 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 | |
| 122 | fn 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 | |
| 129 | fn 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 | |
| 139 | fn 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 | |
| 149 | fn 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 | |
| 157 | fn 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 | |
| 167 | fn 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 | |
| 177 | fn 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 | |
| 187 | fn 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 | |
| 197 | fn 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 | |
| 206 | fn 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 | |