You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by lu...@apache.org on 2026/10/02 19:55:02 UTC

(datafusion-sqlparser-rs) 01/01: Gate qualified $ placeholder field access to Snowflake

This is an automated email from the ASF dual-hosted git repository.

LucaCappelletti94 pushed a commit to branch upstream/qualified-dollar-placeholder-gate
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git

commit 79bb1319e134560d28d0c8d9f0cad4357debe15e
Author: LucaCappelletti94 <ca...@gmail.com>
AuthorDate: Fri Oct 2 21:51:26 2026 +0200

    Gate qualified $ placeholder field access to Snowflake
---
 src/dialect/mod.rs        |  7 +++++++
 src/dialect/snowflake.rs  |  5 +++++
 src/parser/mod.rs         | 17 +++++++++++++----
 tests/sqlparser_common.rs | 38 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 63 insertions(+), 4 deletions(-)

diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index 2beb5655..3b9200d8 100644
--- a/src/dialect/mod.rs
+++ b/src/dialect/mod.rs
@@ -511,6 +511,13 @@ pub trait Dialect: Debug + Any {
         false
     }
 
+    /// Returns true if a `$1`-style placeholder directly after a period is a field
+    /// name, such as the positional column references in Snowflake staged data
+    /// queries `SELECT t.$1 FROM @stage t`.
+    fn supports_placeholder_as_field_name(&self) -> bool {
+        false
+    }
+
     /// Returns true if the dialect supports numbers containing underscores, e.g. `10_000_000`
     fn supports_numeric_literal_underscores(&self) -> bool {
         false
diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs
index 3ebc1686..b927596b 100644
--- a/src/dialect/snowflake.rs
+++ b/src/dialect/snowflake.rs
@@ -698,6 +698,11 @@ impl Dialect for SnowflakeDialect {
         true
     }
 
+    /// See <https://docs.snowflake.com/en/user-guide/querying-stage>
+    fn supports_placeholder_as_field_name(&self) -> bool {
+        true
+    }
+
     /// See <https://docs.snowflake.com/en/sql-reference/sql/select#parameters>
     fn supports_select_wildcard_replace(&self) -> bool {
         true
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 1a46c6e5..986a0949 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -1356,9 +1356,9 @@ impl<'a> Parser<'a> {
                             // SQLite has single-quoted identifiers
                             id_parts.push(Ident::with_quote('\'', s))
                         }
-                        Token::Placeholder(s) => {
-                            // Snowflake uses $1, $2, etc. for positional column references
-                            // in staged data queries like: SELECT t.$1 FROM @stage t
+                        Token::Placeholder(s)
+                            if self.dialect.supports_placeholder_as_field_name() =>
+                        {
                             id_parts.push(Ident::new(s))
                         }
                         Token::Mul => {
@@ -2108,7 +2108,7 @@ impl<'a> Parser<'a> {
                         chain.push(AccessExpr::Dot(expr));
                         self.advance_token(); // The consumed string
                     }
-                    Token::Placeholder(s) => {
+                    Token::Placeholder(s) if self.dialect.supports_placeholder_as_field_name() => {
                         // Snowflake uses $1, $2, etc. for positional column references
                         // in staged data queries like: SELECT t.$1 FROM @stage t
                         let expr = Expr::Identifier(Ident::with_span(next_token.span, s));
@@ -2140,6 +2140,15 @@ impl<'a> Parser<'a> {
                             self.maybe_parse(|parser| {
                                 let expr = parser.parse_prefix()?;
                                 match &expr {
+                                    Expr::Value(ValueWithSpan {
+                                        value: Value::Placeholder(_),
+                                        ..
+                                    }) if !parser.dialect.supports_placeholder_as_field_name() => {
+                                        parser.expected_ref(
+                                            "an identifier or value",
+                                            parser.peek_token_ref(),
+                                        )
+                                    }
                                     Expr::CompoundFieldAccess { .. }
                                     | Expr::CompoundIdentifier(_)
                                     | Expr::Identifier(_)
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index 8e08c657..c3a719e9 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -20433,3 +20433,41 @@ fn parse_bang_not_renders_apart_from_operand() {
     dialects.verified_stmt("SET eaac_cion = ! !o");
     dialects.one_statement_parses_to("SET eaac_cion = ! ! o", "SET eaac_cion = ! !o");
 }
+
+#[test]
+fn parse_placeholder_as_field_name() {
+    let supported = all_dialects_where(|d| d.supports_placeholder_as_field_name());
+    let select = supported.verified_only_select("SELECT t.$1, t.$2 FROM t");
+    assert_eq!(
+        select.projection,
+        vec![
+            SelectItem::UnnamedExpr(Expr::CompoundIdentifier(vec![
+                Ident::new("t"),
+                Ident::new("$1")
+            ])),
+            SelectItem::UnnamedExpr(Expr::CompoundIdentifier(vec![
+                Ident::new("t"),
+                Ident::new("$2")
+            ])),
+        ]
+    );
+    supported.verified_stmt("SELECT (t).$1 FROM t");
+    supported.verified_stmt("SELECT t.$name FROM t");
+
+    let unsupported = all_dialects_where(|d| {
+        !d.supports_placeholder_as_field_name() && !d.is_identifier_start('$')
+    });
+    for sql in [
+        "SELECT t.$name FROM t",
+        "SELECT t.$1 FROM t",
+        "SELECT c.$ame FROM c",
+        "SELECT (t).$1 FROM t",
+        "SELECT (t).$name FROM t",
+    ] {
+        assert!(unsupported.parse_sql_statements(sql).is_err());
+    }
+
+    // Bare placeholders are unaffected by the capability.
+    unsupported.verified_stmt("SELECT $1, $2 FROM t");
+    unsupported.verified_only_select("SELECT * FROM student WHERE id = $Id1");
+}


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