Skip to content

Feat: BQ io_manager to allow allow column case presevetion#33973

Open
adschem wants to merge 3 commits into
dagster-io:masterfrom
adschem:feat/bq-io-manager-column-case
Open

Feat: BQ io_manager to allow allow column case presevetion#33973
adschem wants to merge 3 commits into
dagster-io:masterfrom
adschem:feat/bq-io-manager-column-case

Conversation

@adschem

@adschem adschem commented Jul 2, 2026

Copy link
Copy Markdown
  • io manger and bigquery_pandas_type_handler change to allow for lower case
  • type_handler test added for column validation

Summary & Motivation

  • BigQueryPandasTypeHandler has always forced DataFrame column names to uppercase on write and lowercase on read, unconditionally
  • Not required by BigQuery — it's case-insensitive for matching but case-preserving in storage; the uppercasing was inherited from the Snowflake pandas type handler pattern, where it's load-bearing (Snowflake folds unquoted identifiers to uppercase), but that reasoning doesn't apply to BigQuery
  • Adds uppercase_column_names config on BigQueryIOManager (defaults True, i.e. today's behavior unchanged) — set False to preserve original DataFrame column casing end-to-end (write and read)
  • Purely additive/opt-in; zero behavior change for existing pipelines that don't set it

Test Plan

  • New unit test test_handle_output_preserve_column_case — mocked connection, no BigQuery/network dependency, asserts mixed-case column names ("Foo") pass through unchanged to load_table_from_dataframe when uppercase_column_names=False
  • Existing test_handle_output_empty_dataframe/test_handle_output_nonempty_dataframe continue to cover default (True) behavior
  • Ran full non-integration suite in dagster_gcp_pandas_tests/bigquery/test_type_handler.py

Changelog

  • dagster-gcp / dagster-gcp-pandas: Added uppercase_column_names config option to BigQueryIOManager (and subclasses BigQueryPandasIOManager, bigquery_pandas_io_manager). Defaults to True to preserve existing behavior; set to False to preserve the original casing of Pandas DataFrame column names when writing to and reading from BigQuery.

@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an opt-in uppercase_column_names config field (default True) to BigQueryIOManager and BigQueryPandasTypeHandler, allowing users to preserve the original DataFrame column casing end-to-end instead of the historical uppercase-on-write / lowercase-on-read behavior. The default preserves full backward compatibility.

  • BigQueryIOManager gains a uppercase_column_names: bool = True Pydantic field, automatically included in both the Pydantic-style and legacy build_bigquery_io_manager config schemas.
  • BigQueryPandasTypeHandler.handle_output and load_input now gate their column-case transforms behind a _should_uppercase_column_names helper that reads from context.resource_config, following the same pattern used for location and timeout.
  • Two new unit tests cover both the write (handle_output) and read (load_input) paths for uppercase_column_names=False.

Confidence Score: 5/5

Safe to merge — purely additive with default=True preserving all existing behavior unchanged.

The change is purely opt-in: the default value keeps every existing pipeline's column-casing behavior identical. The implementation follows the existing pattern for other config values and both write and read paths are covered by new unit tests.

No files require special attention; the base-class placement of uppercase_column_names is worth a second look but does not affect runtime behavior.

Important Files Changed

Filename Overview
python_modules/libraries/dagster-gcp/dagster_gcp/bigquery/io_manager.py Adds uppercase_column_names: bool field (default True) to BigQueryIOManager. The field is read by the Pandas type handler but is placed on the shared base class, making it visible to all BigQuery IO manager users regardless of their type handler.
python_modules/libraries/dagster-gcp-pandas/dagster_gcp_pandas/bigquery/bigquery_pandas_type_handler.py Adds _should_uppercase_column_names helper and wires it into both handle_output (conditional rename(columns=str.upper)) and load_input (conditional str.lower on result columns). Logic follows existing patterns for reading location/timeout from resource_config, and default=True preserves backward compatibility.
python_modules/libraries/dagster-gcp-pandas/dagster_gcp_pandas_tests/bigquery/test_type_handler.py Adds test_handle_output_preserve_column_case and test_load_input_preserve_column_case, covering both sides of the new flag (write and read) with mocked connections and mixed-case column names.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[BigQueryIOManager config\nuppercase_column_names: bool\ndefault = True] --> B{_should_uppercase_column_names\ncontext.resource_config}

    B -->|True / not set| C[handle_output:\nobj.rename columns=str.upper]
    B -->|False| D[handle_output:\npass obj as-is]

    C --> E[load_table_from_dataframe\nUPPERCASE columns in BQ]
    D --> F[load_table_from_dataframe\noriginal-case columns in BQ]

    E --> G[load_input:\nresult.columns = map str.lower]
    F --> H[load_input:\nno column rename]

    G --> I[Return DataFrame\nlowercase columns]
    H --> J[Return DataFrame\noriginal-case columns]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[BigQueryIOManager config\nuppercase_column_names: bool\ndefault = True] --> B{_should_uppercase_column_names\ncontext.resource_config}

    B -->|True / not set| C[handle_output:\nobj.rename columns=str.upper]
    B -->|False| D[handle_output:\npass obj as-is]

    C --> E[load_table_from_dataframe\nUPPERCASE columns in BQ]
    D --> F[load_table_from_dataframe\noriginal-case columns in BQ]

    E --> G[load_input:\nresult.columns = map str.lower]
    F --> H[load_input:\nno column rename]

    G --> I[Return DataFrame\nlowercase columns]
    H --> J[Return DataFrame\noriginal-case columns]
Loading

Reviews (2): Last reviewed commit: "adding in the load_input test feedback f..." | Re-trigger Greptile

Comment on lines +119 to +133
def test_handle_output_preserve_column_case():
handler = BigQueryPandasTypeHandler()
df = pd.DataFrame({"Foo": ["a", "b"], "bar": [1, 2]})
connection = MagicMock()
output_context = build_output_context(resource_config={"uppercase_column_names": False})

handler.handle_output(
output_context,
TableSlice(table="my_table", schema="my_schema", database="my_db"),
df,
connection,
)

loaded_df = connection.load_table_from_dataframe.call_args.kwargs["dataframe"]
assert list(loaded_df.columns) == ["Foo", "bar"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing unit test for load_input with uppercase_column_names=False

The new test only exercises handle_output; the symmetric change in load_input (skipping str.lower when the flag is False) has no unit test. A test with connection.query(...) mocked to return a DataFrame whose columns are already mixed-case (e.g., "Foo" / "bar") would confirm that those column names are returned unchanged when uppercase_column_names=False, and lowercased only when the flag is True (existing default). Without this coverage, the load_input side of the feature can be silently broken in a future refactor without any test catching it.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant