Repository navigation
fix(duckdb): preserve case-colliding pandas columns #528
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
JohnnyWilson16
merged 4 commits into
FreshCode-Org:main
from
WilliamK112:codex/duckdb-case-collision-fallback-527
Oct 13, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
2ec9f10
fix(duckdb): preserve case-colliding pandas columns
WilliamK112 27759b7
docs: align DuckDB collision fallback matrix with input guard
WilliamK112 9e1a83f
Merge remote-tracking branch 'origin/main' into codex/duckdb-case-col…
JohnnyWilson16 225c2b0
Merge remote-tracking branch 'origin/main' into codex/duckdb-case-col…
JohnnyWilson16 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| """DuckDB cannot safely carry case-insensitive pandas header collisions.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import pandas as pd | ||
| import pytest | ||
|
|
||
| import freshdata as fd | ||
| from freshdata.execution import FallbackError, FallbackWarning | ||
| from freshdata.execution.backends._duckdb import DuckDBEngine | ||
|
|
||
| pytest.importorskip("duckdb") | ||
| KW = {"strategy": "conservative", "fix_dtypes": False, "verbose": False} | ||
|
|
||
|
|
||
| def _frame(empty_second=False, unrelated_empty=False): | ||
| frame = pd.DataFrame({ | ||
| "A": [1, 2, 3], "a": [None] * 3 if empty_second else ["x", "y", "z"], | ||
| "A 1": pd.Series([4, 5, 6], dtype="Int64"), | ||
| }) | ||
| if unrelated_empty: | ||
| frame["Unused"] = None | ||
| return frame | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("column_names", [True, False]) | ||
| @pytest.mark.parametrize(("empty_second", "unrelated_empty"), [ | ||
| (False, False), (True, False), (False, True), | ||
| ]) | ||
| def test_case_colliding_headers_preserve_pandas_values_and_schema( | ||
| column_names, empty_second, unrelated_empty | ||
| ): | ||
| frame = _frame(empty_second, unrelated_empty) | ||
| original = frame.copy(deep=True) | ||
| expected, reference = fd.clean( | ||
| frame, engine="pandas", column_names=column_names, return_report=True, **KW | ||
| ) | ||
| out, report = fd.clean( | ||
| frame, engine="duckdb", column_names=column_names, return_report=True, **KW | ||
| ) | ||
| pd.testing.assert_frame_equal(pd.DataFrame(out), pd.DataFrame(expected)) | ||
| pd.testing.assert_frame_equal(frame, original) | ||
| assert report.backend == "pandas" | ||
| assert report.requested_backend == "duckdb" | ||
| assert "case-insensitive" in report.fallback_events[0]["fallback_reason"] | ||
| assert [(a.step, a.count) for a in report.actions] == [ | ||
| (a.step, a.count) for a in reference.actions | ||
| ] | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("column_names", [True, False]) | ||
| def test_case_collision_warn_policy_is_disclosed(column_names): | ||
| with pytest.warns(FallbackWarning, match="case-insensitive"): | ||
| out = fd.clean(_frame(True), engine="duckdb", column_names=column_names, | ||
| fallback_policy="warn", **KW) | ||
| assert out.shape == (3, 2) | ||
|
|
||
|
|
||
| def test_strict_policy_rejects_before_native_ingestion_or_pandas_work(monkeypatch): | ||
| def forbidden(*args, **kwargs): | ||
| raise AssertionError("strict policy must reject before cleaning") | ||
|
|
||
| monkeypatch.setattr(DuckDBEngine, "_register_source", forbidden) | ||
| monkeypatch.setattr(DuckDBEngine, "_fallback", forbidden) | ||
| with pytest.raises(FallbackError, match="case-insensitive"): | ||
| fd.clean(_frame(True), engine="duckdb", fallback_policy="error", **KW) | ||
|
|
||
|
|
||
| def test_native_handle_request_reports_the_materialized_preservation_fallback(): | ||
| out, report = fd.clean(_frame(True), engine="duckdb", output_format="duckdb", | ||
| return_report=True, **KW) | ||
| assert isinstance(out, pd.DataFrame) | ||
| assert report.materialized is True | ||
| assert out.shape == (3, 2) | ||
| assert report.fallback_events | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("names", [("A B", "A-B"), ("Ä", "ä"), ("ß", "ss")]) | ||
| def test_non_colliding_duckdb_identifiers_remain_native(names): | ||
| frame = pd.DataFrame([[1, 2], [3, 4]], columns=names) | ||
| out, report = fd.clean(frame, engine="duckdb", return_report=True, **KW) | ||
| assert out.shape == (2, 2) | ||
| assert report.backend == "duckdb" | ||
| assert report.fallback_events == [] | ||
|
|
||
|
|
||
| def test_non_string_labels_that_collide_in_duckdb_are_preserved_by_pandas(): | ||
| frame = pd.DataFrame({7: [1, 2, 3], "7": [4, 5, 6]}) | ||
| expected = fd.clean(frame, engine="pandas", column_names=False, **KW) | ||
| out, report = fd.clean(frame, engine="duckdb", column_names=False, | ||
| return_report=True, **KW) | ||
| pd.testing.assert_frame_equal(pd.DataFrame(out), pd.DataFrame(expected)) | ||
| assert report.backend == "pandas" |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a pandas frame with labels
7and"7", the unchanged row 43 still marks DuckDB asnative, but the new detector in_ingest.pyforces pandas andfallback_policy="error"raises. This paragraph therefore contradicts the primary matrix for the exact case it documents, so users selecting strict/out-of-core settings from the table get the wrong behavior; change the DuckDB cell in row 43 topandasand update its rationale.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in the latest documentation commit: the stringified-label collision row now marks DuckDB as pandas and explains the alias-selection risk. The surrounding paragraph and table now agree for labels such as
7/"7", including strict-policy rejection before ingestion. Strict MkDocs and diff checks pass.