Skip to content

Fix UnknownTimeZoneError in SnowflakeIOManager with ADBC connector#33922

Open
I-am-Uchenna wants to merge 7 commits into
dagster-io:masterfrom
I-am-Uchenna:fix/snowflake-adbc-timezone-error
Open

Fix UnknownTimeZoneError in SnowflakeIOManager with ADBC connector#33922
I-am-Uchenna wants to merge 7 commits into
dagster-io:masterfrom
I-am-Uchenna:fix/snowflake-adbc-timezone-error

Conversation

@I-am-Uchenna

Copy link
Copy Markdown

Summary & Motivation

Closes #33750

When using SnowflakePolarsIOManager (which defaults to connector = "adbc"), asset writes fail on Windows with:

pytz.exceptions.UnknownTimeZoneError: 'Local'

The error originates in SnowflakeDbClient.ensure_schema_exists, which runs SHOW SCHEMAS via an ADBC cursor. The result set contains TIMESTAMP_LTZ columns. When no session-level TIMEZONE has been set, Snowflake reports the timezone as 'Local' in the Arrow schema metadata. PyArrow calls pytz.timezone('Local') during cursor.fetchall(), which raises UnknownTimeZoneError because 'Local' is not a valid IANA timezone identifier.

The native snowflake-connector-python handles this internally via a tzlocal fallback, but the ADBC path delegates timezone resolution to PyArrow + pytz which have no such fallback.

A second occurrence of the same pattern exists in _table_exists in the Polars type handler, which runs SHOW TABLES through the same ADBC cursor path.

Fix

Replaced SHOW SCHEMAS and SHOW TABLES commands with equivalent information_schema queries:

  • ensure_schema_exists in snowflake_io_manager.py: queries information_schema.schemata instead of SHOW SCHEMAS
  • _table_exists in snowflake_polars_type_handler.py: queries information_schema.tables instead of SHOW TABLES
    By selecting a constant (SELECT 1) rather than all columns, the result set contains no TIMESTAMP_LTZ values. PyArrow never encounters the 'Local' timezone string, which eliminates the error regardless of session timezone configuration.

This approach:

  • Does not modify any session state (no ALTER SESSION SET TIMEZONE)
  • Does not change behavior for non-ADBC connectors
  • Preserves case-insensitive schema/table name matching via LOWER()
  • Works with the same permissions as SHOW commands since information_schema is accessible to all roles that have privileges on the referenced objects

Test Plan

Added unit tests for ensure_schema_exists:

  • test_ensure_schema_exists_creates_missing_schema: verifies information_schema.schemata is queried and schema is created when missing
  • test_ensure_schema_exists_skips_create_when_schema_exists: verifies schema creation is skipped when schema already exists

Changelog

Fix UnknownTimeZoneError: 'Local' when using SnowflakePolarsIOManager with ADBC connector on systems where no session timezone is set. Replaced SHOW SCHEMAS and SHOW TABLES commands with information_schema queries to avoid TIMESTAMP_LTZ columns in result sets.

Replace SHOW SCHEMAS with information_schema.schemata query to avoid
pytz.exceptions.UnknownTimeZoneError when using ADBC connector.

SHOW SCHEMAS returns TIMESTAMP_LTZ columns that trigger
pytz.timezone('Local') failure when no session timezone is set.
…BLES

Updated _table_exists function to query information_schema.tables instead of using SHOW TABLES to prevent UnknownTimeZoneError with ADBC connections.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e79dd01c5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +22 to +24
f"SELECT 1 FROM {table_slice.database}.information_schema.tables"
f" WHERE LOWER(table_schema) = LOWER('{table_slice.schema}')"
f" AND LOWER(table_name) = LOWER('{table_slice.table}')"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exclude views when checking for existing tables

When a partitioned asset targets a name that currently exists only as a view, this new information_schema.tables query returns a row because Snowflake's TABLES view includes rows with TABLE_TYPE = 'VIEW'. That makes handle_output take the append/delete path and issue DELETE FROM ... plus an append write against the view, whereas the previous SHOW TABLES check would not match views and would leave the handler in replace mode. Please filter TABLE_TYPE to actual table types so views do not masquerade as existing tables.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point. Fixed in 527dc6e -- added AND TABLE_TYPE = 'BASE TABLE' filter to the query so views are excluded from the existence check.

@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a pytz.exceptions.UnknownTimeZoneError: 'Local' crash on Windows when using SnowflakePolarsIOManager with the ADBC connector. The root cause is that SHOW SCHEMAS / SHOW TABLES return TIMESTAMP_LTZ columns whose metadata carries 'Local' as the timezone string, which PyArrow passes to pytz.timezone() — a call that fails because 'Local' is not a valid IANA identifier.

  • ensure_schema_exists (snowflake_io_manager.py): replaces SHOW SCHEMAS LIKE with a SELECT 1 FROM db.information_schema.schemata WHERE LOWER(schema_name) = LOWER(%s) query; schema name is now parameterized.
  • _table_exists (snowflake_polars_type_handler.py): replaces SHOW TABLES LIKE with a SELECT 1 FROM db.information_schema.tables query; schema and table names are parameterized and a TABLE_TYPE = 'BASE TABLE' filter is added to exclude views.
  • Tests (test_snowflake_io_manager.py): two unit tests are added for ensure_schema_exists, covering the "schema missing → CREATE" and "schema exists → skip" branches.

Confidence Score: 5/5

The change is safe to merge: it surgically swaps two SHOW commands for equivalent information_schema queries, eliminating the PyArrow timezone crash without altering session state or behavior for non-ADBC connectors.

Both query substitutions are straightforward, the schema/table values are correctly parameterized, and the logic for schema creation and partition-table detection is unchanged. The remaining unquoted database identifier is a pre-existing pattern shared across the codebase and carries no immediate runtime risk given that database names come from Dagster asset configuration.

No files require special attention; the database identifier quoting noted in snowflake_io_manager.py and snowflake_polars_type_handler.py is a hardening suggestion, not a blocking issue.

Important Files Changed

Filename Overview
python_modules/libraries/dagster-snowflake/dagster_snowflake/snowflake_io_manager.py Replaces SHOW SCHEMAS LIKE with an information_schema.schemata query using a parameterized %s for the schema name; table_slice.database is still f-string interpolated in the FROM clause identifier.
python_modules/libraries/dagster-snowflake-polars/dagster_snowflake_polars/snowflake_polars_type_handler.py Replaces SHOW TABLES LIKE with an information_schema.tables query; schema and table names are now parameterized, TABLE_TYPE = 'BASE TABLE' filter added, but table_slice.database remains f-string interpolated. No new unit tests for this function.
python_modules/libraries/dagster-snowflake/dagster_snowflake_tests/test_snowflake_io_manager.py Adds two unit tests for ensure_schema_exists covering the "schema missing → CREATE" and "schema present → skip CREATE" branches; both correctly verify query text and parameters.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Asset
    participant SnowflakeDbClient
    participant Cursor
    participant Snowflake

    Asset->>SnowflakeDbClient: ensure_schema_exists()
    SnowflakeDbClient->>Cursor: "execute(SELECT 1 FROM db.information_schema.schemata WHERE LOWER(schema_name)=LOWER(%s))"
    Cursor->>Snowflake: parameterized query (no TIMESTAMP_LTZ columns)
    Snowflake-->>Cursor: result rows (integer only)
    Cursor-->>SnowflakeDbClient: fetchall()
    alt schema not found
        SnowflakeDbClient->>Cursor: execute(CREATE SCHEMA schema_name)
        Cursor->>Snowflake: DDL
    end

    Asset->>SnowflakePolarsTypeHandler: handle_output() partitioned
    SnowflakePolarsTypeHandler->>Cursor: "execute(SELECT 1 FROM db.information_schema.tables WHERE LOWER(table_schema)=LOWER(%s) AND LOWER(table_name)=LOWER(%s))"
    Cursor->>Snowflake: parameterized query (no TIMESTAMP_LTZ columns)
    Snowflake-->>Cursor: result rows
    Cursor-->>SnowflakePolarsTypeHandler: fetchall()
    alt table exists
        SnowflakePolarsTypeHandler->>Snowflake: DELETE partition slice
        SnowflakePolarsTypeHandler->>Snowflake: write_database append
    else table not found
        SnowflakePolarsTypeHandler->>Snowflake: write_database replace
    end
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"}}}%%
sequenceDiagram
    participant Asset
    participant SnowflakeDbClient
    participant Cursor
    participant Snowflake

    Asset->>SnowflakeDbClient: ensure_schema_exists()
    SnowflakeDbClient->>Cursor: "execute(SELECT 1 FROM db.information_schema.schemata WHERE LOWER(schema_name)=LOWER(%s))"
    Cursor->>Snowflake: parameterized query (no TIMESTAMP_LTZ columns)
    Snowflake-->>Cursor: result rows (integer only)
    Cursor-->>SnowflakeDbClient: fetchall()
    alt schema not found
        SnowflakeDbClient->>Cursor: execute(CREATE SCHEMA schema_name)
        Cursor->>Snowflake: DDL
    end

    Asset->>SnowflakePolarsTypeHandler: handle_output() partitioned
    SnowflakePolarsTypeHandler->>Cursor: "execute(SELECT 1 FROM db.information_schema.tables WHERE LOWER(table_schema)=LOWER(%s) AND LOWER(table_name)=LOWER(%s))"
    Cursor->>Snowflake: parameterized query (no TIMESTAMP_LTZ columns)
    Snowflake-->>Cursor: result rows
    Cursor-->>SnowflakePolarsTypeHandler: fetchall()
    alt table exists
        SnowflakePolarsTypeHandler->>Snowflake: DELETE partition slice
        SnowflakePolarsTypeHandler->>Snowflake: write_database append
    else table not found
        SnowflakePolarsTypeHandler->>Snowflake: write_database replace
    end
Loading

Reviews (3): Last reviewed commit: "Fix snowflake schema existence test asse..." | Re-trigger Greptile

Comment on lines 372 to 375
cursor.execute(
f"show schemas like '{table_slice.schema}' in database {table_slice.database}"
f"SELECT 1 FROM {table_slice.database}.information_schema.schemata"
f" WHERE LOWER(schema_name) = LOWER('{table_slice.schema}')"
)

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.

P1 security SQL injection via table_slice.schema — the value is interpolated directly into the query string without any sanitization. Although schema names typically come from Dagster asset configuration, a schema name containing a single quote (e.g. it's_schema) will break the query with a syntax error, and a maliciously crafted name could manipulate the WHERE clause. Prefer a parameterized query (:1 or %s depending on the cursor API) or at minimum escape/quote the identifier.

Suggested change
cursor.execute(
f"show schemas like '{table_slice.schema}' in database {table_slice.database}"
f"SELECT 1 FROM {table_slice.database}.information_schema.schemata"
f" WHERE LOWER(schema_name) = LOWER('{table_slice.schema}')"
)
cursor.execute(
f"SELECT 1 FROM {table_slice.database}.information_schema.schemata"
" WHERE LOWER(schema_name) = LOWER(%s)",
(table_slice.schema,),
)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch. Fixed in 9d61e03 -- both ensure_schema_exists and _table_exists now use %s parameterized queries instead of f-string interpolation.

Comment on lines 21 to 25
cursor.execute(
f"SHOW TABLES LIKE '{table_slice.table}' IN SCHEMA"
f" {table_slice.database}.{table_slice.schema}"
f"SELECT 1 FROM {table_slice.database}.information_schema.tables"
f" WHERE LOWER(table_schema) = LOWER('{table_slice.schema}')"
f" AND LOWER(table_name) = LOWER('{table_slice.table}')"
)

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.

P1 security Same SQL injection risk as in ensure_schema_exists: table_slice.schema and table_slice.table are interpolated directly into the query string. A schema or table name containing a single quote causes a syntax error; a crafted name could manipulate the WHERE clause. Use a parameterized query or escape identifiers properly.

Suggested change
cursor.execute(
f"SHOW TABLES LIKE '{table_slice.table}' IN SCHEMA"
f" {table_slice.database}.{table_slice.schema}"
f"SELECT 1 FROM {table_slice.database}.information_schema.tables"
f" WHERE LOWER(table_schema) = LOWER('{table_slice.schema}')"
f" AND LOWER(table_name) = LOWER('{table_slice.table}')"
)
cursor.execute(
f"SELECT 1 FROM {table_slice.database}.information_schema.tables"
" WHERE LOWER(table_schema) = LOWER(%s)"
" AND LOWER(table_name) = LOWER(%s)",
(table_slice.schema, table_slice.table),
)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 527dc6e -- switched to %s parameterized queries and also added a TABLE_TYPE = 'BASE TABLE' filter to exclude views from the existence check.

Addresses review feedback:
- Replace f-string interpolation with %s parameterized queries to
  prevent SQL injection in the _table_exists function
- Add TABLE_TYPE = 'BASE TABLE' filter to exclude views from the
  existence check, since information_schema.tables includes views
  while SHOW TABLES (the previous approach) did not
Replace f-string interpolation with %s parameterized query in
ensure_schema_exists to prevent SQL injection, consistent with
the _table_exists fix in the polars type handler.
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.

UnknownTimeZoneError in SnowflakeIOManager when using SnowflakePolarsIOManager locally on Windows

1 participant