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 18:53:39 UTC
(datafusion-sqlparser-rs) 01/01: Gate infix colon JSON access and literal/placeholder field access by dialect
This is an automated email from the ASF dual-hosted git repository.
LucaCappelletti94 pushed a commit to branch upstream/colon-access-dialect-gate
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
commit c308be781107ed042c2f368d65519b39da2a6194
Author: LucaCappelletti94 <ca...@gmail.com>
AuthorDate: Fri Oct 2 20:52:52 2026 +0200
Gate infix colon JSON access and literal/placeholder field access by dialect
---
derive/src/dialect.rs | 2 +-
src/dialect/databricks.rs | 4 ++++
src/dialect/generic.rs | 4 ++++
src/dialect/mod.rs | 30 ++++++++++++++++++++++--------
src/dialect/postgresql.rs | 8 ++++++++
src/dialect/snowflake.rs | 4 ++++
src/dialect/sqlite.rs | 9 ++++++++-
src/parser/mod.rs | 12 +++++++++---
tests/sqlparser_common.rs | 31 +++++++++++++++++++++++++------
tests/sqlparser_postgres.rs | 29 +++++++++++++++++++++++++++++
tests/sqlparser_sqlite.rs | 14 +++++++++++++-
11 files changed, 127 insertions(+), 20 deletions(-)
diff --git a/derive/src/dialect.rs b/derive/src/dialect.rs
index af17067a..0aad90ae 100644
--- a/derive/src/dialect.rs
+++ b/derive/src/dialect.rs
@@ -229,7 +229,7 @@ fn generate_derived_dialect(input: &DeriveDialectInput, methods: &[DialectMethod
const _: () = {
use ::core::iter::Peekable;
use ::core::str::Chars;
- use sqlparser::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectNamePart, Statement};
+ use sqlparser::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectNamePart, Statement, Value};
use sqlparser::dialect::{Dialect, Precedence};
use sqlparser::keywords::Keyword;
use sqlparser::parser::{Parser, ParserError};
diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs
index ca8610dd..9efd03e8 100644
--- a/src/dialect/databricks.rs
+++ b/src/dialect/databricks.rs
@@ -48,6 +48,10 @@ impl Dialect for DatabricksDialect {
true
}
+ fn supports_semi_structured_data_traversal(&self) -> bool {
+ true
+ }
+
fn supports_filter_during_aggregation(&self) -> bool {
true
}
diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs
index 09a55185..b83b15b3 100644
--- a/src/dialect/generic.rs
+++ b/src/dialect/generic.rs
@@ -28,6 +28,10 @@ impl Dialect for GenericDialect {
true
}
+ fn supports_semi_structured_data_traversal(&self) -> bool {
+ true
+ }
+
fn is_delimited_identifier_start(&self, ch: char) -> bool {
ch == '"' || ch == '`'
}
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index 2beb5655..2534105d 100644
--- a/src/dialect/mod.rs
+++ b/src/dialect/mod.rs
@@ -132,7 +132,7 @@ pub use self::teradata::TeradataDialect;
#[cfg(feature = "derive-dialect")]
pub use sqlparser_derive::derive_dialect;
-use crate::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectNamePart, Statement};
+use crate::ast::{ColumnOption, Expr, GranteesType, Ident, ObjectNamePart, Statement, Value};
pub use crate::keywords;
use crate::keywords::Keyword;
use crate::parser::{Parser, ParserError};
@@ -973,13 +973,15 @@ pub trait Dialect: Debug + Any {
Token::DoubleColon | Token::ExclamationMark | Token::LBracket | Token::CaretAt => {
Ok(p!(DoubleColon))
}
- Token::Colon => match &parser.peek_nth_token_ref(1).token {
- // When colon is followed by a string or a number, it's usually in MAP syntax.
- Token::SingleQuotedString(_) | Token::Number(_, _) => Ok(self.prec_unknown()),
- // In other cases, it's used in semi-structured data traversal like in variant or JSON
- // string columns. See `JsonAccess`.
- _ => Ok(p!(Colon)),
- },
+ Token::Colon if self.supports_semi_structured_data_traversal() => {
+ match &parser.peek_nth_token_ref(1).token {
+ // When colon is followed by a string or a number, it's usually in MAP syntax.
+ Token::SingleQuotedString(_) | Token::Number(_, _) => Ok(self.prec_unknown()),
+ // In other cases, it's used in semi-structured data traversal like in variant or JSON
+ // string columns. See `JsonAccess`.
+ _ => Ok(p!(Colon)),
+ }
+ }
Token::Arrow
| Token::LongArrow
| Token::HashArrow
@@ -1361,6 +1363,18 @@ pub trait Dialect: Debug + Any {
false
}
+ /// Returns true if the dialect supports traversing semi-structured data (JSON,
+ /// variant, etc.) with the infix `:` operator, e.g. `a:b`.
+ fn supports_semi_structured_data_traversal(&self) -> bool {
+ false
+ }
+
+ /// Returns true if `value` may be the root of a `.field` or `[index]`
+ /// access chain under this dialect.
+ fn supports_value_access(&self, _value: &Value) -> bool {
+ true
+ }
+
/// Returns true if the dialect supports object-unpivot table factors in the FROM clause.
///
/// Syntax:
diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs
index 3bec6ceb..93b92023 100644
--- a/src/dialect/postgresql.rs
+++ b/src/dialect/postgresql.rs
@@ -28,6 +28,7 @@
// limitations under the License.
use log::debug;
+use crate::ast::Value;
use crate::dialect::{Dialect, Precedence};
use crate::keywords::Keyword;
use crate::parser::{Parser, ParserError};
@@ -368,4 +369,11 @@ impl Dialect for PostgreSqlDialect {
fn supports_comment_optimizer_hint(&self) -> bool {
true
}
+
+ /// Postgres' `c_expr: PARAM opt_indirection` grammar allows `.field`/`[index]`
+ /// on a numbered parameter (`$1`), but not on other placeholder forms, on
+ /// string/boolean/null literals, or on a bare numeric literal.
+ fn supports_value_access(&self, value: &Value) -> bool {
+ matches!(value, Value::Placeholder(name) if name.starts_with('$'))
+ }
}
diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs
index 3ebc1686..cb02f2c6 100644
--- a/src/dialect/snowflake.rs
+++ b/src/dialect/snowflake.rs
@@ -507,6 +507,10 @@ impl Dialect for SnowflakeDialect {
true
}
+ fn supports_semi_structured_data_traversal(&self) -> bool {
+ true
+ }
+
fn is_column_alias(&self, kw: &Keyword, parser: &mut Parser) -> bool {
match kw {
// The following keywords can be considered an alias as long as
diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs
index 13bc555a..27d0cdbb 100644
--- a/src/dialect/sqlite.rs
+++ b/src/dialect/sqlite.rs
@@ -19,7 +19,7 @@
use alloc::boxed::Box;
use crate::ast::BinaryOperator;
-use crate::ast::{Expr, Statement};
+use crate::ast::{Expr, Statement, Value};
use crate::dialect::Dialect;
use crate::keywords::Keyword;
use crate::parser::{Parser, ParserError};
@@ -145,4 +145,11 @@ impl Dialect for SQLiteDialect {
fn supports_national_string_literal(&self) -> bool {
false
}
+
+ /// SQLite's grammar takes a quoted string as a table qualifier
+ /// (`nm ::= id | STRING`), so a string-literal root is a valid `.field`
+ /// access root; bind parameters and bare numeric literals aren't.
+ fn supports_value_access(&self, value: &Value) -> bool {
+ matches!(value, Value::SingleQuotedString(_))
+ }
}
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 1a46c6e5..3d647fa6 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -2082,8 +2082,13 @@ impl<'a> Parser<'a> {
mut chain: Vec<AccessExpr>,
) -> Result<Expr, ParserError> {
let mut ending_wildcard: Option<TokenWithSpan> = None;
+ // Whether `root` is allowed to start a `.field` / `[index]` access chain under this dialect.
+ let root_value_access_allowed = match &root {
+ Expr::Value(v) => self.dialect.supports_value_access(&v.value),
+ _ => true,
+ };
loop {
- if self.consume_token(&Token::Period) {
+ if root_value_access_allowed && self.consume_token(&Token::Period) {
let next_token = self.peek_token_ref();
match &next_token.token {
Token::Mul => {
@@ -2183,7 +2188,8 @@ impl<'a> Parser<'a> {
}
}
}
- } else if !self.dialect.supports_partiql()
+ } else if root_value_access_allowed
+ && !self.dialect.supports_partiql()
&& self.peek_token_ref().token == Token::LBracket
{
self.parse_multi_dim_subscript(&mut chain)?;
@@ -4262,7 +4268,7 @@ impl<'a> Parser<'a> {
expr: Box::new(expr),
})
} else if Token::LBracket == *tok && self.dialect.supports_partiql()
- || (Token::Colon == *tok)
+ || (Token::Colon == *tok && self.dialect.supports_semi_structured_data_traversal())
{
self.prev_token();
self.parse_json_access(expr)
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index 8e08c657..f0d06eed 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -20242,12 +20242,15 @@ fn parse_alter_table_column_position() {
#[test]
fn parse_compound_field_access_numeric_display() {
- all_dialects().verified_stmt("SELECT 1 . i");
- all_dialects().verified_stmt("SELECT (1 . i)");
- all_dialects().verified_stmt("SELECT 1 . 2");
- all_dialects().verified_stmt("SELECT (a . 1 . b)");
- all_dialects().verified_stmt("SELECT 1 . i.j");
- all_dialects().verified_stmt("SELECT 1 . 2 . 3");
+ // PostgreSQL and SQLite reject a bare numeric literal as a `.field` access root
+ // (see `parse_indirection_rejects_non_param_value_roots`, `parse_placeholder_field_access_rejected`).
+ let dialects = all_dialects_except(|d| d.is::<PostgreSqlDialect>() || d.is::<SQLiteDialect>());
+ dialects.verified_stmt("SELECT 1 . i");
+ dialects.verified_stmt("SELECT (1 . i)");
+ dialects.verified_stmt("SELECT 1 . 2");
+ dialects.verified_stmt("SELECT (a . 1 . b)");
+ dialects.verified_stmt("SELECT 1 . i.j");
+ dialects.verified_stmt("SELECT 1 . 2 . 3");
all_dialects().verified_stmt("SELECT a.b.c");
let err = all_dialects_where(|d| !d.supports_numeric_prefix())
@@ -20433,3 +20436,19 @@ 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_colon_json_access_requires_dialect_support() {
+ let unsupported = all_dialects_where(|d| !d.supports_semi_structured_data_traversal());
+ assert_eq!(
+ unsupported
+ .parse_sql_statements("SELECT a:b FROM t")
+ .unwrap_err(),
+ ParserError::ParserError("Expected: end of statement, found: :".to_string())
+ );
+
+ // The infix `:` operator is unrelated to `::` casts and `:name` placeholders,
+ // both of which every dialect still accepts.
+ unsupported.verified_stmt("SELECT a::INT FROM t");
+ unsupported.verified_stmt("SELECT :name");
+}
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index fb998510..0a3c608a 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -10126,3 +10126,32 @@ fn parse_bitstring_literal_escaping() {
pg_and_generic().verified_stmt("SELECT B''''");
pg_and_generic().verified_stmt("SELECT B'it''s'");
}
+
+#[test]
+fn parse_param_indirection() {
+ // `c_expr: PARAM opt_indirection` allows `.field`/`[index]` on a numbered parameter.
+ pg().verified_stmt("SELECT $1.a");
+ pg().verified_stmt("SELECT $1[1]");
+ pg().verified_stmt("SELECT $1[1].a");
+}
+
+#[test]
+fn parse_indirection_rejects_non_param_value_roots() {
+ // Unlike `$n`, string literals, bare numbers and `:name` placeholders don't carry `opt_indirection`.
+ for sql in [
+ "SELECT 'abc'.foo",
+ "SELECT c = :NA.o FROM t",
+ "SELECT 2 .id FROM t",
+ ] {
+ assert_eq!(
+ pg().parse_sql_statements(sql).unwrap_err(),
+ ParserError::ParserError("Expected: end of statement, found: .".to_string()),
+ );
+ }
+ for sql in ["SELECT 'abc'[1]", "SELECT c = :NA[0] FROM t"] {
+ assert_eq!(
+ pg().parse_sql_statements(sql).unwrap_err(),
+ ParserError::ParserError("No infix parser for token LBracket".to_string()),
+ );
+ }
+}
diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs
index 81059761..72e744ab 100644
--- a/tests/sqlparser_sqlite.rs
+++ b/tests/sqlparser_sqlite.rs
@@ -398,7 +398,19 @@ fn parse_create_table_with_strict() {
#[test]
fn parse_single_quoted_identified() {
sqlite().verified_only_select("SELECT 't'.*, t.'x' FROM 't'");
- // TODO: add support for select 't'.x
+ // SQLite's `nm ::= id | STRING` grammar takes a quoted string as a table qualifier.
+ sqlite().verified_only_select("SELECT 't'.x FROM 't'");
+}
+
+#[test]
+fn parse_field_access_rejects_non_string_value_roots() {
+ // Bind parameters and bare numeric literals are scalar, so they can't root a `.field` access chain.
+ for sql in ["SELECT c = :NA.o FROM t", "SELECT 2 .id FROM t"] {
+ assert_eq!(
+ sqlite().parse_sql_statements(sql).unwrap_err(),
+ ParserError::ParserError("Expected: end of statement, found: .".to_string())
+ );
+ }
}
#[test]
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org