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

iffyio pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 8de3a629 BigQuery: Add support for select expr star (#1680)
8de3a629 is described below

commit 8de3a62948d3384b9c13a387b0039984d51752fb
Author: Ifeanyi Ubah <[email protected]>
AuthorDate: Tue Jan 28 11:33:07 2025 +0100

    BigQuery: Add support for select expr star (#1680)
---
 src/ast/mod.rs               | 13 +++----
 src/ast/query.rs             | 34 +++++++++++++++---
 src/ast/spans.rs             | 18 +++++++---
 src/dialect/bigquery.rs      |  5 +++
 src/dialect/mod.rs           | 11 ++++++
 src/parser/mod.rs            | 30 ++++++++++++----
 tests/sqlparser_bigquery.rs  | 11 ++++--
 tests/sqlparser_common.rs    | 85 ++++++++++++++++++++++++++++++++++++++++++--
 tests/sqlparser_duckdb.rs    |  2 +-
 tests/sqlparser_snowflake.rs |  4 +--
 10 files changed, 183 insertions(+), 30 deletions(-)

diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index 6917b7c9..d3a028b0 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -68,12 +68,13 @@ pub use self::query::{
     NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, 
OpenJsonTableColumn,
     OrderBy, OrderByExpr, PivotValueSource, ProjectionSelect, Query, 
RenameSelectItem,
     RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, 
RowsPerMatch, Select,
-    SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, 
SymbolDefinition, Table,
-    TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs, 
TableIndexHintForClause,
-    TableIndexHintType, TableIndexHints, TableIndexType, TableSample, 
TableSampleBucket,
-    TableSampleKind, TableSampleMethod, TableSampleModifier, 
TableSampleQuantity, TableSampleSeed,
-    TableSampleSeedModifier, TableSampleUnit, TableVersion, TableWithJoins, 
Top, TopQuantity,
-    UpdateTableFromKind, ValueTableMode, Values, WildcardAdditionalOptions, 
With, WithFill,
+    SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, 
SetOperator, SetQuantifier,
+    Setting, SymbolDefinition, Table, TableAlias, TableAliasColumnDef, 
TableFactor,
+    TableFunctionArgs, TableIndexHintForClause, TableIndexHintType, 
TableIndexHints,
+    TableIndexType, TableSample, TableSampleBucket, TableSampleKind, 
TableSampleMethod,
+    TableSampleModifier, TableSampleQuantity, TableSampleSeed, 
TableSampleSeedModifier,
+    TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity, 
UpdateTableFromKind,
+    ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill,
 };
 
 pub use self::trigger::{
diff --git a/src/ast/query.rs b/src/ast/query.rs
index 09058f76..747d925c 100644
--- a/src/ast/query.rs
+++ b/src/ast/query.rs
@@ -586,6 +586,20 @@ impl fmt::Display for Cte {
     }
 }
 
+/// Represents an expression behind a wildcard expansion in a projection.
+/// `SELECT T.* FROM T;
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub enum SelectItemQualifiedWildcardKind {
+    /// Expression is an object name.
+    /// e.g. `alias.*` or even `schema.table.*`
+    ObjectName(ObjectName),
+    /// Select star on an arbitrary expression.
+    /// e.g. `STRUCT<STRING>('foo').*`
+    Expr(Expr),
+}
+
 /// One item of the comma-separated list following `SELECT`
 #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -595,12 +609,24 @@ pub enum SelectItem {
     UnnamedExpr(Expr),
     /// An expression, followed by `[ AS ] alias`
     ExprWithAlias { expr: Expr, alias: Ident },
-    /// `alias.*` or even `schema.table.*`
-    QualifiedWildcard(ObjectName, WildcardAdditionalOptions),
+    /// An expression, followed by a wildcard expansion.
+    /// e.g. `alias.*`, `STRUCT<STRING>('foo').*`
+    QualifiedWildcard(SelectItemQualifiedWildcardKind, 
WildcardAdditionalOptions),
     /// An unqualified `*`
     Wildcard(WildcardAdditionalOptions),
 }
 
+impl fmt::Display for SelectItemQualifiedWildcardKind {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match &self {
+            SelectItemQualifiedWildcardKind::ObjectName(object_name) => {
+                write!(f, "{object_name}.*")
+            }
+            SelectItemQualifiedWildcardKind::Expr(expr) => write!(f, 
"{expr}.*"),
+        }
+    }
+}
+
 /// Single aliased identifier
 ///
 /// # Syntax
@@ -867,8 +893,8 @@ impl fmt::Display for SelectItem {
         match &self {
             SelectItem::UnnamedExpr(expr) => write!(f, "{expr}"),
             SelectItem::ExprWithAlias { expr, alias } => write!(f, "{expr} AS 
{alias}"),
-            SelectItem::QualifiedWildcard(prefix, additional_options) => {
-                write!(f, "{prefix}.*")?;
+            SelectItem::QualifiedWildcard(kind, additional_options) => {
+                write!(f, "{kind}")?;
                 write!(f, "{additional_options}")?;
                 Ok(())
             }
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index 8f72c26f..58fa27aa 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -15,6 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
+use crate::ast::query::SelectItemQualifiedWildcardKind;
 use core::iter;
 
 use crate::tokenizer::Span;
@@ -1623,16 +1624,23 @@ impl Spanned for JsonPathElem {
     }
 }
 
+impl Spanned for SelectItemQualifiedWildcardKind {
+    fn span(&self) -> Span {
+        match self {
+            SelectItemQualifiedWildcardKind::ObjectName(object_name) => 
object_name.span(),
+            SelectItemQualifiedWildcardKind::Expr(expr) => expr.span(),
+        }
+    }
+}
+
 impl Spanned for SelectItem {
     fn span(&self) -> Span {
         match self {
             SelectItem::UnnamedExpr(expr) => expr.span(),
             SelectItem::ExprWithAlias { expr, alias } => 
expr.span().union(&alias.span),
-            SelectItem::QualifiedWildcard(object_name, 
wildcard_additional_options) => union_spans(
-                object_name
-                    .0
-                    .iter()
-                    .map(|i| i.span())
+            SelectItem::QualifiedWildcard(kind, wildcard_additional_options) 
=> union_spans(
+                [kind.span()]
+                    .into_iter()
                     .chain(iter::once(wildcard_additional_options.span())),
             ),
             SelectItem::Wildcard(wildcard_additional_options) => 
wildcard_additional_options.span(),
diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs
index 71617439..b4575421 100644
--- a/src/dialect/bigquery.rs
+++ b/src/dialect/bigquery.rs
@@ -83,6 +83,11 @@ impl Dialect for BigQueryDialect {
         true
     }
 
+    /// See 
<https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#select_expression_star>
+    fn supports_select_expr_star(&self) -> bool {
+        true
+    }
+
     // See <https://cloud.google.com/bigquery/docs/access-historical-data>
     fn supports_timestamp_versioning(&self) -> bool {
         true
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index 6329c5cf..bc3c0c96 100644
--- a/src/dialect/mod.rs
+++ b/src/dialect/mod.rs
@@ -447,6 +447,17 @@ pub trait Dialect: Debug + Any {
         false
     }
 
+    /// Return true if the dialect supports wildcard expansion on
+    /// arbitrary expressions in projections.
+    ///
+    /// Example:
+    /// ```sql
+    /// SELECT STRUCT<STRING>('foo').* FROM T
+    /// ```
+    fn supports_select_expr_star(&self) -> bool {
+        false
+    }
+
     /// Does the dialect support MySQL-style `'user'@'host'` grantee syntax?
     fn supports_user_host_grantee(&self) -> bool {
         false
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index c8ff01f7..179c120b 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -1528,10 +1528,17 @@ impl<'a> Parser<'a> {
                     // function array_agg traverses this control flow
                     if dialect_of!(self is PostgreSqlDialect) {
                         ending_wildcard = Some(next_token);
-                        break;
                     } else {
-                        return self.expected("an identifier after '.'", 
next_token);
+                        // Put back the consumed .* tokens before exiting.
+                        // If this expression is being parsed in the
+                        // context of a projection, then this could imply
+                        // a wildcard expansion. For example:
+                        // `SELECT STRUCT('foo').* FROM T`
+                        self.prev_token(); // *
+                        self.prev_token(); // .
                     }
+
+                    break;
                 }
                 Token::SingleQuotedString(s) => {
                     let expr = Expr::Identifier(Ident::with_quote('\'', s));
@@ -1568,18 +1575,18 @@ impl<'a> Parser<'a> {
             } else {
                 self.parse_function(ObjectName::from(id_parts))
             }
+        } else if chain.is_empty() {
+            Ok(root)
         } else {
             if Self::is_all_ident(&root, &chain) {
                 return Ok(Expr::CompoundIdentifier(Self::exprs_to_idents(
                     root, chain,
                 )?));
             }
-            if chain.is_empty() {
-                return Ok(root);
-            }
+
             Ok(Expr::CompoundFieldAccess {
                 root: Box::new(root),
-                access_chain: chain.clone(),
+                access_chain: chain,
             })
         }
     }
@@ -12935,7 +12942,7 @@ impl<'a> Parser<'a> {
     pub fn parse_select_item(&mut self) -> Result<SelectItem, ParserError> {
         match self.parse_wildcard_expr()? {
             Expr::QualifiedWildcard(prefix, token) => 
Ok(SelectItem::QualifiedWildcard(
-                prefix,
+                SelectItemQualifiedWildcardKind::ObjectName(prefix),
                 self.parse_wildcard_additional_options(token.0)?,
             )),
             Expr::Wildcard(token) => Ok(SelectItem::Wildcard(
@@ -12965,6 +12972,15 @@ impl<'a> Parser<'a> {
                     alias,
                 })
             }
+            expr if self.dialect.supports_select_expr_star()
+                && self.consume_tokens(&[Token::Period, Token::Mul]) =>
+            {
+                let wildcard_token = self.get_previous_token().clone();
+                Ok(SelectItem::QualifiedWildcard(
+                    SelectItemQualifiedWildcardKind::Expr(expr),
+                    self.parse_wildcard_additional_options(wildcard_token)?,
+                ))
+            }
             expr => self
                 .maybe_parse_select_item_alias()
                 .map(|alias| match alias {
diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs
index 45d87a8b..921a37a8 100644
--- a/tests/sqlparser_bigquery.rs
+++ b/tests/sqlparser_bigquery.rs
@@ -1540,9 +1540,6 @@ fn parse_hyphenated_table_identifiers() {
             ]))
         })
     );
-
-    let error_sql = "select foo-bar.* from foo-bar";
-    assert!(bigquery().parse_sql_statements(error_sql).is_err());
 }
 
 #[test]
@@ -2204,6 +2201,14 @@ fn parse_extract_weekday() {
     );
 }
 
+#[test]
+fn bigquery_select_expr_star() {
+    bigquery()
+        .verified_only_select("SELECT STRUCT<STRING>((SELECT foo FROM T WHERE 
true)).* FROM T");
+    bigquery().verified_only_select("SELECT [STRUCT<STRING>('foo')][0].* 
EXCEPT (foo) FROM T");
+    bigquery().verified_only_select("SELECT myfunc()[0].* FROM T");
+}
+
 #[test]
 fn test_select_as_struct() {
     bigquery().verified_only_select("SELECT * FROM (SELECT AS VALUE STRUCT(123 
AS a, false AS b))");
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index 2489ce2d..5dd74e1f 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -1002,7 +1002,7 @@ fn parse_select_wildcard() {
     let select = verified_only_select(sql);
     assert_eq!(
         &SelectItem::QualifiedWildcard(
-            ObjectName::from(vec![Ident::new("foo")]),
+            
SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("foo")])),
             WildcardAdditionalOptions::default()
         ),
         only(&select.projection)
@@ -1012,7 +1012,10 @@ fn parse_select_wildcard() {
     let select = verified_only_select(sql);
     assert_eq!(
         &SelectItem::QualifiedWildcard(
-            ObjectName::from(vec![Ident::new("myschema"), 
Ident::new("mytable"),]),
+            SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![
+                Ident::new("myschema"),
+                Ident::new("mytable"),
+            ])),
             WildcardAdditionalOptions::default(),
         ),
         only(&select.projection)
@@ -1057,6 +1060,84 @@ fn parse_column_aliases() {
     one_statement_parses_to("SELECT a.col + 1 newname FROM foo AS a", sql);
 }
 
+#[test]
+fn parse_select_expr_star() {
+    let dialects = all_dialects_where(|d| d.supports_select_expr_star());
+
+    // Identifier wildcard expansion.
+    let select = dialects.verified_only_select("SELECT foo.bar.* FROM T");
+    let 
SelectItem::QualifiedWildcard(SelectItemQualifiedWildcardKind::ObjectName(object_name),
 _) =
+        only(&select.projection)
+    else {
+        unreachable!(
+            "expected wildcard select item: got {:?}",
+            &select.projection[0]
+        )
+    };
+    assert_eq!(
+        &ObjectName::from(
+            ["foo", "bar"]
+                .into_iter()
+                .map(Ident::new)
+                .collect::<Vec<_>>()
+        ),
+        object_name
+    );
+
+    // Arbitrary compound expression with wildcard expansion.
+    let select = dialects.verified_only_select("SELECT foo - bar.* FROM T");
+    let SelectItem::QualifiedWildcard(
+        SelectItemQualifiedWildcardKind::Expr(Expr::BinaryOp { left, op, right 
}),
+        _,
+    ) = only(&select.projection)
+    else {
+        unreachable!(
+            "expected wildcard select item: got {:?}",
+            &select.projection[0]
+        )
+    };
+    let (Expr::Identifier(left), BinaryOperator::Minus, 
Expr::Identifier(right)) =
+        (left.as_ref(), op, right.as_ref())
+    else {
+        unreachable!("expected binary op expr: got {:?}", 
&select.projection[0])
+    };
+    assert_eq!(&Ident::new("foo"), left);
+    assert_eq!(&Ident::new("bar"), right);
+
+    // Arbitrary expression wildcard expansion.
+    let select = dialects.verified_only_select("SELECT myfunc().foo.* FROM T");
+    let SelectItem::QualifiedWildcard(
+        SelectItemQualifiedWildcardKind::Expr(Expr::CompoundFieldAccess { 
root, access_chain }),
+        _,
+    ) = only(&select.projection)
+    else {
+        unreachable!("expected wildcard expr: got {:?}", &select.projection[0])
+    };
+    assert!(matches!(root.as_ref(), Expr::Function(_)));
+    assert_eq!(1, access_chain.len());
+    assert!(matches!(
+        &access_chain[0],
+        AccessExpr::Dot(Expr::Identifier(_))
+    ));
+
+    dialects.one_statement_parses_to(
+        "SELECT 2. * 3 FROM T",
+        #[cfg(feature = "bigdecimal")]
+        "SELECT 2 * 3 FROM T",
+        #[cfg(not(feature = "bigdecimal"))]
+        "SELECT 2. * 3 FROM T",
+    );
+    dialects.verified_only_select("SELECT myfunc().* FROM T");
+    dialects.verified_only_select("SELECT myfunc().* EXCEPT (foo) FROM T");
+
+    // Invalid
+    let res = dialects.parse_sql_statements("SELECT foo.*.* FROM T");
+    assert_eq!(
+        ParserError::ParserError("Expected: end of statement, found: 
.".to_string()),
+        res.unwrap_err()
+    );
+}
+
 #[test]
 fn test_eof_after_as() {
     let res = parse_sql_statements("SELECT foo AS");
diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs
index aee6d654..4289ebd1 100644
--- a/tests/sqlparser_duckdb.rs
+++ b/tests/sqlparser_duckdb.rs
@@ -160,7 +160,7 @@ fn test_select_wildcard_with_exclude() {
     let select =
         duckdb().verified_only_select("SELECT name.* EXCLUDE department_id 
FROM employee_table");
     let expected = SelectItem::QualifiedWildcard(
-        ObjectName::from(vec![Ident::new("name")]),
+        
SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("name")])),
         WildcardAdditionalOptions {
             opt_exclude: 
Some(ExcludeSelectItem::Single(Ident::new("department_id"))),
             ..Default::default()
diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs
index 2b235093..f8b58dd1 100644
--- a/tests/sqlparser_snowflake.rs
+++ b/tests/sqlparser_snowflake.rs
@@ -1368,7 +1368,7 @@ fn test_select_wildcard_with_exclude() {
     let select = snowflake_and_generic()
         .verified_only_select("SELECT name.* EXCLUDE department_id FROM 
employee_table");
     let expected = SelectItem::QualifiedWildcard(
-        ObjectName::from(vec![Ident::new("name")]),
+        
SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("name")])),
         WildcardAdditionalOptions {
             opt_exclude: 
Some(ExcludeSelectItem::Single(Ident::new("department_id"))),
             ..Default::default()
@@ -1405,7 +1405,7 @@ fn test_select_wildcard_with_rename() {
         "SELECT name.* RENAME (department_id AS new_dep, employee_id AS 
new_emp) FROM employee_table",
     );
     let expected = SelectItem::QualifiedWildcard(
-        ObjectName::from(vec![Ident::new("name")]),
+        
SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![Ident::new("name")])),
         WildcardAdditionalOptions {
             opt_rename: Some(RenameSelectItem::Multiple(vec![
                 IdentWithAlias {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to