Repository: apache/datafusion-sqlparser-rs (Rust)

Issue:
BigQuery's dialect `SELECT AS STRUCT` and `SELECT AS VALUE` ends up with Parse Error
Currently syntax for BigQuery's `SELECT AS STRUCT` ([documentation](https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#select_as_struct)) and `SELECT AS VALUE` ([documentation](https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#select_as_value)) raises a parsing error.

They should both produce a [Value table](https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#value_tables) 

Minimal examples for tests:

```
#[test]
fn parse_select_as_struct_syntax() {
    // syntax: SELECT AS STRUCT 1 a, 2 b
    let sql = r#"SELECT AS STRUCT 1 a, 2 b"#;
    ...
}
```

```
#[test]
fn parse_select_as_struct_syntax() {
    // syntax: SELECT AS VALUE STRUCT(123 AS a, FALSE AS b)
    let sql = r#"SELECT AS VALUE STRUCT(123 AS a, FALSE AS b)"#;
    ...
}
```



Discussion:

Patch:
diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index a22dcc04d..a7bddfb2b 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -44,8 +44,8 @@ pub use self::query::{
     JsonTableColumnErrorHandling, LateralView, LockClause, LockType, NamedWindowDefinition,
     NonBlock, Offset, OffsetRows, OrderByExpr, Query, RenameSelectItem, ReplaceSelectElement,
     ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Table,
-    TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, Values,
-    WildcardAdditionalOptions, With,
+    TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode,
+    Values, WildcardAdditionalOptions, With,
 };
 pub use self::value::{
     escape_quoted_string, DateTimeField, DollarQuotedString, TrimWhereField, Value,
diff --git a/src/ast/query.rs b/src/ast/query.rs
index 64bda663a..f3c2d532c 100644
--- a/src/ast/query.rs
+++ b/src/ast/query.rs
@@ -245,11 +245,18 @@ pub struct Select {
     pub named_window: Vec<NamedWindowDefinition>,
     /// QUALIFY (Snowflake)
     pub qualify: Option<Expr>,
+    /// BigQuery syntax: `SELECT AS VALUE | SELECT AS STRUCT`
+    pub value_table_mode: Option<ValueTableMode>,
 }
 
 impl fmt::Display for Select {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(f, "SELECT")?;
+
+        if let Some(value_table_mode) = self.value_table_mode {
+            write!(f, " {value_table_mode}")?;
+        }
+
         if let Some(ref distinct) = self.distinct {
             write!(f, " {distinct}")?;
         }
@@ -1574,3 +1581,24 @@ impl fmt::Display for JsonTableColumnErrorHandling {
         }
     }
 }
+
+/// BigQuery supports ValueTables which have 2 modes:
+/// `SELECT AS STRUCT`
+/// `SELECT AS VALUE`
+/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#value_tables>
+#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub enum ValueTableMode {
+    AsStruct,
+    AsValue,
+}
+
+impl fmt::Display for ValueTableMode {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            ValueTableMode::AsStruct => write!(f, "AS STRUCT"),
+            ValueTableMode::AsValue => write!(f, "AS VALUE"),
+        }
+    }
+}
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 210d157d1..c6de8b18b 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -6800,6 +6800,19 @@ impl<'a> Parser<'a> {
     /// Parse a restricted `SELECT` statement (no CTEs / `UNION` / `ORDER BY`),
     /// assuming the initial `SELECT` was already consumed
     pub fn parse_select(&mut self) -> Result<Select, ParserError> {
+        let value_table_mode =
+            if dialect_of!(self is BigQueryDialect) && self.parse_keyword(Keyword::AS) {
+                if self.parse_keyword(Keyword::VALUE) {
+                    Some(ValueTableMode::AsValue)
+                } else if self.parse_keyword(Keyword::STRUCT) {
+                    Some(ValueTableMode::AsStruct)
+                } else {
+                    self.expected("VALUE or STRUCT", self.peek_token())?
+                }
+            } else {
+                None
+            };
+
         let distinct = self.parse_all_or_distinct()?;
 
         let top = if self.parse_keyword(Keyword::TOP) {
@@ -6936,6 +6949,7 @@ impl<'a> Parser<'a> {
             having,
             named_window: named_windows,
             qualify,
+            value_table_mode,
         })
     }
 


Test patch:
diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs
index 5e79db33a..827291a17 100644
--- a/tests/sqlparser_bigquery.rs
+++ b/tests/sqlparser_bigquery.rs
@@ -1352,3 +1352,19 @@ fn test_bigquery_trim() {
         bigquery().parse_sql_statements(error_sql).unwrap_err()
     );
 }
+
+#[test]
+fn test_select_as_struct() {
+    bigquery().verified_only_select("SELECT * FROM (SELECT AS VALUE STRUCT(123 AS a, false AS b))");
+    let select = bigquery().verified_only_select("SELECT AS STRUCT 1 AS a, 2 AS b");
+    assert_eq!(Some(ValueTableMode::AsStruct), select.value_table_mode);
+}
+
+#[test]
+fn test_select_as_value() {
+    bigquery().verified_only_select(
+        "SELECT * FROM (SELECT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))",
+    );
+    let select = bigquery().verified_only_select("SELECT AS VALUE STRUCT(1 AS a, 2 AS b) AS xyz");
+    assert_eq!(Some(ValueTableMode::AsValue), select.value_table_mode);
+}
diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs
index e7c85c2a3..efb53377f 100644
--- a/tests/sqlparser_clickhouse.rs
+++ b/tests/sqlparser_clickhouse.rs
@@ -110,6 +110,7 @@ fn parse_map_access_expr() {
             having: None,
             named_window: vec![],
             qualify: None,
+            value_table_mode: None,
         },
         select
     );
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index bd95e3164..13bc333c4 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -400,7 +400,8 @@ fn parse_update_set_from() {
                             sort_by: vec![],
                             having: None,
                             named_window: vec![],
-                            qualify: None
+                            qualify: None,
+                            value_table_mode: None,
                         }))),
                         order_by: vec![],
                         limit: None,
@@ -4212,6 +4213,7 @@ fn test_parse_named_window() {
             ),
         ],
         qualify: None,
+        value_table_mode: None,
     };
     assert_eq!(actual_select_only, expected);
 }
@@ -4567,6 +4569,7 @@ fn parse_interval_and_or_xor() {
             having: None,
             named_window: vec![],
             qualify: None,
+            value_table_mode: None,
         }))),
         order_by: vec![],
         limit: None,
@@ -6550,6 +6553,7 @@ fn lateral_function() {
         having: None,
         named_window: vec![],
         qualify: None,
+        value_table_mode: None,
     };
     assert_eq!(actual_select_only, expected);
 }
@@ -7193,6 +7197,7 @@ fn parse_merge() {
                             having: None,
                             named_window: vec![],
                             qualify: None,
+                            value_table_mode: None,
                         }))),
                         order_by: vec![],
                         limit: None,
diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs
index 97bdd8e1c..45ae01bf0 100644
--- a/tests/sqlparser_duckdb.rs
+++ b/tests/sqlparser_duckdb.rs
@@ -177,6 +177,7 @@ fn test_select_union_by_name() {
                 having: None,
                 named_window: vec![],
                 qualify: None,
+                value_table_mode: None,
             }))),
             right: Box::<SetExpr>::new(SetExpr::Select(Box::new(Select {
                 distinct: None,
@@ -211,6 +212,7 @@ fn test_select_union_by_name() {
                 having: None,
                 named_window: vec![],
                 qualify: None,
+                value_table_mode: None,
             }))),
         });
         assert_eq!(ast.body, expected);
diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs
index fc0d6394c..ff3e75569 100644
--- a/tests/sqlparser_mssql.rs
+++ b/tests/sqlparser_mssql.rs
@@ -112,7 +112,8 @@ fn parse_create_procedure() {
                     sort_by: vec![],
                     having: None,
                     named_window: vec![],
-                    qualify: None
+                    qualify: None,
+                    value_table_mode: None,
                 })))
             }))],
             params: Some(vec![
@@ -595,7 +596,8 @@ fn parse_substring_in_select() {
                         sort_by: vec![],
                         having: None,
                         named_window: vec![],
-                        qualify: None
+                        qualify: None,
+                        value_table_mode: None,
                     }))),
                     order_by: vec![],
                     limit: None,
diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs
index 103530928..90579788f 100644
--- a/tests/sqlparser_mysql.rs
+++ b/tests/sqlparser_mysql.rs
@@ -785,7 +785,8 @@ fn parse_escaped_quote_identifiers_with_escape() {
                 sort_by: vec![],
                 having: None,
                 named_window: vec![],
-                qualify: None
+                qualify: None,
+                value_table_mode: None,
             }))),
             order_by: vec![],
             limit: None,
@@ -829,7 +830,8 @@ fn parse_escaped_quote_identifiers_with_no_escape() {
                 sort_by: vec![],
                 having: None,
                 named_window: vec![],
-                qualify: None
+                qualify: None,
+                value_table_mode: None,
             }))),
             order_by: vec![],
             limit: None,
@@ -870,7 +872,8 @@ fn parse_escaped_backticks_with_escape() {
                 sort_by: vec![],
                 having: None,
                 named_window: vec![],
-                qualify: None
+                qualify: None,
+                value_table_mode: None,
             }))),
             order_by: vec![],
             limit: None,
@@ -911,7 +914,8 @@ fn parse_escaped_backticks_with_no_escape() {
                 sort_by: vec![],
                 having: None,
                 named_window: vec![],
-                qualify: None
+                qualify: None,
+                value_table_mode: None,
             }))),
             order_by: vec![],
             limit: None,
@@ -1581,6 +1585,7 @@ fn parse_select_with_numeric_prefix_column_name() {
                     having: None,
                     named_window: vec![],
                     qualify: None,
+                    value_table_mode: None,
                 })))
             );
         }
@@ -1631,6 +1636,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() {
                     having: None,
                     named_window: vec![],
                     qualify: None,
+                    value_table_mode: None,
                 })))
             );
         }
@@ -1841,7 +1847,8 @@ fn parse_substring_in_select() {
                         sort_by: vec![],
                         having: None,
                         named_window: vec![],
-                        qualify: None
+                        qualify: None,
+                        value_table_mode: None,
                     }))),
                     order_by: vec![],
                     limit: None,
@@ -2143,6 +2150,7 @@ fn parse_hex_string_introducer() {
                 having: None,
                 named_window: vec![],
                 qualify: None,
+                value_table_mode: None,
                 into: None
             }))),
             order_by: vec![],
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index 45a3dbed2..a663b54f3 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -1082,6 +1082,7 @@ fn parse_copy_to() {
                     distribute_by: vec![],
                     sort_by: vec![],
                     qualify: None,
+                    value_table_mode: None,
                 }))),
                 order_by: vec![],
                 limit: None,
@@ -2139,6 +2140,7 @@ fn parse_array_subquery_expr() {
                     having: None,
                     named_window: vec![],
                     qualify: None,
+                    value_table_mode: None,
                 }))),
                 right: Box::new(SetExpr::Select(Box::new(Select {
                     distinct: None,
@@ -2155,6 +2157,7 @@ fn parse_array_subquery_expr() {
                     having: None,
                     named_window: vec![],
                     qualify: None,
+                    value_table_mode: None,
                 }))),
             }),
             order_by: vec![],