| 1 | module pg |
| 2 | |
| 3 | import orm |
| 4 | import time |
| 5 | |
| 6 | // These tests exercise the PostgreSQL TIMESTAMP/TIMESTAMPTZ text decoder |
| 7 | // directly, so they do not require a running PostgreSQL server. |
| 8 | |
| 9 | fn 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 | |
| 20 | fn 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 | |
| 28 | fn 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 | |
| 40 | fn 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 | |
| 48 | fn 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 | |
| 55 | fn 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 | |
| 62 | fn 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 | |
| 69 | fn 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 | |
| 77 | fn 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 | |
| 83 | fn 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 | |
| 91 | fn 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 | |
| 99 | fn 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 | |
| 107 | fn 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 | |
| 115 | fn 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 | |
| 121 | fn 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 | |
| 128 | fn 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 | |
| 138 | fn 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 | |
| 148 | fn 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 | |
| 156 | fn 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 | |
| 166 | fn 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 | |
| 176 | fn 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 | |
| 186 | fn 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 | |
| 196 | fn 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 | |
| 205 | fn 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 | |