You are viewing a plain text version of this content. The canonical link for it is here.
Posted to github@datafusion.apache.org by "dvasdekis (via GitHub)" <gi...@apache.org> on 2026/10/02 01:06:59 UTC

[PR] MSSQL: Add typed TRY_PARSE expressions [datafusion-sqlparser-rs]

dvasdekis opened a new pull request, #2608:
URL: https://github.com/apache/datafusion-sqlparser-rs/pull/2608

   MSSQL: Add typed TRY_PARSE expressions
   
   `SELECT TRY_PARSE(value AS DECIMAL(10,2) USING 'en-US')` currently fails to parse.
   
   Add a dialect capability and a dedicated AST node retaining the input, target type, and optional culture.
   Support SQL formatting, child spans, visitor traversal, and serde round-trips.
   Reject malformed calls without falling back to ordinary function parsing; preserve bare identifiers and other dialects.
   
   Syntax reference: https://learn.microsoft.com/en-us/sql/t-sql/functions/try-parse-transact-sql
   
   Validation: `cargo test --all-features`, formatting, strict all-target/all-feature Clippy, and a bounded parser fuzz run.
   All examples and fixtures are synthetic.
   Prepared with GitHub Copilot assistance.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org


Re: [PR] MSSQL: Add typed TRY_PARSE expressions [datafusion-sqlparser-rs]

Posted by "LucaCappelletti94 (via GitHub)" <gi...@apache.org>.
LucaCappelletti94 commented on code in PR #2608:
URL: https://github.com/apache/datafusion-sqlparser-rs/pull/2608#discussion_r4163892664


##########
src/dialect/mod.rs:
##########
@@ -1284,6 +1284,11 @@ pub trait Dialect: Debug + Any {
         false
     }
 
+    /// Returns true for `TRY_PARSE(expr AS data_type [USING culture])`.

Review Comment:
   Add link to syntax



##########
tests/sqlparser_mssql.rs:
##########
@@ -2963,3 +2963,143 @@ fn parse_create_proc() {
         .expect_err("PROC should remain MSSQL-specific");
     ms_and_generic().verified_stmt("SELECT proc FROM jobs");
 }
+
+#[test]
+fn parse_try_parse() {
+    let expr = ms().verified_expr("TRY_PARSE(value AS DECIMAL(10,2) USING culture)");
+    let Expr::TryParse {
+        expr,
+        data_type,
+        culture,
+    } = expr
+    else {
+        panic!("expected TRY_PARSE");
+    };
+    assert_eq!(*expr, Expr::Identifier(Ident::new("value")));
+    assert_eq!(data_type.to_string(), "DECIMAL(10,2)");
+    assert_eq!(
+        culture,
+        Some(Box::new(Expr::Identifier(Ident::new("culture"))))
+    );
+
+    for sql in [
+        "TRY_PARSE('123' AS INT)",
+        "TRY_PARSE(N'31/12/2025' AS DATE USING 'en-GB')",
+        "TRY_PARSE(value AS FLOAT USING @culture)",
+        "TRY_PARSE(CONCAT(prefix, value) AS MONEY USING COALESCE(@culture, 'en-US'))",
+        "TRY_PARSE(TRY_PARSE(value AS INT) AS BIGINT)",
+    ] {
+        assert!(matches!(ms().verified_expr(sql), Expr::TryParse { .. }));
+    }
+    let Expr::TryParse { culture, .. } = ms().verified_expr("TRY_PARSE(value AS INT)") else {
+        unreachable!()
+    };
+    assert!(culture.is_none());
+    ms().expr_parses_to(
+        "try_parse(value as int using 'en-US')",
+        "TRY_PARSE(value AS INT USING 'en-US')",
+    );
+}
+
+#[test]
+fn parse_try_parse_rejects_malformed_arguments() {
+    for sql in [
+        "SELECT TRY_PARSE()",
+        "SELECT TRY_PARSE(value)",
+        "SELECT TRY_PARSE(value AS)",
+        "SELECT TRY_PARSE(value AS INT USING)",
+        "SELECT TRY_PARSE(value AS INT USING 'en-US', 'en-GB')",
+        "SELECT TRY_PARSE(value AS INT USING 'en-US' USING 'en-GB')",
+        "SELECT TRY_PARSE(value AS INT",
+    ] {
+        assert!(ms().parse_sql_statements(sql).is_err(), "{sql}");
+    }
+}
+
+#[test]
+fn parse_try_parse_is_dialect_gated() {
+    let sql = "SELECT TRY_PARSE(value AS INT USING 'en-US')";
+    assert!(Parser::parse_sql(&GenericDialect {}, sql).is_err());
+    ms_and_generic().verified_stmt("SELECT TRY_PARSE FROM t");
+    let generic = TestedDialects::new(vec![Box::new(GenericDialect {})]);
+    assert!(matches!(
+        generic.verified_expr("TRY_PARSE(value)"),
+        Expr::Function(_)
+    ));
+    assert!(matches!(
+        ms().verified_expr("TRY_CAST(value AS INT)"),
+        Expr::Cast { .. }
+    ));
+    assert!(matches!(
+        ms().verified_expr("TRY_CONVERT(INT, value)"),
+        Expr::Convert { is_try: true, .. }
+    ));
+}
+
+#[test]
+fn try_parse_child_spans() {
+    let sql = "SELECT TRY_PARSE(source_value AS INT USING culture_value)";
+    let statements = Parser::parse_sql(&MsSqlDialect {}, sql).unwrap();
+    let Statement::Query(query) = &statements[0] else {
+        unreachable!()
+    };
+    let expr = expr_from_projection(&query.body.as_select().unwrap().projection[0]);
+    let Expr::TryParse {
+        expr: input,
+        culture,
+        ..
+    } = expr
+    else {
+        unreachable!()
+    };
+    let input_start = sql.find("source_value").unwrap() as u64 + 1;
+    let culture_start = sql.find("culture_value").unwrap() as u64 + 1;
+    assert_eq!(
+        input.span(),
+        Span::new(
+            Location::new(1, input_start),
+            Location::new(1, input_start + 12)
+        )
+    );
+    assert_eq!(
+        culture.as_ref().unwrap().span(),
+        Span::new(
+            Location::new(1, culture_start),
+            Location::new(1, culture_start + 13)
+        )
+    );
+    assert_eq!(
+        expr.span(),
+        input.span().union(&culture.as_ref().unwrap().span())
+    );
+}
+
+#[cfg(feature = "visitor")]
+#[test]
+fn try_parse_visits_input_and_culture() {
+    use core::ops::ControlFlow;
+
+    #[derive(Default)]
+    struct Identifiers(Vec<String>);
+    impl Visitor for Identifiers {
+        type Break = ();
+        fn pre_visit_expr(&mut self, expr: &Expr) -> ControlFlow<Self::Break> {
+            if let Expr::Identifier(identifier) = expr {
+                self.0.push(identifier.value.clone());
+            }
+            ControlFlow::Continue(())
+        }
+    }
+    let expr = ms().verified_expr("TRY_PARSE(value AS INT USING culture)");
+    let mut identifiers = Identifiers::default();
+    assert_eq!(expr.visit(&mut identifiers), ControlFlow::Continue(()));
+    assert_eq!(identifiers.0, vec!["value", "culture"]);
+}
+
+#[cfg(all(feature = "serde", feature = "json_example"))]

Review Comment:
   This is not a feature meant to be used in tests.



##########
src/parser/mod.rs:
##########
@@ -1753,6 +1753,15 @@ impl<'a> Parser<'a> {
             return prefix;
         }
 
+        if self.dialect.supports_try_parse()
+            && matches!(&self.peek_token_ref().token, Token::Word(word)

Review Comment:
   Unless I am mistaken, here you can directly use the utility to peek for the keyword.



##########
tests/sqlparser_mssql.rs:
##########
@@ -2963,3 +2963,143 @@ fn parse_create_proc() {
         .expect_err("PROC should remain MSSQL-specific");
     ms_and_generic().verified_stmt("SELECT proc FROM jobs");
 }
+
+#[test]
+fn parse_try_parse() {
+    let expr = ms().verified_expr("TRY_PARSE(value AS DECIMAL(10,2) USING culture)");
+    let Expr::TryParse {
+        expr,
+        data_type,
+        culture,
+    } = expr
+    else {
+        panic!("expected TRY_PARSE");
+    };
+    assert_eq!(*expr, Expr::Identifier(Ident::new("value")));
+    assert_eq!(data_type.to_string(), "DECIMAL(10,2)");
+    assert_eq!(
+        culture,
+        Some(Box::new(Expr::Identifier(Ident::new("culture"))))
+    );
+
+    for sql in [
+        "TRY_PARSE('123' AS INT)",
+        "TRY_PARSE(N'31/12/2025' AS DATE USING 'en-GB')",
+        "TRY_PARSE(value AS FLOAT USING @culture)",
+        "TRY_PARSE(CONCAT(prefix, value) AS MONEY USING COALESCE(@culture, 'en-US'))",
+        "TRY_PARSE(TRY_PARSE(value AS INT) AS BIGINT)",
+    ] {
+        assert!(matches!(ms().verified_expr(sql), Expr::TryParse { .. }));
+    }
+    let Expr::TryParse { culture, .. } = ms().verified_expr("TRY_PARSE(value AS INT)") else {
+        unreachable!()
+    };
+    assert!(culture.is_none());
+    ms().expr_parses_to(
+        "try_parse(value as int using 'en-US')",
+        "TRY_PARSE(value AS INT USING 'en-US')",
+    );
+}
+
+#[test]
+fn parse_try_parse_rejects_malformed_arguments() {
+    for sql in [
+        "SELECT TRY_PARSE()",
+        "SELECT TRY_PARSE(value)",
+        "SELECT TRY_PARSE(value AS)",
+        "SELECT TRY_PARSE(value AS INT USING)",
+        "SELECT TRY_PARSE(value AS INT USING 'en-US', 'en-GB')",
+        "SELECT TRY_PARSE(value AS INT USING 'en-US' USING 'en-GB')",
+        "SELECT TRY_PARSE(value AS INT",
+    ] {
+        assert!(ms().parse_sql_statements(sql).is_err(), "{sql}");
+    }
+}
+
+#[test]
+fn parse_try_parse_is_dialect_gated() {
+    let sql = "SELECT TRY_PARSE(value AS INT USING 'en-US')";
+    assert!(Parser::parse_sql(&GenericDialect {}, sql).is_err());
+    ms_and_generic().verified_stmt("SELECT TRY_PARSE FROM t");
+    let generic = TestedDialects::new(vec![Box::new(GenericDialect {})]);
+    assert!(matches!(
+        generic.verified_expr("TRY_PARSE(value)"),
+        Expr::Function(_)
+    ));
+    assert!(matches!(
+        ms().verified_expr("TRY_CAST(value AS INT)"),
+        Expr::Cast { .. }
+    ));
+    assert!(matches!(
+        ms().verified_expr("TRY_CONVERT(INT, value)"),
+        Expr::Convert { is_try: true, .. }
+    ));
+}
+
+#[test]
+fn try_parse_child_spans() {
+    let sql = "SELECT TRY_PARSE(source_value AS INT USING culture_value)";
+    let statements = Parser::parse_sql(&MsSqlDialect {}, sql).unwrap();
+    let Statement::Query(query) = &statements[0] else {
+        unreachable!()
+    };
+    let expr = expr_from_projection(&query.body.as_select().unwrap().projection[0]);
+    let Expr::TryParse {
+        expr: input,
+        culture,
+        ..
+    } = expr
+    else {
+        unreachable!()
+    };
+    let input_start = sql.find("source_value").unwrap() as u64 + 1;
+    let culture_start = sql.find("culture_value").unwrap() as u64 + 1;
+    assert_eq!(
+        input.span(),
+        Span::new(
+            Location::new(1, input_start),
+            Location::new(1, input_start + 12)
+        )
+    );
+    assert_eq!(
+        culture.as_ref().unwrap().span(),
+        Span::new(
+            Location::new(1, culture_start),
+            Location::new(1, culture_start + 13)
+        )
+    );
+    assert_eq!(
+        expr.span(),
+        input.span().union(&culture.as_ref().unwrap().span())
+    );
+}
+
+#[cfg(feature = "visitor")]

Review Comment:
   I do not believe this is a meaningful test to include.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscribe@datafusion.apache.org
For additional commands, e-mail: github-help@datafusion.apache.org