From bbbfcce337ae1045ff403fae088f1d70651cb9f9 Mon Sep 17 00:00:00 2001 From: Liang Jingyi <124090346@link.cuhk.edu.cn> Date: Fri, 23 Oct 2026 02:34:39 +0800 Subject: [PATCH] Fix explicit AS aliases for current date and time scalars --- CHANGELOG | 3 ++- sqlparse/engine/grouping.py | 4 +++- tests/test_format.py | 33 +++++++++++++++++++++++++++++++++ tests/test_grouping.py | 22 ++++++++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 44d5938e..06d13b51 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,8 @@ Development Version ------------------- -Nothing yet. +* Fix SELECT-list alignment for CURRENT_DATE, CURRENT_TIME, and + CURRENT_TIMESTAMP with explicit AS aliases (issue550). Release 0.6.0 (Aug 13, 2026) diff --git a/sqlparse/engine/grouping.py b/sqlparse/engine/grouping.py index d8cfa9e1..29294d09 100644 --- a/sqlparse/engine/grouping.py +++ b/sqlparse/engine/grouping.py @@ -196,7 +196,9 @@ def match(token): return token.is_keyword and token.normalized == 'AS' def valid_prev(token): - return token.normalized == 'NULL' or not token.is_keyword + return (token.normalized in ('NULL', 'CURRENT_DATE', 'CURRENT_TIME', + 'CURRENT_TIMESTAMP') + or not token.is_keyword) def valid_next(token): ttypes = T.DML, T.DDL, T.CTE diff --git a/tests/test_format.py b/tests/test_format.py index 93495067..f048f807 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -491,6 +491,39 @@ def test_identifier_list(self): 'from a,', ' b']) + @pytest.mark.parametrize('date', [ + 'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP', + ]) + @pytest.mark.parametrize('case', [str.upper, str.lower, str.title]) + def test_date_scalar_as_alias(self, date, case): + # issue550 + aliased = f'{case(date)} {case("AS")} CREATED_AT' + s = (f"SELECT 'value 1' AS VALUE_1, {aliased}, " + "'testing' AS VALUE_2, 'testing2' AS VALUE_3 FROM TABLE_1") + expected = '\n'.join([ + "SELECT 'value 1' AS VALUE_1,", + f' {aliased},', + " 'testing' AS VALUE_2,", + " 'testing2' AS VALUE_3", + 'FROM TABLE_1', + ]) + assert sqlparse.format(s, reindent=True) == expected + assert sqlparse.format(expected, reindent=True) == expected + + def test_date_scalar_as_alias_trailing_comma(self): + # Preserve the trailing comma in issue550's reported output separately + # from the valid SELECT-list regression above. + s = ("SELECT 'value 1' AS VALUE_1, CURRENT_DATE AS CREATED_AT, " + "'testing' AS VALUE_2, 'testing2' AS VALUE_3, FROM TABLE_1") + assert str(sqlparse.parse(s)[0]) == s + assert sqlparse.format(s, reindent=True) == '\n'.join([ + "SELECT 'value 1' AS VALUE_1,", + ' CURRENT_DATE AS CREATED_AT,', + " 'testing' AS VALUE_2,", + " 'testing2' AS VALUE_3,", + 'FROM TABLE_1', + ]) + def test_identifier_list_with_wrap_after(self): f = lambda sql: sqlparse.format(sql, reindent=True, wrap_after=14) s = 'select foo, bar, baz from table1, table2 where 1 = 2' diff --git a/tests/test_grouping.py b/tests/test_grouping.py index 20fab9b7..b9cb29f5 100644 --- a/tests/test_grouping.py +++ b/tests/test_grouping.py @@ -332,6 +332,28 @@ def test_grouping_alias(): assert p.tokens[4].get_alias() == 'view' +@pytest.mark.parametrize('expression', [ + 'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP', 'NULL', 'created_on', +]) +@pytest.mark.parametrize('case', [str.upper, str.lower, str.title]) +def test_grouping_date_scalar_as_alias(expression, case): + # issue550: the scalar and its explicit alias must stay in one projection. + aliased = f'{case(expression)}\t{case("AS")}\n CREATED_AT' + s = (f"SELECT 'value 1' AS VALUE_1, {aliased}, " + "'testing' AS VALUE_2, 'testing2' AS VALUE_3 FROM TABLE_1") + p = sqlparse.parse(s)[0] + assert str(p) == s + assert isinstance(p.tokens[2], sql.IdentifierList) + identifiers = list(p.tokens[2].get_identifiers()) + assert [str(item) for item in identifiers] == [ + "'value 1' AS VALUE_1", aliased, + "'testing' AS VALUE_2", "'testing2' AS VALUE_3", + ] + assert [item.get_alias() for item in identifiers] == [ + 'VALUE_1', 'CREATED_AT', 'VALUE_2', 'VALUE_3', + ] + + def test_grouping_alias_case(): # see issue46 p = sqlparse.parse('CASE WHEN 1 THEN 2 ELSE 3 END foo')[0]