Skip to content

Commit e71ed34

Browse files
Additional Changes to "Escape quotes in identifiers (#771)" (#811)
* Escape quotes in identifiers * Add more tests to confirm that escaping quotes in identifiers works and remove FIXME placeholders --------- Co-authored-by: Alexander Malyga <[email protected]>
1 parent 6da153e commit e71ed34

File tree

7 files changed

+72
-6
lines changed

7 files changed

+72
-6
lines changed

pypika/queries.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ def __getattr__(self, item: str) -> "Table":
9898
return Table(item, schema=self)
9999

100100
def get_sql(self, quote_char: Optional[str] = None, **kwargs: Any) -> str:
101-
# FIXME escape
102101
schema_sql = format_quotes(self._name, quote_char)
103102

104103
if self._parent is not None:
@@ -150,7 +149,6 @@ def get_table_name(self) -> str:
150149

151150
def get_sql(self, **kwargs: Any) -> str:
152151
quote_char = kwargs.get("quote_char")
153-
# FIXME escape
154152
table_sql = format_quotes(self._table_name, quote_char)
155153

156154
if self._schema is not None:

pypika/terms.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -442,15 +442,13 @@ def get_value_sql(self, **kwargs: Any) -> str:
442442
def get_formatted_value(cls, value: Any, **kwargs):
443443
quote_char = kwargs.get("secondary_quote_char") or ""
444444

445-
# FIXME escape values
446445
if isinstance(value, Term):
447446
return value.get_sql(**kwargs)
448447
if isinstance(value, Enum):
449448
return cls.get_formatted_value(value.value, **kwargs)
450449
if isinstance(value, (date, datetime, time)):
451450
return cls.get_formatted_value(value.isoformat(), **kwargs)
452451
if isinstance(value, str):
453-
value = value.replace(quote_char, quote_char * 2)
454452
return format_quotes(value, quote_char)
455453
if isinstance(value, bool):
456454
return str.lower(str(value))
@@ -956,7 +954,6 @@ def __init__(self, container, alias=None):
956954
self._is_negated = False
957955

958956
def get_sql(self, **kwargs):
959-
# FIXME escape
960957
return "{not_}EXISTS {container}".format(
961958
container=self.container.get_sql(**kwargs), not_='NOT ' if self._is_negated else ''
962959
)
@@ -1000,7 +997,6 @@ def replace_table(self, current_table: Optional["Table"], new_table: Optional["T
1000997
self.term = self.term.replace_table(current_table, new_table)
1001998

1002999
def get_sql(self, **kwargs: Any) -> str:
1003-
# FIXME escape
10041000
sql = "{term} BETWEEN {start} AND {end}".format(
10051001
term=self.term.get_sql(**kwargs),
10061002
start=self.start.get_sql(**kwargs),

pypika/tests/test_criterions.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,16 @@ def test_not_exists(self):
731731
'SELECT "t1"."field1" FROM "def" "t1" WHERE NOT EXISTS (SELECT "t2"."field2" FROM "abc" "t2")', str(q1)
732732
)
733733

734+
def test_exists_with_double_quotes(self):
735+
t3 = Table('abc"', alias='t3"')
736+
q3 = QueryBuilder().from_(t3).select(t3.field2)
737+
t1 = Table("def", alias="t1")
738+
q1 = QueryBuilder().from_(t1).where(ExistsCriterion(q3)).select(t1.field1)
739+
740+
self.assertEqual(
741+
'SELECT "t1"."field1" FROM "def" "t1" WHERE EXISTS (SELECT "t3"""."field2" FROM "abc""" "t3""")', str(q1)
742+
)
743+
734744

735745
class ComplexCriterionTests(unittest.TestCase):
736746
table_abc, table_efg = Table("abc", alias="cx0"), Table("efg", alias="cx1")
@@ -798,6 +808,11 @@ def test__between_and_field(self):
798808
self.assertEqual('"foo" BETWEEN 0 AND 1 AND "bool_field"', str(c1 & c2))
799809
self.assertEqual('"bool_field" AND "foo" BETWEEN 0 AND 1', str(c2 & c1))
800810

811+
def test__between_with_quotes(self):
812+
c = Field('foo"\'').between("a'", "c'")
813+
814+
self.assertEqual('"foo""\'" BETWEEN \'a\'\'\' AND \'c\'\'\'', str(c))
815+
801816

802817
class FieldsAsCriterionTests(unittest.TestCase):
803818
def test__field_and_field(self):

pypika/tests/test_selects.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ def test_select_no_with_alias_from(self):
4949

5050
self.assertEqual('SELECT 1 "test"', str(q))
5151

52+
def test_select_literal_with_alias_with_quotes(self):
53+
q = Query.select(ValueWrapper("contains'\"quotes", "contains'\"quotes"))
54+
55+
self.assertEqual('SELECT \'contains\'\'"quotes\' "contains\'""quotes"', str(q))
56+
5257
def test_select_no_from_with_field_raises_exception(self):
5358
with self.assertRaises(QueryException):
5459
Query.select("asdf")
@@ -73,6 +78,11 @@ def test_select__table_schema_with_multiple_levels_as_list(self):
7378

7479
self.assertEqual('SELECT * FROM "schema1"."schema2"."abc"', str(q))
7580

81+
def test_select__table_schema_escape_double_quote(self):
82+
q = Query.from_(Table("abc", 'schema_with_double_quote"')).select("*")
83+
84+
self.assertEqual('SELECT * FROM "schema_with_double_quote"""."abc"', str(q))
85+
7686
def test_select__star__replacement(self):
7787
q = Query.from_("abc").select("foo").select("*")
7888

pypika/tests/test_tables.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,33 @@ def test_table_sql(self):
1313

1414
self.assertEqual('"test_table"', str(table))
1515

16+
def test_table_sql_with_double_quote(self):
17+
table = Table('test_table_with_double_quote"')
18+
19+
self.assertEqual('"test_table_with_double_quote"""', str(table))
20+
21+
def test_table_sql_with_several_double_quotes(self):
22+
table = Table('test"table""with"""double"quotes')
23+
24+
self.assertEqual('"test""table""""with""""""double""quotes"', str(table))
25+
26+
def test_table_sql_with_single_quote(self):
27+
table = Table("test_table_with_single_quote'")
28+
29+
self.assertEqual('"test_table_with_single_quote\'"', str(table))
30+
1631
def test_table_with_alias(self):
1732
table = Table("test_table").as_("my_table")
1833

1934
self.assertEqual('"test_table" "my_table"', table.get_sql(with_alias=True, quote_char='"'))
2035

36+
def test_table_with_alias_with_double_quote(self):
37+
table = Table('test_table_with_double_quote"').as_("my_alias\"")
38+
39+
self.assertEqual(
40+
'"test_table_with_double_quote""" "my_alias"""', table.get_sql(with_alias=True, quote_char='"')
41+
)
42+
2143
def test_schema_table_attr(self):
2244
table = Schema("x_schema").test_table
2345

pypika/tests/test_terms.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,25 @@ def test_passes_kwargs_to_field_get_sql(self):
8080
'FROM "customers" JOIN "accounts" ON "customers"."account_id"="accounts"."account_id"',
8181
query.get_sql(with_namespace=True),
8282
)
83+
84+
85+
class IdentifierEscapingTests(TestCase):
86+
def test_escape_identifier_quotes(self):
87+
customers = Table('customers"')
88+
customer_id = getattr(customers, '"id')
89+
email = getattr(customers, 'email"').as_('customer_email"')
90+
91+
query = (
92+
Query.from_(customers)
93+
.select(customer_id, email)
94+
.where(customer_id == "abc")
95+
.where(email == "[email protected]")
96+
.orderby(email, customer_id)
97+
)
98+
99+
self.assertEqual(
100+
'SELECT """id","email""" "customer_email""" '
101+
'FROM "customers""" WHERE """id"=\'abc\' AND "email"""=\'[email protected]\' '
102+
'ORDER BY "customer_email""","""id"',
103+
query.get_sql(),
104+
)

pypika/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ def resolve_is_aggregate(values: List[Optional[bool]]) -> Optional[bool]:
120120

121121

122122
def format_quotes(value: Any, quote_char: Optional[str]) -> str:
123+
if quote_char:
124+
value = value.replace(quote_char, quote_char * 2)
125+
123126
return "{quote}{value}{quote}".format(value=value, quote=quote_char or "")
124127

125128

0 commit comments

Comments
 (0)