Skip to content

Add support for MSSQL's OPENJSON WITH clause #1498

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
Nov 13, 2024
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
12 changes: 6 additions & 6 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ pub use self::query::{
InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonTableColumn,
JsonTableColumnErrorHandling, JsonTableNamedColumn, JsonTableNestedColumn, LateralView,
LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure,
NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OrderBy, OrderByExpr,
PivotValueSource, ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier,
ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr,
SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableFactor,
TableFunctionArgs, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode, Values,
WildcardAdditionalOptions, With, WithFill,
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, TableFactor, TableFunctionArgs, TableVersion, TableWithJoins, Top, TopQuantity,
ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill,
};

pub use self::trigger::{
Expand Down
74 changes: 74 additions & 0 deletions src/ast/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,27 @@ pub enum TableFactor {
/// The alias for the table.
alias: Option<TableAlias>,
},
/// The MSSQL's `OPENJSON` table-valued function.
///
/// ```sql
/// OPENJSON( jsonExpression [ , path ] ) [ <with_clause> ]
///
/// <with_clause> ::= WITH ( { colName type [ column_path ] [ AS JSON ] } [ ,...n ] )
/// ````
///
/// Reference: <https://learn.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql?view=sql-server-ver16#syntax>
OpenJsonTable {
/// The JSON expression to be evaluated. It must evaluate to a json string
json_expr: Expr,
/// The path to the array or object to be iterated over.
/// It must evaluate to a json array or object.
json_path: Option<Value>,
/// The columns to be extracted from each element of the array or object.
/// Each column must have a name and a type.
columns: Vec<OpenJsonTableColumn>,
/// The alias for the table.
alias: Option<TableAlias>,
},
/// Represents a parenthesized table factor. The SQL spec only allows a
/// join expression (`(foo <JOIN> bar [ <JOIN> baz ... ])`) to be nested,
/// possibly several times.
Expand Down Expand Up @@ -1461,6 +1482,25 @@ impl fmt::Display for TableFactor {
}
Ok(())
}
TableFactor::OpenJsonTable {
json_expr,
json_path,
columns,
alias,
} => {
write!(f, "OPENJSON({json_expr}")?;
if let Some(json_path) = json_path {
write!(f, ", {json_path}")?;
}
write!(f, ")")?;
if !columns.is_empty() {
write!(f, " WITH ({})", display_comma_separated(columns))?;
}
if let Some(alias) = alias {
write!(f, " AS {alias}")?;
}
Ok(())
}
TableFactor::NestedJoin {
table_with_joins,
alias,
Expand Down Expand Up @@ -2421,6 +2461,40 @@ impl fmt::Display for JsonTableColumnErrorHandling {
}
}

/// A single column definition in MSSQL's `OPENJSON WITH` clause.
///
/// ```sql
/// colName type [ column_path ] [ AS JSON ]
/// ```
///
/// Reference: <https://learn.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql?view=sql-server-ver16#syntax>
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct OpenJsonTableColumn {
/// The name of the column to be extracted.
pub name: Ident,
/// The type of the column to be extracted.
pub r#type: DataType,
/// The path to the column to be extracted. Must be a literal string.
pub path: Option<String>,
/// The `AS JSON` option.
pub as_json: bool,
}

impl fmt::Display for OpenJsonTableColumn {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} {}", self.name, self.r#type)?;
if let Some(path) = &self.path {
write!(f, " '{}'", value::escape_single_quote_string(path))?;
}
if self.as_json {
write!(f, " AS JSON")?;
}
Ok(())
}
}

/// BigQuery supports ValueTables which have 2 modes:
/// `SELECT AS STRUCT`
/// `SELECT AS VALUE`
Expand Down
1 change: 1 addition & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ define_keywords!(
ONE,
ONLY,
OPEN,
OPENJSON,
OPERATOR,
OPTIMIZE,
OPTIMIZER_COSTS,
Expand Down
60 changes: 60 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10043,6 +10043,7 @@ impl<'a> Parser<'a> {
| TableFactor::Function { alias, .. }
| TableFactor::UNNEST { alias, .. }
| TableFactor::JsonTable { alias, .. }
| TableFactor::OpenJsonTable { alias, .. }
| TableFactor::TableFunction { alias, .. }
| TableFactor::Pivot { alias, .. }
| TableFactor::Unpivot { alias, .. }
Expand Down Expand Up @@ -10156,6 +10157,9 @@ impl<'a> Parser<'a> {
columns,
alias,
})
} else if self.parse_keyword_with_tokens(Keyword::OPENJSON, &[Token::LParen]) {
self.prev_token();
self.parse_open_json_table_factor()
} else {
let name = self.parse_object_name(true)?;

Expand Down Expand Up @@ -10221,6 +10225,34 @@ impl<'a> Parser<'a> {
}
}

/// Parses `OPENJSON( jsonExpression [ , path ] ) [ <with_clause> ]` clause,
/// assuming the `OPENJSON` keyword was already consumed.
fn parse_open_json_table_factor(&mut self) -> Result<TableFactor, ParserError> {
self.expect_token(&Token::LParen)?;
let json_expr = self.parse_expr()?;
let json_path = if self.consume_token(&Token::Comma) {
Some(self.parse_value()?)
} else {
None
};
self.expect_token(&Token::RParen)?;
let columns = if self.parse_keyword(Keyword::WITH) {
self.expect_token(&Token::LParen)?;
let columns = self.parse_comma_separated(Parser::parse_openjson_table_column_def)?;
self.expect_token(&Token::RParen)?;
columns
} else {
Vec::new()
};
let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?;
Ok(TableFactor::OpenJsonTable {
json_expr,
json_path,
columns,
alias,
})
}

fn parse_match_recognize(&mut self, table: TableFactor) -> Result<TableFactor, ParserError> {
self.expect_token(&Token::LParen)?;

Expand Down Expand Up @@ -10507,6 +10539,34 @@ impl<'a> Parser<'a> {
}))
}

/// Parses MSSQL's `OPENJSON WITH` column definition.
///
/// ```sql
/// colName type [ column_path ] [ AS JSON ]
/// ```
///
/// Reference: <https://learn.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql?view=sql-server-ver16#syntax>
pub fn parse_openjson_table_column_def(&mut self) -> Result<OpenJsonTableColumn, ParserError> {
let name = self.parse_identifier(false)?;
let r#type = self.parse_data_type()?;
let path = if let Token::SingleQuotedString(path) = self.peek_token().token {
self.next_token();
Some(path)
} else {
None
};
let as_json = self.parse_keyword(Keyword::AS);
if as_json {
self.expect_keyword(Keyword::JSON)?;
}
Ok(OpenJsonTableColumn {
name,
r#type,
path,
as_json,
})
}

fn parse_json_table_column_error_handling(
&mut self,
) -> Result<Option<JsonTableColumnErrorHandling>, ParserError> {
Expand Down
Loading
Loading