Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 6 additions & 5 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ pub use self::query::{
InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonTableColumn,
JsonTableColumnErrorHandling, 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,
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 @@ -1026,6 +1026,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 @@ -1451,6 +1472,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 @@ -2346,6 +2386,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<Value>,
/// 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, " {}", 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 @@ -535,6 +535,7 @@ define_keywords!(
ONE,
ONLY,
OPEN,
OPENJSON,
OPERATOR,
OPTIMIZE,
OPTIMIZER_COSTS,
Expand Down
55 changes: 54 additions & 1 deletion src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10000,7 +10000,7 @@ impl<'a> Parser<'a> {
table_with_joins: Box::new(table_and_joins),
alias,
})
} else if dialect_of!(self is SnowflakeDialect | GenericDialect) {
} else if dialect_of!(self is SnowflakeDialect | GenericDialect | MsSqlDialect) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to update the comments below too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah to @lovasoa's suggestion, one double check on my end @gaoqiangz is the parenthesis around the joined table supported syntax for the openjson syntax? Thinking if its not, then we would be able to skip this change entirely. If it is then indeed lets update the comments to account for mssql.
Additionally, can we add a test case covering the behavior (as in a roundtrip test like SELECT ... FROM T AS A CROSS APPLY (OPENJSON(...) WITH ...) AS B so that we catch any regression on the behavior going forward

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have checked that MSSQL doesn't support parenthesis around the openjson syntax.
I'll revert this change.

// Dialect-specific behavior: Snowflake diverges from the
// standard and from most of the other implementations by
// allowing extra parentheses not only around a join (B), but
Expand All @@ -10020,6 +10020,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 @@ -10133,6 +10134,30 @@ impl<'a> Parser<'a> {
columns,
alias,
})
} else if self.parse_keyword_with_tokens(Keyword::OPENJSON, &[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,
})
} else {
let name = self.parse_object_name(true)?;

Expand Down Expand Up @@ -10468,6 +10493,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(Value::SingleQuotedString(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
29 changes: 29 additions & 0 deletions tests/sqlparser_mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,35 @@ fn parse_mssql_apply_join() {
);
}

#[test]
fn parse_mssql_cross_apply_json() {
let _ = ms().verified_only_select(
"SELECT B.kind, B.id_list \
FROM t_test_table AS A \
CROSS APPLY OPENJSON(A.param, '$.config') WITH (kind VARCHAR(20) '$.kind', [id_list] NVARCHAR(MAX) '$.id_list' AS JSON) AS B",
);
let _ = ms().verified_only_select(
"SELECT B.kind, B.id_list \
FROM t_test_table AS A \
CROSS APPLY OPENJSON(A.param) WITH (kind VARCHAR(20) '$.kind', [id_list] NVARCHAR(MAX) '$.id_list' AS JSON) AS B",
);
let _ = ms().verified_only_select(
"SELECT B.kind, B.id_list \
FROM t_test_table AS A \
CROSS APPLY OPENJSON(A.param) WITH (kind VARCHAR(20), [id_list] NVARCHAR(MAX) AS JSON) AS B",
);
let _ = ms().verified_only_select(
"SELECT B.kind, B.id_list \
FROM t_test_table AS A \
CROSS APPLY OPENJSON(A.param, '$.config') AS B",
);
let _ = ms().verified_only_select(
"SELECT B.kind, B.id_list \
FROM t_test_table AS A \
CROSS APPLY OPENJSON(A.param) AS B",
);
}

#[test]
fn parse_mssql_top_paren() {
let sql = "SELECT TOP (5) * FROM foo";
Expand Down