Skip to content

Commit d721e59

Browse files
committed
fix: role name quoting.
1 parent 43506f8 commit d721e59

File tree

6 files changed

+27
-16
lines changed

6 files changed

+27
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### 0.15.12
66
- fix: Enable registering zero objects (enable without declaring anything).
77
- fix: Add explicit relkind casts to ensure compatibility with postgres 11.
8+
- fix: Improve quoting of role names, particularly in postgres.
89

910
### 0.15.11
1011
- fix: Exclude extension-created objects from comparisons.

src/sqlalchemy_declarative_extensions/dialects/postgresql/role.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,29 +91,28 @@ def __repr__(self):
9191
return f'{cls_name}("{self.name}", {options})'
9292

9393
def to_sql_create(self, raw: bool = True) -> list[str]:
94-
segments = ["CREATE ROLE", self.name]
94+
segments = ["CREATE ROLE", generic.quote_name(self.name)]
9595

9696
options = postgres_render_role_options(self, raw=raw)
9797
if options:
9898
segments.append("WITH")
9999
segments.extend(options)
100100

101101
if self.in_roles is not None:
102-
in_roles = ", ".join(generic.role_names(self.in_roles))
103-
segment = f"IN ROLE {in_roles}"
102+
segment = f"IN ROLE {generic.quote_names(self.in_roles)}"
104103
segments.append(segment)
105104

106105
command = " ".join(segments) + ";"
107106
return [command]
108107

109108
def to_sql_update(self, to_role: Role, raw: bool = True) -> list[str]:
110-
role_name = to_role.name
109+
role_name = generic.quote_name(to_role.name)
111110
diff = RoleDiff.diff(self, to_role)
112111

113112
result = []
114113

115114
if self.use_role:
116-
result.append(f"SET ROLE {self.use_role};")
115+
result.append(f"""SET ROLE "{generic.quote_name(self.use_role)}";""")
117116

118117
diff_options = postgres_render_role_options(diff, raw=raw)
119118
if diff_options:
@@ -122,10 +121,12 @@ def to_sql_update(self, to_role: Role, raw: bool = True) -> list[str]:
122121
result.append(alter_role)
123122

124123
for add_name in diff.add_roles:
125-
result.append(f"GRANT {add_name} TO {role_name};")
124+
result.append(f"""GRANT {generic.quote_name(add_name)} TO {role_name};""")
126125

127126
for remove_name in diff.remove_roles:
128-
result.append(f"REVOKE {remove_name} FROM {role_name};")
127+
result.append(
128+
f"""REVOKE {generic.quote_name(remove_name)} FROM {role_name};"""
129+
)
129130

130131
if self.use_role:
131132
result.append("RESET ROLE")

src/sqlalchemy_declarative_extensions/role/generic.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44
from dataclasses import dataclass, replace
5+
from typing import Sequence
56

67
from sqlalchemy_declarative_extensions.context import context
78

@@ -78,10 +79,9 @@ def normalize(self):
7879
)
7980

8081
def to_sql_create(self, raw: bool = True) -> list[str]:
81-
statement = f'CREATE ROLE "{self.name}"'
82+
statement = f"CREATE ROLE {quote_name(self.name)}"
8283
if self.in_roles is not None:
83-
in_roles = ", ".join(role_names(self.in_roles))
84-
statement += f"IN ROLE {in_roles}"
84+
statement += f"IN ROLE {quote_names(self.in_roles)}"
8585
return [statement + ";"]
8686

8787
def to_sql_update(self, to_role, raw: bool = True) -> list[str]:
@@ -90,7 +90,7 @@ def to_sql_update(self, to_role, raw: bool = True) -> list[str]:
9090
)
9191

9292
def to_sql_drop(self, raw: bool = True) -> list[str]:
93-
return [f'DROP ROLE "{self.name}";']
93+
return [f"DROP ROLE {quote_name(self.name)};"]
9494

9595
def to_sql_use(self, undo: bool) -> list[str]:
9696
raise NotImplementedError()
@@ -132,3 +132,11 @@ def role_name(role: Role | str) -> str:
132132

133133
def role_names(roles: list[Role | str]) -> list[str]:
134134
return [role_name(r) for r in roles]
135+
136+
137+
def quote_name(role: Role | str) -> str:
138+
return f'"{role_name(role)}"'
139+
140+
141+
def quote_names(roles: Sequence[Role | str]) -> str:
142+
return ", ".join(quote_name(role) for role in roles)

tests/grant/test_only_defined_roles.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
class Base(_Base): # type: ignore
1919
__abstract__ = True
2020

21+
roles = ["user"]
2122
grants = Grants(only_defined_roles=False, ignore_self_grants=False).are(
2223
DefaultGrant.on_tables_in_schema("public").grant(
2324
"select",

tests/role/test_remove_valid_until.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def test_valid_until_date_being_removed(pg):
2222
result = compare_roles(conn, roles)
2323
assert len(result) == 1
2424
sql = result[0].to_sql()
25-
assert sql == ["ALTER ROLE forever_valid WITH VALID UNTIL 'infinity';"]
25+
assert sql == ["""ALTER ROLE "forever_valid" WITH VALID UNTIL 'infinity';"""]
2626

2727

2828
def test_valid_until_infinity_ignored(pg):

tests/role/test_update_parent_role.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ def test_createall_role(pg):
4141

4242
child_no_parent_diff = result[0].to_sql()
4343
assert child_no_parent_diff == [
44-
"REVOKE parent1 FROM child_no_parent;",
45-
"REVOKE parent2 FROM child_no_parent;",
44+
"""REVOKE "parent1" FROM "child_no_parent";""",
45+
"""REVOKE "parent2" FROM "child_no_parent";""",
4646
]
4747

4848
child_with_parent_diff = result[1].to_sql()
4949
assert child_with_parent_diff == [
50-
"GRANT parent1 TO child_with_parent;",
51-
"GRANT parent2 TO child_with_parent;",
50+
"""GRANT "parent1" TO "child_with_parent";""",
51+
"""GRANT "parent2" TO "child_with_parent";""",
5252
]

0 commit comments

Comments
 (0)