Skip to content

Commit efbc5e2

Browse files
authored
fix(postgres)!: add missing TO_CHAR format tokens (Mon, Month, Day, Dy, AM/PM, HH) (#7477)
Add bare (non-TM-prefixed) month names, weekday names, AM/PM meridiem indicators, and bare HH to Postgres TIME_MAPPING. These tokens are standard PostgreSQL TO_CHAR format specifiers but were not being converted during transpilation, causing issues when targeting dialects like ClickHouse. Previously, `Day` was mangled to `%uay` because the single-char `D` rule matched first. The trie correctly handles longer matches, so `Day` → `%A`, `Dy` → `%a` now take precedence over `D` → `%u`. Add INVERSE_TIME_MAPPING to prefer the canonical bare forms (Mon, Day, Dy, Month, AM, HH12) when generating Postgres SQL from the internal strftime representation. Fixes #7476
1 parent 3a76d5f commit efbc5e2

3 files changed

Lines changed: 91 additions & 3 deletions

File tree

sqlglot/dialects/postgres.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,48 @@ class Postgres(Dialect):
2121
}
2222

2323
TIME_MAPPING = {
24+
"AM": "%p", # AM/PM meridiem indicator
25+
"A.M.": "%p", # AM/PM with periods
2426
"d": "%u", # 1-based day of week
2527
"D": "%u", # 1-based day of week
28+
"day": "%A", # full weekday name (lowercase)
29+
"Day": "%A", # full weekday name (capitalized)
30+
"DAY": "%A", # full weekday name (uppercase)
2631
"dd": "%d", # day of month
2732
"DD": "%d", # day of month
2833
"ddd": "%j", # zero padded day of year
2934
"DDD": "%j", # zero padded day of year
35+
"dy": "%a", # abbreviated weekday name (lowercase)
36+
"Dy": "%a", # abbreviated weekday name (capitalized)
37+
"DY": "%a", # abbreviated weekday name (uppercase)
3038
"FMDD": "%-d", # - is no leading zero for Python; same for FM in postgres
3139
"FMDDD": "%-j", # day of year
3240
"FMHH12": "%-I", # 9
3341
"FMHH24": "%-H", # 9
3442
"FMMI": "%-M", # Minute
3543
"FMMM": "%-m", # 1
3644
"FMSS": "%-S", # Second
45+
"hh": "%I", # 12-hour (HH defaults to 12-hour in PG)
46+
"HH": "%I", # 12-hour (HH defaults to 12-hour in PG)
3747
"HH12": "%I", # 09
3848
"HH24": "%H", # 09
3949
"mi": "%M", # zero padded minute
4050
"MI": "%M", # zero padded minute
4151
"mm": "%m", # 01
4252
"MM": "%m", # 01
53+
"mon": "%b", # abbreviated month name (lowercase)
54+
"Mon": "%b", # abbreviated month name (capitalized)
55+
"MON": "%b", # abbreviated month name (uppercase)
56+
"month": "%B", # full month name (lowercase)
57+
"Month": "%B", # full month name (capitalized)
58+
"MONTH": "%B", # full month name (uppercase)
4359
"OF": "%z", # utc offset
60+
"PM": "%p", # PG treats AM/PM as synonymous; both print actual meridiem
61+
"P.M.": "%p",
62+
"am": "%p",
63+
"a.m.": "%p",
64+
"pm": "%p",
65+
"p.m.": "%p",
4466
"ss": "%S", # zero padded second
4567
"SS": "%S", # zero padded second
4668
"TMDay": "%A", # TM is locale dependent
@@ -57,6 +79,17 @@ class Postgres(Dialect):
5779
"YYYY": "%Y", # 2015
5880
}
5981

82+
# Prefer bare forms (Mon, Day, Dy, Month) over TM-prefixed forms when
83+
# generating Postgres SQL from the internal strftime representation.
84+
INVERSE_TIME_MAPPING = {
85+
"%A": "Day",
86+
"%a": "Dy",
87+
"%b": "Mon",
88+
"%B": "Month",
89+
"%I": "HH12",
90+
"%p": "AM",
91+
}
92+
6093
class Tokenizer(tokens.Tokenizer):
6194
BIT_STRINGS = [("b'", "'"), ("B'", "'")]
6295
HEX_STRINGS = [("x'", "'"), ("X'", "'")]

tests/dialects/test_exasol.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ def test_datetime_functions(self):
464464
write={
465465
"exasol": "SELECT TO_CHAR(CAST('2024-07-08 13:45:00' AS TIMESTAMP), 'DY')",
466466
"oracle": "SELECT TO_CHAR(CAST('2024-07-08 13:45:00' AS TIMESTAMP), 'DY')",
467-
"postgres": "SELECT TO_CHAR(CAST('2024-07-08 13:45:00' AS TIMESTAMP), 'TMDy')",
467+
"postgres": "SELECT TO_CHAR(CAST('2024-07-08 13:45:00' AS TIMESTAMP), 'Dy')",
468468
"databricks": "SELECT DATE_FORMAT(CAST('2024-07-08 13:45:00' AS TIMESTAMP), 'EEE')",
469469
},
470470
)

tests/dialects/test_postgres.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,12 @@ def test_postgres(self):
152152
"SELECT TO_TIMESTAMP(1284352323.5), TO_TIMESTAMP('05 Dec 2000', 'DD Mon YYYY')"
153153
)
154154
self.validate_identity(
155-
"SELECT TO_TIMESTAMP('05 Dec 2000 10:00 AM', 'DD Mon YYYY HH:MI AM')"
155+
"SELECT TO_TIMESTAMP('05 Dec 2000 10:00 AM', 'DD Mon YYYY HH:MI AM')",
156+
"SELECT TO_TIMESTAMP('05 Dec 2000 10:00 AM', 'DD Mon YYYY HH12:MI AM')",
156157
)
157158
self.validate_identity(
158-
"SELECT TO_TIMESTAMP('05 Dec 2000 10:00 PM', 'DD Mon YYYY HH:MI PM')"
159+
"SELECT TO_TIMESTAMP('05 Dec 2000 10:00 PM', 'DD Mon YYYY HH:MI PM')",
160+
"SELECT TO_TIMESTAMP('05 Dec 2000 10:00 PM', 'DD Mon YYYY HH12:MI AM')",
159161
)
160162
self.validate_identity(
161163
"SELECT * FROM foo, LATERAL (SELECT * FROM bar WHERE bar.id = foo.bar_id) AS ss"
@@ -1020,6 +1022,59 @@ def test_postgres(self):
10201022
"redshift": "SELECT TO_CHAR(foo, bar)",
10211023
},
10221024
)
1025+
1026+
# TO_CHAR format token conversions: month names, weekday names, AM/PM, HH
1027+
self.validate_all(
1028+
"SELECT TO_CHAR(dt, 'Mon YYYY')",
1029+
write={
1030+
"clickhouse": "SELECT formatDateTime(dt, '%b %Y')",
1031+
"postgres": "SELECT TO_CHAR(dt, 'Mon YYYY')",
1032+
},
1033+
)
1034+
self.validate_all(
1035+
"SELECT TO_CHAR(dt, 'Month YYYY')",
1036+
write={
1037+
"clickhouse": "SELECT formatDateTime(dt, '%B %Y')",
1038+
"postgres": "SELECT TO_CHAR(dt, 'Month YYYY')",
1039+
},
1040+
)
1041+
self.validate_all(
1042+
"SELECT TO_CHAR(dt, 'Day')",
1043+
write={
1044+
"clickhouse": "SELECT formatDateTime(dt, '%A')",
1045+
"postgres": "SELECT TO_CHAR(dt, 'Day')",
1046+
},
1047+
)
1048+
self.validate_all(
1049+
"SELECT TO_CHAR(dt, 'Dy')",
1050+
write={
1051+
"clickhouse": "SELECT formatDateTime(dt, '%a')",
1052+
"postgres": "SELECT TO_CHAR(dt, 'Dy')",
1053+
},
1054+
)
1055+
self.validate_all(
1056+
"SELECT TO_CHAR(dt, 'HH12:MI AM')",
1057+
write={
1058+
"clickhouse": "SELECT formatDateTime(dt, '%I:%M %p')",
1059+
"postgres": "SELECT TO_CHAR(dt, 'HH12:MI AM')",
1060+
},
1061+
)
1062+
self.validate_all(
1063+
"SELECT TO_CHAR(dt, 'DD Mon YYYY HH24:MI')",
1064+
write={
1065+
"clickhouse": "SELECT formatDateTime(dt, '%d %b %Y %H:%M')",
1066+
"postgres": "SELECT TO_CHAR(dt, 'DD Mon YYYY HH24:MI')",
1067+
},
1068+
)
1069+
# Bare HH (no 12/24 suffix) defaults to 12-hour in PostgreSQL
1070+
self.validate_all(
1071+
"SELECT TO_CHAR(dt, 'HH:MI')",
1072+
write={
1073+
"clickhouse": "SELECT formatDateTime(dt, '%I:%M')",
1074+
"postgres": "SELECT TO_CHAR(dt, 'HH12:MI')",
1075+
},
1076+
)
1077+
10231078
self.validate_all(
10241079
"CREATE TABLE table1 (a INT, b INT, PRIMARY KEY (a))",
10251080
read={

0 commit comments

Comments
 (0)