Skip to content

Commit d1b01d8

Browse files
committed
Add handling for identifiers containing double quotes
1 parent 5921278 commit d1b01d8

File tree

2 files changed

+21
-2
lines changed

2 files changed

+21
-2
lines changed

duckdb/polars_io.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@ def _pl_operation_to_sql(op: str) -> str:
5858
raise NotImplementedError(op)
5959

6060

61+
def _escape_sql_identifier(identifier: str) -> str:
62+
"""
63+
Escape SQL identifiers by doubling any double quotes and wrapping in double quotes.
64+
65+
Example:
66+
>>> _escape_sql_identifier('column"name')
67+
'"column""name"'
68+
"""
69+
escaped = identifier.replace('"', '""')
70+
return f'"{escaped}"'
71+
72+
6173
def _pl_tree_to_sql(tree: dict) -> str:
6274
"""
6375
Recursively convert a Polars expression tree (as JSON) to a SQL string.
@@ -96,7 +108,7 @@ def _pl_tree_to_sql(tree: dict) -> str:
96108
if node_type == "Column":
97109
# A reference to a column name
98110
# Wrap in quotes to handle special characters
99-
return f'"{subtree}"'
111+
return _escape_sql_identifier(subtree)
100112

101113
if node_type in ("Literal", "Dyn"):
102114
# Recursively process dynamic or literal values
@@ -197,7 +209,7 @@ def source_generator(
197209
duck_predicate = None
198210
relation_final = relation
199211
if with_columns is not None:
200-
cols = ",".join(f'"{col}"' for col in with_columns)
212+
cols = ",".join(map(_escape_sql_identifier, with_columns))
201213
relation_final = relation_final.project(cols)
202214
if n_rows is not None:
203215
relation_final = relation_final.limit(n_rows)

tests/fast/arrow/test_polars.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ def test_polars_column_with_tricky_name(self, duckdb_cursor):
154154
result = lf.select(pl.all()).filter(pl.col("x.y") == 1).collect()
155155
assert result.to_dicts() == [{"x.y": 1}]
156156

157+
df_quote = pl.DataFrame({'"xy"': [1, 2]})
158+
lf = duckdb_cursor.sql("from df_quote").pl(lazy=True)
159+
result = lf.select(pl.all()).collect()
160+
assert result.to_dicts() == [{'"xy"': 1}, {'"xy"': 2}]
161+
result = lf.select(pl.all()).filter(pl.col('"xy"') == 1).collect()
162+
assert result.to_dicts() == [{'"xy"': 1}]
163+
157164
@pytest.mark.parametrize(
158165
'data_type',
159166
[

0 commit comments

Comments
 (0)