diff --git a/CHANGELOG b/CHANGELOG index 44d5938e..03322131 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,9 @@ Development Version ------------------- -Nothing yet. +Bug Fixes + +* Group single-quoted string literals with their implicit aliases (issue518). Release 0.6.0 (Aug 13, 2026) diff --git a/sqlparse/engine/grouping.py b/sqlparse/engine/grouping.py index d8cfa9e1..a7f42792 100644 --- a/sqlparse/engine/grouping.py +++ b/sqlparse/engine/grouping.py @@ -367,13 +367,14 @@ def group_where(tlist): def group_aliased(tlist): I_ALIAS = (sql.Parenthesis, sql.Function, sql.Case, sql.Identifier, sql.Operation, sql.Comparison) + ttypes = [T.Number, T.String.Single] - tidx, token = tlist.token_next_by(i=I_ALIAS, t=T.Number) + tidx, token = tlist.token_next_by(i=I_ALIAS, t=ttypes) while token: nidx, next_ = tlist.token_next(tidx) if isinstance(next_, sql.Identifier): tlist.group_tokens(sql.Identifier, tidx, nidx, extend=True) - tidx, token = tlist.token_next_by(i=I_ALIAS, t=T.Number, idx=tidx) + tidx, token = tlist.token_next_by(i=I_ALIAS, t=ttypes, idx=tidx) @recurse(sql.Function) diff --git a/tests/test_format.py b/tests/test_format.py index 93495067..16d444c2 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -597,6 +597,17 @@ def test_keywordfunctions(self): ' foo,', ' bar']) + @pytest.mark.parametrize('position', range(3)) + def test_aliased_string(self, position): + # issue518: align following columns with the complete expression. + columns = ['field1', 'field2'] + columns.insert(position, "'fixed_text' alias1") + query = 'SELECT {} FROM tbl'.format(', '.join(columns)) + expected = 'SELECT ' + ',\n '.join(columns) + '\nFROM tbl' + formatted = sqlparse.format(query, reindent=True) + assert formatted == expected + assert sqlparse.format(formatted, reindent=True) == expected + def test_identifier_and_functions(self): # issue45 f = lambda sql: sqlparse.format(sql, reindent=True) diff --git a/tests/test_grouping.py b/tests/test_grouping.py index 20fab9b7..ee35abdf 100644 --- a/tests/test_grouping.py +++ b/tests/test_grouping.py @@ -669,12 +669,68 @@ def test_aliased_function_without_as(): assert p.get_alias() == 'baz' -def test_aliased_literal_without_as(): - p = sqlparse.parse('1 foo')[0].tokens +@pytest.mark.parametrize('literal', ['1', '1.5', '1e2', '0xff']) +def test_aliased_literal_without_as(literal): + p = sqlparse.parse(f'{literal} foo')[0].tokens assert len(p) == 1 assert p[0].get_alias() == 'foo' +@pytest.mark.parametrize('literal', ["'fixed_text'", "''", "'a''b'", "'a\nb'"]) +@pytest.mark.parametrize('alias', ['alias1', '"Alias Name"']) +def test_aliased_string_without_as(literal, alias): + # issue518: keep the literal and its implicit alias in one expression. + text = f'{literal} {alias}' + p = sqlparse.parse(text)[0] + assert str(p) == text + assert len(p.tokens) == 1 + identifier = p.tokens[0] + assert isinstance(identifier, sql.Identifier) + assert identifier.get_alias() == alias.strip('"') + assert identifier.tokens[0].ttype is T.String.Single + assert identifier.tokens[0].value == literal + assert identifier.tokens[0].parent is identifier + + +@pytest.mark.parametrize('position', range(3)) +@pytest.mark.parametrize('nested', [False, True]) +def test_aliased_string_in_projection(position, nested): + # issue518: an implicit alias must not fragment the projection list. + columns = ['field1', 'field2'] + columns.insert(position, "'fixed_text' alias1") + query = 'SELECT {} FROM tbl'.format(', '.join(columns)) + text = f'SELECT * FROM ({query}) subquery' if nested else query + statement = sqlparse.parse(text)[0] + assert str(statement) == text + parsed = statement.tokens[-1].tokens[0] if nested else statement + _, identifiers = parsed.token_next_by(i=sql.IdentifierList) + assert isinstance(identifiers, sql.IdentifierList) + items = list(identifiers.get_identifiers()) + assert [str(item) for item in items] == columns + assert items[position].get_alias() == 'alias1' + assert all(item.parent is identifiers for item in items) + + +@pytest.mark.parametrize('text', ["'foo'", "''", "'a''b'", "'foo' 'bar'", + "'foo'\n'bar'"]) +def test_unaliased_strings_remain_literals(text): + # issue111: recognizing aliases must not group bare/adjacent strings. + statement = sqlparse.parse(text)[0] + assert str(statement) == text + tokens = [token for token in statement if not token.is_whitespace] + assert all(token.ttype is T.String.Single for token in tokens) + + +@pytest.mark.parametrize('text', ["DATE '2020-01-01'", "INTERVAL '1' DAY"]) +def test_string_alias_grouping_preserves_typed_literals(text): + statement = sqlparse.parse(f'SELECT {text}')[0] + assert str(statement) == f'SELECT {text}' + assert len(statement.tokens) == 3 + assert isinstance(statement.tokens[2], sql.TypedLiteral) + assert any(token.ttype is T.String.Single + for token in statement.tokens[2].flatten()) + + def test_grouping_as_cte(): p = sqlparse.parse('foo AS WITH apple AS 1, banana AS 2')[0].tokens assert len(p) > 4