Skip to content

feat(duckdb): Add transpilation support for BOOLEAN and TEXT cases of TRY_CAST function#7681

Open
fivetran-amrutabhimsenayachit wants to merge 1 commit into
mainfrom
RD-1069322-try-cast-boolean-text
Open

feat(duckdb): Add transpilation support for BOOLEAN and TEXT cases of TRY_CAST function#7681
fivetran-amrutabhimsenayachit wants to merge 1 commit into
mainfrom
RD-1069322-try-cast-boolean-text

Conversation

@fivetran-amrutabhimsenayachit
Copy link
Copy Markdown
Collaborator

@fivetran-amrutabhimsenayachit fivetran-amrutabhimsenayachit commented May 26, 2026

PR#1
Refer #7672 for comments.

BOOLEAN: Snowflake's TRY_CAST(x AS BOOLEAN) accepts 'on'/'off' and returns TRUE/FALSE, but DuckDB's native TRY_CAST(x AS BOOLEAN) returns NULL for those two values silently.

TEXT(n): Snowflake's TRY_CAST(x AS VARCHAR(n)) returns NULL if the string exceeds length n, but DuckDB's native TRY_CAST(x AS VARCHAR(n)) silently ignores the length constraint and returns the full string.

After fixing the above,
Boolean:

SF:
SELECT TRY_CAST('on' AS BOOLEAN), TRY_CAST('off' AS BOOLEAN), TRY_CAST('true' AS BOOLEAN), TRY_CAST('false' AS BOOLEAN), TRY_CAST('yes' AS BOOLEAN), TRY_CAST('invalid' AS BOOLEAN), TRY_CAST(NULL AS BOOLEAN);
+-----------------------------------------------------------------------------------------------------------------------------------------------------------+
| TRY_CAST('ON' AS    | TRY_CAST('OFF' AS   | TRY_CAST('TRUE' AS  | TRY_CAST('FALSE' AS | TRY_CAST('YES' AS    | TRY_CAST('INVALID'  | TRY_CAST(NULL AS     |
| BOOLEAN)            | BOOLEAN)            | BOOLEAN)            | BOOLEAN)            | BOOLEAN)             | AS BOOLEAN)         | BOOLEAN)             |
|---------------------+---------------------+---------------------+---------------------+----------------------+---------------------+----------------------|
| True                | False               | True                | False               | True                 | None                | None                 |
+-----------------------------------------------------------------------------------------------------------------------------------------------------------+

DDB:
┌───────────────────────┬───────────────────────┬───────────────────────┬──────────────────────┬──────────────────────┬───────────────────────┐
│ CASE  WHEN ((upper(CA │ CASE  WHEN ((upper(CA │ CASE  WHEN ((upper(CA │ CASE  WHEN ((upper(C │ CASE  WHEN ((upper(C │ CASE  WHEN ((upper(CA │
│ ST('on' AS VARCHAR))  │ ST('off' AS VARCHAR)) │ ST('true' AS VARCHAR) │ AST('false' AS VARCH │ AST('invalid' AS VAR │ ST(NULL AS VARCHAR))  │
│ = 'ON')) THEN (CAST(' │  = 'ON')) THEN (CAST( │ ) = 'ON')) THEN (CAST │ AR)) = 'ON')) THEN ( │ CHAR)) = 'ON')) THEN │ = 'ON')) THEN (CAST(' │
│ t' AS BOOLEAN)) WHEN  │ 't' AS BOOLEAN)) WHEN │ ('t' AS BOOLEAN)) WHE │ CAST('t' AS BOOLEAN) │  (CAST('t' AS BOOLEA │ t' AS BOOLEAN)) WHEN  │
│ ((upper(CAST('on' AS  │  ((upper(CAST('off' A │ N ((upper(CAST('true' │ ) WHEN ((upper(CAST( │ N)) WHEN ((upper(CAS │ ((upper(CAST(NULL AS  │
│ VARCHAR)) = 'OFF')) T │ S VARCHAR)) = 'OFF')) │  AS VARCHAR)) = 'OFF' │ 'false' AS VARCHAR)) │ T('invalid' AS VARCH │ VARCHAR)) = 'OFF')) T │
│ HEN (CAST('f' AS BOOL │  THEN (CAST('f' AS BO │ )) THEN (CAST('f' AS  │  = 'OFF')) THEN (CAS │ AR)) = 'OFF')) THEN  │ HEN (CAST('f' AS BOOL │
│ EAN)) ELSE TRY_CAST(' │ OLEAN)) ELSE TRY_CAST │ BOOLEAN)) ELSE TRY_CA │ T('f' AS BOOLEAN)) E │ (CAST('f' AS BOOLEAN │ EAN)) ELSE TRY_CAST(N │
│  on' AS BOOLEAN) END  │ ('off' AS BOOLEAN) EN │ ST('true' AS BOOLEAN) │ LSE TRY_CAST('false' │ )) ELSE TRY_CAST('in │  ULL AS BOOLEAN) END  │
│                       │           D           │          END          │    AS BOOLEAN) END   │ valid' AS BOOLEAN) E │                       │
│                       │                       │                       │                      │          ND          │                       │
│        boolean        │        boolean        │        boolean        │       boolean        │       boolean        │        boolean        │
├───────────────────────┼───────────────────────┼───────────────────────┼──────────────────────┼──────────────────────┼───────────────────────┤
│ true                  │ false                 │ true                  │ false                │ NULL                 │ NULL                  │
└───────────────────────┴───────────────────────┴───────────────────────┴──────────────────────┴──────────────────────┴───────────────────────┘

Text:

SF:
SELECT
  CASE WHEN LENGTH('hello') <= 3 THEN CAST('hello' AS TEXT) ELSE NULL END,
  CASE WHEN LENGTH('hi') <= 3 THEN CAST('hi' AS TEXT) ELSE NULL END,
  CASE WHEN LENGTH(NULL) <= 3 THEN CAST(NULL AS TEXT) ELSE NULL END;
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| CASE WHEN LENGTH('HELLO') <= 3 THEN CAST('HELLO' AS TEXT) ELSE NULL END | CASE WHEN LENGTH('HI') <= 3 THEN CAST('HI' AS TEXT) ELSE NULL END | CASE WHEN LENGTH(NULL) <= 3 THEN CAST(NULL AS TEXT) ELSE NULL END |
|-------------------------------------------------------------------------+-------------------------------------------------------------------+-------------------------------------------------------------------|
| None                                                                    | hi                                                                | None                                                              |
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+


DDB:
"SELECT CASE WHEN LENGTH('hello') <= 3 THEN CAST('hello' AS TEXT) ELSE NULL END, CASE WHEN LENGTH('hi') <= 3 THEN CAST('hi' AS TEXT) ELSE NULL END, CASE WHEN LENGTH(NULL) <= 3 THEN CAST(NULL AS TEXT) ELSE NULL END"
┌───────────────────────────────────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────┐
│ CASE  WHEN ((length('hello') <= 3)) THEN (CAST('hello' AS VARCHAR)) ELSE NULL END │ CASE  WHEN ((length('hi') <= 3)) THEN (CAST('hi' AS VARCHAR)) ELSE NULL END │ CASE  WHEN ((length(NULL) <= 3)) THEN (CAST(NULL AS VARCHAR)) ELSE NULL END │
│                                      varchar                                      │                                   varchar                                   │                                   varchar                                   │
├───────────────────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
│ NULL                                                                              │ hi                                                                          │ NULL                                                                        │

Comment thread sqlglot/generators/duckdb.py Outdated
Comment thread sqlglot/generators/duckdb.py Outdated
Comment thread sqlglot/generators/duckdb.py Outdated
Comment thread sqlglot/generators/duckdb.py Outdated
@fivetran-amrutabhimsenayachit fivetran-amrutabhimsenayachit force-pushed the RD-1069322-try-cast-boolean-text branch from 66c32b6 to 42ac8f4 Compare May 28, 2026 15:32
@github-actions

This comment was marked as outdated.

Comment thread sqlglot/expressions/functions.py Outdated

class TryCast(Cast):
arg_types = {**Cast.arg_types, "requires_string": False}
arg_types = {**Cast.arg_types, "requires_string": False, "dialect_cast": False}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This isn't very descriptive. We should choose a name that reflects the semantics well. In this case, the behavior we're trying to represent with the new arg is: "do TRY_CASTs to text types (CHAR, VARCHAR, TEXT, etc) with a max length specifier result in NULL, when their argument exceeds said length?"

Comment thread sqlglot/generators/duckdb.py Outdated
Comment on lines +4337 to +4338
if to_type == exp.DType.BOOLEAN:
return _to_boolean_sql(self, exp.ToBoolean(this=src, safe=True))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This should not be handled in DuckDB's trycast_sql. We should instead convert (try-)casts to boolean in Snowflake into ToBoolean nodes, at parse time. From Snowflake's docs:

The semantics of CAST are the same as the semantics of the corresponding TO_ datatype conversion functions.

src = expression.this
to = expression.to
to_type = to.this

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's get rid of the first branch altogether, and assign the constituent args to variables only within the transpilation branch. We should fall back to the base trycast_sql only at the end of the method.

Comment thread sqlglot/parser.py Outdated
Comment on lines +9931 to +9934
if self.dialect.TRY_CAST_REQUIRES_STRING and (to := kwargs.get("to")):
kwargs["dialect_cast"] = to.this == exp.DataType.Type.BOOLEAN or (
to.this in exp.DataType.TEXT_TYPES and bool(to.expressions)
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This isn't right, we should not conflate the two concepts. These properties are orthogonal to each other:

  • TRY_CAST in Snowflake only operates on string values
  • TRY_CAST to text types with a length specifier result in NULL when the value exceeds said length

I would honestly refactor this whole section so that Snowflake overrides build_cast and sets these two kwargs after the fact, when the instance is a TryCast. The TRY_CAST_REQUIRES_STRING seems redundant right now, only Snowflake sets it.

@fivetran-amrutabhimsenayachit fivetran-amrutabhimsenayachit force-pushed the RD-1069322-try-cast-boolean-text branch from 42ac8f4 to 3d63802 Compare May 28, 2026 19:01
… TRY_CAST function [CLAUDE]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@fivetran-amrutabhimsenayachit fivetran-amrutabhimsenayachit force-pushed the RD-1069322-try-cast-boolean-text branch from 3d63802 to 6bf87a6 Compare May 28, 2026 19:08
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.

2 participants