Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Development Version
-------------------

Nothing yet.
* Keep GRANT and REVOKE privilege clauses together when reindenting (issue409).


Release 0.6.0 (Aug 13, 2026)
Expand Down
9 changes: 9 additions & 0 deletions sqlparse/filters/reindent.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,15 @@ def _process_default(self, tlist, stmts=True):
for sgroup in tlist.get_sublists():
self._process(sgroup)

def _process_statement(self, tlist):
first = tlist.token_first(skip_cm=True)
if first and first.ttype == T.Keyword.DCL:
# Permission names and REVOKE's FROM are not query boundaries.
for sgroup in tlist.get_sublists():
self._process(sgroup)
else:
self._process_default(tlist)

def process(self, stmt):
self._curr_stmt = stmt
self._process(stmt)
Expand Down
47 changes: 47 additions & 0 deletions tests/test_dcl_reindent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Privilege names and REVOKE's FROM are not query-clause boundaries."""

import pytest

import sqlparse
from sqlparse import tokens as T


@pytest.mark.parametrize('statement', [
'GRANT SELECT ON a_schema.an_object TO a_role;',
'GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON t TO a_role;',
'GRANT SELECT ON ALL TABLES IN SCHEMA public TO umbrella;',
'GRANT ALL PRIVILEGES ON TABLE t TO a_role WITH GRANT OPTION;',
'REVOKE SELECT ON TABLE customers FROM umbrella;',
'REVOKE GRANT OPTION FOR SELECT, UPDATE ON t FROM a_role;',
'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM a_role;',
])
def test_privileges_are_not_split_as_statements(statement):
formatted = sqlparse.format(statement, reindent=True)
assert formatted == statement
assert sqlparse.format(formatted, reindent=True) == formatted


@pytest.mark.parametrize('comment', ['-- privileges\n', '/* privileges */ '])
def test_dcl_after_comment_is_recognized(comment):
statement = comment + 'GRANT SELECT ON t TO a_role;'
assert sqlparse.format(statement, reindent=True) == statement


def test_dcl_retains_keyword_case_filtering():
statement = 'revoke select on table customers from umbrella;'
assert sqlparse.format(statement, reindent=True, keyword_case='upper') == (
'REVOKE SELECT ON TABLE customers FROM umbrella;')


def test_dcl_does_not_disable_following_query_indentation():
statement = 'GRANT SELECT ON t TO a_role; SELECT a, b FROM t;'
assert sqlparse.format(statement, reindent=True) == (
'GRANT SELECT ON t TO a_role;\n\nSELECT a,\n b\nFROM t;')


def test_privilege_tokens_keep_their_existing_lexical_types():
statement = sqlparse.parse('GRANT SELECT ON t TO a_role;')[0]
keywords = [(token.value, token.ttype) for token in statement.flatten()
if token.ttype in T.Keyword]
assert keywords == [('GRANT', T.Keyword.DCL), ('SELECT', T.Keyword.DML),
('ON', T.Keyword), ('TO', T.Keyword)]