Skip to content

Commit 58866e1

Browse files
martinv13claude
andauthored
Fix DuckDB bulk_insert failing when quoted CSV cells fall outside sniffer sample (#64)
* Fix DuckDB bulk_insert failing when quoted CSV cells fall outside sniffer sample (#62) DuckDB's read_csv sniffer only examines the first ~20 480 rows; if none are quoted, it sets quote=(empty) and then errors on any later quoted cell with a column-count mismatch. Passing quote='"' explicitly bypasses auto-detection. Adds a regression test that inserts 25 000 plain rows followed by one row whose value contains a comma (triggering csv.writer quoting). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Bump version to 0.13.3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add explicit escape='\"' to read_csv alongside quote='\"' Makes the RFC 4180 doubling behaviour explicit rather than relying on DuckDB defaulting escape to the same char as quote. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a8c5471 commit 58866e1

3 files changed

Lines changed: 23 additions & 2 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "xml2db"
7-
version = "0.13.2"
7+
version = "0.13.3"
88
authors = [
99
{ name="Commission de régulation de l'énergie", email="opensource@cre.fr" },
1010
]

src/xml2db/dialect/duckdb.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def bulk_insert(self, conn: Any, table: Any, records: list) -> None:
158158
sql = text(
159159
f"INSERT INTO {full_name} ({insert_cols}) "
160160
f"SELECT {select_exprs} "
161-
f"FROM read_csv('{safe_path}', header=true, nullstr='', all_varchar=true)"
161+
f"FROM read_csv('{safe_path}', header=true, nullstr='', all_varchar=true, quote='\"', escape='\"')"
162162
)
163163
conn.execute(sql)
164164
finally:

tests/test_bulk_insert.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,27 @@ def test_duckdb_bulk_insert_scalar_column_default(duckdb_engine):
168168
assert rows[1]["flag"] is False
169169

170170

171+
def test_duckdb_bulk_insert_quoted_csv_field_after_large_unquoted_sample(duckdb_engine):
172+
"""Regression: DuckDB's CSV sniffer uses only the first ~20k rows as a sample.
173+
174+
If all sampled rows are unquoted, the sniffer sets quote=(empty), causing a
175+
column-count error when it later hits a row whose cell value contains a comma
176+
(making csv.writer emit a quoted field). Explicitly passing quote='"' to
177+
read_csv bypasses auto-detection and must always be present.
178+
"""
179+
table = _make_table(duckdb_engine, "quoted_field_test")
180+
# 'vals' value that contains a comma — document.py's 'join' transform can produce
181+
# strings like '"val,ue",other' which csv.writer then wraps in outer quotes,
182+
# yielding a quoted CSV cell.
183+
problematic_value = '"val,ue",other_value'
184+
records = [
185+
{"id": i, "label": "simple"} for i in range(25_000) # exceeds sniffer sample
186+
] + [{"id": 25_000, "label": problematic_value}]
187+
rows = _roundtrip(duckdb_engine, table, records)
188+
assert len(rows) == 25_001
189+
assert rows[-1]["label"] == problematic_value
190+
191+
171192
def test_duckdb_bulk_insert_empty(duckdb_engine):
172193
table = _make_table(duckdb_engine, "empty_test")
173194
dialect = DuckDBDialect()

0 commit comments

Comments
 (0)