Skip to content

Support DISTINCT AS { STRUCT | VALUE } for BigQuery #1880

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/ast/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3338,22 +3338,28 @@ impl fmt::Display for OpenJsonTableColumn {
}

/// BigQuery supports ValueTables which have 2 modes:
/// `SELECT AS STRUCT`
/// `SELECT AS VALUE`
/// `SELECT [ALL | DISTINCT] AS STRUCT`
/// `SELECT [ALL | DISTINCT] AS VALUE`
///
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#value_tables>
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#select_list>
#[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,
DistinctAsStruct,
DistinctAsValue,
}

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"),
ValueTableMode::DistinctAsStruct => write!(f, "DISTINCT AS STRUCT"),
ValueTableMode::DistinctAsValue => write!(f, "DISTINCT AS VALUE"),
}
}
}
Expand Down
39 changes: 27 additions & 12 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11505,18 +11505,7 @@ impl<'a> Parser<'a> {
}

let select_token = self.expect_keyword(Keyword::SELECT)?;
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 value_table_mode = self.parse_value_table_mode()?;

let mut top_before_distinct = false;
let mut top = None;
Expand Down Expand Up @@ -11692,6 +11681,32 @@ impl<'a> Parser<'a> {
})
}

fn parse_value_table_mode(&mut self) -> Result<Option<ValueTableMode>, ParserError> {
if !dialect_of!(self is BigQueryDialect) {
return Ok(None);
}

let mode = if self.parse_keywords(&[Keyword::DISTINCT, Keyword::AS, Keyword::VALUE]) {
Some(ValueTableMode::DistinctAsValue)
} else if self.parse_keywords(&[Keyword::DISTINCT, Keyword::AS, Keyword::STRUCT]) {
Some(ValueTableMode::DistinctAsStruct)
} else if self.parse_keywords(&[Keyword::AS, Keyword::VALUE])
|| self.parse_keywords(&[Keyword::ALL, Keyword::AS, Keyword::VALUE])
{
Some(ValueTableMode::AsValue)
} else if self.parse_keywords(&[Keyword::AS, Keyword::STRUCT])
|| self.parse_keywords(&[Keyword::ALL, Keyword::AS, Keyword::STRUCT])
{
Some(ValueTableMode::AsStruct)
} else if self.parse_keyword(Keyword::AS) {
self.expected("VALUE or STRUCT", self.peek_token())?
} else {
None
};

Ok(mode)
}

/// Invoke `f` after first setting the parser's `ParserState` to `state`.
///
/// Upon return, restores the parser's state to what it started at.
Expand Down
38 changes: 34 additions & 4 deletions tests/sqlparser_bigquery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2313,16 +2313,46 @@ fn bigquery_select_expr_star() {

#[test]
fn test_select_as_struct() {
bigquery().verified_only_select("SELECT * FROM (SELECT AS VALUE STRUCT(123 AS a, false AS b))");
for (sql, parse_to) in [
(
"SELECT * FROM (SELECT AS STRUCT STRUCT(123 AS a, false AS b))",
"SELECT * FROM (SELECT AS STRUCT STRUCT(123 AS a, false AS b))",
),
(
"SELECT * FROM (SELECT DISTINCT AS STRUCT STRUCT(123 AS a, false AS b))",
"SELECT * FROM (SELECT DISTINCT AS STRUCT STRUCT(123 AS a, false AS b))",
),
(
"SELECT * FROM (SELECT ALL AS STRUCT STRUCT(123 AS a, false AS b))",
"SELECT * FROM (SELECT AS STRUCT STRUCT(123 AS a, false AS b))",
),
] {
bigquery().one_statement_parses_to(sql, parse_to);
}

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))",
);
for (sql, parse_to) in [
(
"SELECT * FROM (SELECT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))",
"SELECT * FROM (SELECT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))",
),
(
"SELECT * FROM (SELECT DISTINCT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))",
"SELECT * FROM (SELECT DISTINCT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))",
),
(
"SELECT * FROM (SELECT ALL AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))",
"SELECT * FROM (SELECT AS VALUE STRUCT(5 AS star_rating, false AS up_down_rating))",
),
] {
bigquery().one_statement_parses_to(sql, parse_to);
}

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);
}
Expand Down