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 on lines +4330 to +4332
if not expression.args.get("requires_string"):
return super().trycast_sql(expression)

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 is unnecessary and irrelevant to DuckDB, it's only used for Snowflake roundtrip.

if not expression.args.get("requires_string"):
return super().trycast_sql(expression)

src, to, to_type = expression.this, expression.to, expression.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.

Nit (style): let's split these across three lines, we don't use the multi-assignment syntax as much.

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

if to_type == exp.DType.BOOLEAN:
return _to_boolean_sql(self, exp.ToBoolean(this=src.copy(), 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.

Why are we copying?

Comment on lines +4337 to +4345
elif to_type in exp.DataType.TEXT_TYPES and to.expressions:
return self.sql(
exp.case()
.when(
exp.LTE(this=exp.func("LENGTH", src.copy()), expression=to.expressions[0].this),
exp.cast(src.copy(), "TEXT"),
)
.else_(exp.Null())
)
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 messes up DuckDB's roundtrip, right? This behavior needs to be controlled with a new arg in TryCast.

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