Merge remote-tracking branch 'upstream/main' into eper/snowflake-with-column-options

This commit is contained in:
Elia Perantoni 2025-06-24 09:26:45 +02:00
commit 0fc29b2e16
15 changed files with 608 additions and 154 deletions

View file

@ -446,6 +446,14 @@ pub enum DataType {
///
/// [PostgreSQL]: https://www.postgresql.org/docs/9.5/functions-geometry.html
GeometricType(GeometricTypeKind),
/// PostgreSQL text search vectors, see [PostgreSQL].
///
/// [PostgreSQL]: https://www.postgresql.org/docs/17/datatype-textsearch.html
TsVector,
/// PostgreSQL text search query, see [PostgreSQL].
///
/// [PostgreSQL]: https://www.postgresql.org/docs/17/datatype-textsearch.html
TsQuery,
}
impl fmt::Display for DataType {
@ -738,6 +746,8 @@ impl fmt::Display for DataType {
write!(f, "{} TABLE ({})", name, display_comma_separated(columns))
}
DataType::GeometricType(kind) => write!(f, "{}", kind),
DataType::TsVector => write!(f, "TSVECTOR"),
DataType::TsQuery => write!(f, "TSQUERY"),
}
}
}

View file

@ -30,11 +30,11 @@ use sqlparser_derive::{Visit, VisitMut};
use crate::ast::value::escape_single_quote_string;
use crate::ast::{
display_comma_separated, display_separated, CommentDef, CreateFunctionBody,
display_comma_separated, display_separated, ArgMode, CommentDef, CreateFunctionBody,
CreateFunctionUsing, DataType, Expr, FunctionBehavior, FunctionCalledOnNull,
FunctionDeterminismSpecifier, FunctionParallel, Ident, MySQLColumnPosition, ObjectName,
OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag, Value,
ValueWithSpan,
FunctionDeterminismSpecifier, FunctionParallel, Ident, IndexColumn, MySQLColumnPosition,
ObjectName, OperateFunctionArg, OrderByExpr, ProjectionSelect, SequenceOptions, SqlOption, Tag,
Value, ValueWithSpan,
};
use crate::keywords::Keyword;
use crate::tokenizer::Token;
@ -979,7 +979,7 @@ pub enum TableConstraint {
/// [1]: IndexType
index_type: Option<IndexType>,
/// Identifiers of the columns that are unique.
columns: Vec<Ident>,
columns: Vec<IndexColumn>,
index_options: Vec<IndexOption>,
characteristics: Option<ConstraintCharacteristics>,
/// Optional Postgres nulls handling: `[ NULLS [ NOT ] DISTINCT ]`
@ -1015,7 +1015,7 @@ pub enum TableConstraint {
/// [1]: IndexType
index_type: Option<IndexType>,
/// Identifiers of the columns that form the primary key.
columns: Vec<Ident>,
columns: Vec<IndexColumn>,
index_options: Vec<IndexOption>,
characteristics: Option<ConstraintCharacteristics>,
},
@ -1060,7 +1060,7 @@ pub enum TableConstraint {
/// [1]: IndexType
index_type: Option<IndexType>,
/// Referred column identifier list.
columns: Vec<Ident>,
columns: Vec<IndexColumn>,
},
/// MySQLs [fulltext][1] definition. Since the [`SPATIAL`][2] definition is exactly the same,
/// and MySQL displays both the same way, it is part of this definition as well.
@ -1083,7 +1083,7 @@ pub enum TableConstraint {
/// Optional index name.
opt_index_name: Option<Ident>,
/// Referred column identifier list.
columns: Vec<Ident>,
columns: Vec<IndexColumn>,
},
}
@ -1367,11 +1367,16 @@ impl fmt::Display for NullsDistinctOption {
pub struct ProcedureParam {
pub name: Ident,
pub data_type: DataType,
pub mode: Option<ArgMode>,
}
impl fmt::Display for ProcedureParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} {}", self.name, self.data_type)
if let Some(mode) = &self.mode {
write!(f, "{mode} {} {}", self.name, self.data_type)
} else {
write!(f, "{} {}", self.name, self.data_type)
}
}
}

View file

@ -749,7 +749,7 @@ pub enum Expr {
/// `[ NOT ] IN (SELECT ...)`
InSubquery {
expr: Box<Expr>,
subquery: Box<SetExpr>,
subquery: Box<Query>,
negated: bool,
},
/// `[ NOT ] IN UNNEST(array_expression)`
@ -2991,6 +2991,36 @@ impl From<Set> for Statement {
}
}
/// A representation of a `WHEN` arm with all the identifiers catched and the statements to execute
/// for the arm.
///
/// Snowflake: <https://docs.snowflake.com/en/sql-reference/snowflake-scripting/exception>
/// BigQuery: <https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#beginexceptionend>
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct ExceptionWhen {
pub idents: Vec<Ident>,
pub statements: Vec<Statement>,
}
impl Display for ExceptionWhen {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"WHEN {idents} THEN",
idents = display_separated(&self.idents, " OR ")
)?;
if !self.statements.is_empty() {
write!(f, " ")?;
format_statement_list(f, &self.statements)?;
}
Ok(())
}
}
/// A top-level statement (SELECT, INSERT, CREATE, etc.)
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
@ -3679,17 +3709,20 @@ pub enum Statement {
/// END;
/// ```
statements: Vec<Statement>,
/// Statements of an exception clause.
/// Exception handling with exception clauses.
/// Example:
/// ```sql
/// BEGIN
/// SELECT 1;
/// EXCEPTION WHEN ERROR THEN
/// SELECT 2;
/// SELECT 3;
/// END;
/// EXCEPTION
/// WHEN EXCEPTION_1 THEN
/// SELECT 2;
/// WHEN EXCEPTION_2 OR EXCEPTION_3 THEN
/// SELECT 3;
/// WHEN OTHER THEN
/// SELECT 4;
/// ```
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#beginexceptionend>
exception_statements: Option<Vec<Statement>>,
/// <https://docs.snowflake.com/en/sql-reference/snowflake-scripting/exception>
exception: Option<Vec<ExceptionWhen>>,
/// TRUE if the statement has an `END` keyword.
has_end_keyword: bool,
},
@ -5534,7 +5567,7 @@ impl fmt::Display for Statement {
transaction,
modifier,
statements,
exception_statements,
exception,
has_end_keyword,
} => {
if *syntax_begin {
@ -5556,11 +5589,10 @@ impl fmt::Display for Statement {
write!(f, " ")?;
format_statement_list(f, statements)?;
}
if let Some(exception_statements) = exception_statements {
write!(f, " EXCEPTION WHEN ERROR THEN")?;
if !exception_statements.is_empty() {
write!(f, " ")?;
format_statement_list(f, exception_statements)?;
if let Some(exception_when) = exception {
write!(f, " EXCEPTION")?;
for when in exception_when {
write!(f, " {when}")?;
}
}
if *has_end_keyword {

View file

@ -28,16 +28,17 @@ use super::{
ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, CreateTableOptions, Cte,
Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, FromTable,
Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList,
FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, Insert, Interpolate,
InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView,
LimitClause, MatchRecognizePattern, Measure, NamedParenthesizedList, NamedWindowDefinition,
ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, OnInsert, OpenStatement,
OrderBy, OrderByExpr, OrderByKind, Partition, PivotValueSource, ProjectionSelect, Query,
RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement,
ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption, Statement, Subscript,
SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject,
TableOptionsClustered, TableWithJoins, UpdateTableFromKind, Use, Value, Values, ViewColumnDef,
WhileStatement, WildcardAdditionalOptions, With, WithFill,
FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, IndexColumn, Insert,
Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem,
LateralView, LimitClause, MatchRecognizePattern, Measure, NamedParenthesizedList,
NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction,
OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, Partition, PivotValueSource,
ProjectionSelect, Query, RaiseStatement, RaiseStatementValue, ReferentialAction,
RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem,
SetExpr, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef,
TableConstraint, TableFactor, TableObject, TableOptionsClustered, TableWithJoins,
UpdateTableFromKind, Use, Value, Values, ViewColumnDef, WhileStatement,
WildcardAdditionalOptions, With, WithFill,
};
/// Given an iterator of spans, return the [Span::union] of all spans.
@ -650,7 +651,7 @@ impl Spanned for TableConstraint {
name.iter()
.map(|i| i.span)
.chain(index_name.iter().map(|i| i.span))
.chain(columns.iter().map(|i| i.span))
.chain(columns.iter().map(|i| i.span()))
.chain(characteristics.iter().map(|i| i.span())),
),
TableConstraint::PrimaryKey {
@ -664,7 +665,7 @@ impl Spanned for TableConstraint {
name.iter()
.map(|i| i.span)
.chain(index_name.iter().map(|i| i.span))
.chain(columns.iter().map(|i| i.span))
.chain(columns.iter().map(|i| i.span()))
.chain(characteristics.iter().map(|i| i.span())),
),
TableConstraint::ForeignKey {
@ -700,7 +701,7 @@ impl Spanned for TableConstraint {
} => union_spans(
name.iter()
.map(|i| i.span)
.chain(columns.iter().map(|i| i.span)),
.chain(columns.iter().map(|i| i.span())),
),
TableConstraint::FulltextOrSpatial {
fulltext: _,
@ -711,7 +712,7 @@ impl Spanned for TableConstraint {
opt_index_name
.iter()
.map(|i| i.span)
.chain(columns.iter().map(|i| i.span)),
.chain(columns.iter().map(|i| i.span())),
),
}
}
@ -745,6 +746,12 @@ impl Spanned for CreateIndex {
}
}
impl Spanned for IndexColumn {
fn span(&self) -> Span {
self.column.span()
}
}
impl Spanned for CaseStatement {
fn span(&self) -> Span {
let CaseStatement {

View file

@ -46,7 +46,11 @@ pub struct BigQueryDialect;
impl Dialect for BigQueryDialect {
fn parse_statement(&self, parser: &mut Parser) -> Option<Result<Statement, ParserError>> {
self.maybe_parse_statement(parser)
if parser.parse_keyword(Keyword::BEGIN) {
return Some(parser.parse_begin_exception_end());
}
None
}
/// See <https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#identifiers>
@ -141,48 +145,3 @@ impl Dialect for BigQueryDialect {
true
}
}
impl BigQueryDialect {
fn maybe_parse_statement(&self, parser: &mut Parser) -> Option<Result<Statement, ParserError>> {
if parser.peek_keyword(Keyword::BEGIN) {
return Some(self.parse_begin(parser));
}
None
}
/// Parse a `BEGIN` statement.
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#beginexceptionend>
fn parse_begin(&self, parser: &mut Parser) -> Result<Statement, ParserError> {
parser.expect_keyword(Keyword::BEGIN)?;
let statements = parser.parse_statement_list(&[Keyword::EXCEPTION, Keyword::END])?;
let has_exception_when_clause = parser.parse_keywords(&[
Keyword::EXCEPTION,
Keyword::WHEN,
Keyword::ERROR,
Keyword::THEN,
]);
let exception_statements = if has_exception_when_clause {
if !parser.peek_keyword(Keyword::END) {
Some(parser.parse_statement_list(&[Keyword::END])?)
} else {
Some(Default::default())
}
} else {
None
};
parser.expect_keyword(Keyword::END)?;
Ok(Statement::StartTransaction {
begin: true,
statements,
exception_statements,
has_end_keyword: true,
transaction: None,
modifier: None,
modes: Default::default(),
})
}
}

View file

@ -131,6 +131,10 @@ impl Dialect for SnowflakeDialect {
}
fn parse_statement(&self, parser: &mut Parser) -> Option<Result<Statement, ParserError>> {
if parser.parse_keyword(Keyword::BEGIN) {
return Some(parser.parse_begin_exception_end());
}
if parser.parse_keywords(&[Keyword::ALTER, Keyword::SESSION]) {
// ALTER SESSION
let set = match parser.parse_one_of_keywords(&[Keyword::SET, Keyword::UNSET]) {

View file

@ -646,6 +646,7 @@ define_keywords!(
ORDER,
ORDINALITY,
ORGANIZATION,
OTHER,
OUT,
OUTER,
OUTPUT,
@ -934,6 +935,8 @@ define_keywords!(
TRY,
TRY_CAST,
TRY_CONVERT,
TSQUERY,
TSVECTOR,
TUPLE,
TYPE,
UBIGINT,

View file

@ -3035,7 +3035,6 @@ impl<'a> Parser<'a> {
where
F: FnMut(&mut Parser<'a>) -> Result<(StructField, MatchedTrailingBracket), ParserError>,
{
let start_token = self.peek_token();
self.expect_keyword_is(Keyword::STRUCT)?;
// Nothing to do if we have no type information.
@ -3048,16 +3047,10 @@ impl<'a> Parser<'a> {
let trailing_bracket = loop {
let (def, trailing_bracket) = elem_parser(self)?;
field_defs.push(def);
if !self.consume_token(&Token::Comma) {
// The struct field definition is finished if it occurs `>>` or comma.
if trailing_bracket.0 || !self.consume_token(&Token::Comma) {
break trailing_bracket;
}
// Angle brackets are balanced so we only expect the trailing `>>` after
// we've matched all field types for the current struct.
// e.g. this is invalid syntax `STRUCT<STRUCT<INT>>>, INT>(NULL)`
if trailing_bracket.0 {
return parser_err!("unmatched > in STRUCT definition", start_token.span.start);
}
};
Ok((
@ -3825,7 +3818,7 @@ impl<'a> Parser<'a> {
});
}
self.expect_token(&Token::LParen)?;
let in_op = match self.maybe_parse(|p| p.parse_query_body(p.dialect.prec_unknown()))? {
let in_op = match self.maybe_parse(|p| p.parse_query())? {
Some(subquery) => Expr::InSubquery {
expr: Box::new(expr),
subquery,
@ -6876,9 +6869,7 @@ impl<'a> Parser<'a> {
None
};
self.expect_token(&Token::LParen)?;
let columns = self.parse_comma_separated(Parser::parse_create_index_expr)?;
self.expect_token(&Token::RParen)?;
let columns = self.parse_parenthesized_index_column_list()?;
let include = if self.parse_keyword(Keyword::INCLUDE) {
self.expect_token(&Token::LParen)?;
@ -7634,9 +7625,22 @@ impl<'a> Parser<'a> {
}
pub fn parse_procedure_param(&mut self) -> Result<ProcedureParam, ParserError> {
let mode = if self.parse_keyword(Keyword::IN) {
Some(ArgMode::In)
} else if self.parse_keyword(Keyword::OUT) {
Some(ArgMode::Out)
} else if self.parse_keyword(Keyword::INOUT) {
Some(ArgMode::InOut)
} else {
None
};
let name = self.parse_identifier()?;
let data_type = self.parse_data_type()?;
Ok(ProcedureParam { name, data_type })
Ok(ProcedureParam {
name,
data_type,
mode,
})
}
pub fn parse_column_def(&mut self) -> Result<ColumnDef, ParserError> {
@ -8078,7 +8082,7 @@ impl<'a> Parser<'a> {
let index_name = self.parse_optional_ident()?;
let index_type = self.parse_optional_using_then_index_type()?;
let columns = self.parse_parenthesized_column_list(Mandatory, false)?;
let columns = self.parse_parenthesized_index_column_list()?;
let index_options = self.parse_index_options()?;
let characteristics = self.parse_constraint_characteristics()?;
Ok(Some(TableConstraint::Unique {
@ -8100,7 +8104,7 @@ impl<'a> Parser<'a> {
let index_name = self.parse_optional_ident()?;
let index_type = self.parse_optional_using_then_index_type()?;
let columns = self.parse_parenthesized_column_list(Mandatory, false)?;
let columns = self.parse_parenthesized_index_column_list()?;
let index_options = self.parse_index_options()?;
let characteristics = self.parse_constraint_characteristics()?;
Ok(Some(TableConstraint::PrimaryKey {
@ -8178,7 +8182,7 @@ impl<'a> Parser<'a> {
};
let index_type = self.parse_optional_using_then_index_type()?;
let columns = self.parse_parenthesized_column_list(Mandatory, false)?;
let columns = self.parse_parenthesized_index_column_list()?;
Ok(Some(TableConstraint::Index {
display_as_key,
@ -8207,7 +8211,7 @@ impl<'a> Parser<'a> {
let opt_index_name = self.parse_optional_ident()?;
let columns = self.parse_parenthesized_column_list(Mandatory, false)?;
let columns = self.parse_parenthesized_index_column_list()?;
Ok(Some(TableConstraint::FulltextOrSpatial {
fulltext,
@ -9917,6 +9921,12 @@ impl<'a> Parser<'a> {
Ok(DataType::Unsigned)
}
}
Keyword::TSVECTOR if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => {
Ok(DataType::TsVector)
}
Keyword::TSQUERY if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => {
Ok(DataType::TsQuery)
}
_ => {
self.prev_token();
let type_name = self.parse_object_name(false)?;
@ -10613,6 +10623,14 @@ impl<'a> Parser<'a> {
self.parse_parenthesized_column_list_inner(optional, allow_empty, |p| p.parse_identifier())
}
/// Parses a parenthesized comma-separated list of index columns, which can be arbitrary
/// expressions with ordering information (and an opclass in some dialects).
fn parse_parenthesized_index_column_list(&mut self) -> Result<Vec<IndexColumn>, ParserError> {
self.parse_parenthesized_column_list_inner(Mandatory, false, |p| {
p.parse_create_index_expr()
})
}
/// Parses a parenthesized comma-separated list of qualified, possibly quoted identifiers.
/// For example: `(db1.sc1.tbl1.col1, db1.sc1.tbl1."col 2", ...)`
pub fn parse_parenthesized_qualified_column_list(
@ -15029,7 +15047,8 @@ impl<'a> Parser<'a> {
/// Parse a FETCH clause
pub fn parse_fetch(&mut self) -> Result<Fetch, ParserError> {
self.expect_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT])?;
let _ = self.parse_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]);
let (quantity, percent) = if self
.parse_one_of_keywords(&[Keyword::ROW, Keyword::ROWS])
.is_some()
@ -15038,16 +15057,16 @@ impl<'a> Parser<'a> {
} else {
let quantity = Expr::Value(self.parse_value()?);
let percent = self.parse_keyword(Keyword::PERCENT);
self.expect_one_of_keywords(&[Keyword::ROW, Keyword::ROWS])?;
let _ = self.parse_one_of_keywords(&[Keyword::ROW, Keyword::ROWS]);
(Some(quantity), percent)
};
let with_ties = if self.parse_keyword(Keyword::ONLY) {
false
} else if self.parse_keywords(&[Keyword::WITH, Keyword::TIES]) {
true
} else {
return self.expected("one of ONLY or WITH TIES", self.peek_token());
self.parse_keywords(&[Keyword::WITH, Keyword::TIES])
};
Ok(Fetch {
with_ties,
percent,
@ -15110,7 +15129,7 @@ impl<'a> Parser<'a> {
transaction: Some(BeginTransactionKind::Transaction),
modifier: None,
statements: vec![],
exception_statements: None,
exception: None,
has_end_keyword: false,
})
}
@ -15142,11 +15161,56 @@ impl<'a> Parser<'a> {
transaction,
modifier,
statements: vec![],
exception_statements: None,
exception: None,
has_end_keyword: false,
})
}
pub fn parse_begin_exception_end(&mut self) -> Result<Statement, ParserError> {
let statements = self.parse_statement_list(&[Keyword::EXCEPTION, Keyword::END])?;
let exception = if self.parse_keyword(Keyword::EXCEPTION) {
let mut when = Vec::new();
// We can have multiple `WHEN` arms so we consume all cases until `END`
while !self.peek_keyword(Keyword::END) {
self.expect_keyword(Keyword::WHEN)?;
// Each `WHEN` case can have one or more conditions, e.g.
// WHEN EXCEPTION_1 [OR EXCEPTION_2] THEN
// So we parse identifiers until the `THEN` keyword.
let mut idents = Vec::new();
while !self.parse_keyword(Keyword::THEN) {
let ident = self.parse_identifier()?;
idents.push(ident);
self.maybe_parse(|p| p.expect_keyword(Keyword::OR))?;
}
let statements = self.parse_statement_list(&[Keyword::WHEN, Keyword::END])?;
when.push(ExceptionWhen { idents, statements });
}
Some(when)
} else {
None
};
self.expect_keyword(Keyword::END)?;
Ok(Statement::StartTransaction {
begin: true,
statements,
exception,
has_end_keyword: true,
transaction: None,
modifier: None,
modes: Default::default(),
})
}
pub fn parse_end(&mut self) -> Result<Statement, ParserError> {
let modifier = if !self.dialect.supports_end_transaction_modifier() {
None
@ -16494,6 +16558,20 @@ mod tests {
}};
}
fn mk_expected_col(name: &str) -> IndexColumn {
IndexColumn {
column: OrderByExpr {
expr: Expr::Identifier(name.into()),
options: OrderByOptions {
asc: None,
nulls_first: None,
},
with_fill: None,
},
operator_class: None,
}
}
let dialect =
TestedDialects::new(vec![Box::new(GenericDialect {}), Box::new(MySqlDialect {})]);
@ -16504,7 +16582,7 @@ mod tests {
display_as_key: false,
name: None,
index_type: None,
columns: vec![Ident::new("c1")],
columns: vec![mk_expected_col("c1")],
}
);
@ -16515,7 +16593,7 @@ mod tests {
display_as_key: true,
name: None,
index_type: None,
columns: vec![Ident::new("c1")],
columns: vec![mk_expected_col("c1")],
}
);
@ -16526,7 +16604,7 @@ mod tests {
display_as_key: false,
name: Some(Ident::with_quote('\'', "index")),
index_type: None,
columns: vec![Ident::new("c1"), Ident::new("c2")],
columns: vec![mk_expected_col("c1"), mk_expected_col("c2")],
}
);
@ -16537,7 +16615,7 @@ mod tests {
display_as_key: false,
name: None,
index_type: Some(IndexType::BTree),
columns: vec![Ident::new("c1")],
columns: vec![mk_expected_col("c1")],
}
);
@ -16548,7 +16626,7 @@ mod tests {
display_as_key: false,
name: None,
index_type: Some(IndexType::Hash),
columns: vec![Ident::new("c1")],
columns: vec![mk_expected_col("c1")],
}
);
@ -16559,7 +16637,7 @@ mod tests {
display_as_key: false,
name: Some(Ident::new("idx_name")),
index_type: Some(IndexType::BTree),
columns: vec![Ident::new("c1")],
columns: vec![mk_expected_col("c1")],
}
);
@ -16570,7 +16648,7 @@ mod tests {
display_as_key: false,
name: Some(Ident::new("idx_name")),
index_type: Some(IndexType::Hash),
columns: vec![Ident::new("c1")],
columns: vec![mk_expected_col("c1")],
}
);
}

View file

@ -448,3 +448,47 @@ pub fn call(function: &str, args: impl IntoIterator<Item = Expr>) -> Expr {
within_group: vec![],
})
}
/// Gets the first index column (mysql calls it a key part) of the first index found in a
/// [`Statement::CreateIndex`], [`Statement::CreateTable`], or [`Statement::AlterTable`].
pub fn index_column(stmt: Statement) -> Expr {
match stmt {
Statement::CreateIndex(CreateIndex { columns, .. }) => {
columns.first().unwrap().column.expr.clone()
}
Statement::CreateTable(CreateTable { constraints, .. }) => {
match constraints.first().unwrap() {
TableConstraint::Index { columns, .. } => {
columns.first().unwrap().column.expr.clone()
}
TableConstraint::Unique { columns, .. } => {
columns.first().unwrap().column.expr.clone()
}
TableConstraint::PrimaryKey { columns, .. } => {
columns.first().unwrap().column.expr.clone()
}
TableConstraint::FulltextOrSpatial { columns, .. } => {
columns.first().unwrap().column.expr.clone()
}
_ => panic!("Expected an index, unique, primary, full text, or spatial constraint (foreign key does not support general key part expressions)"),
}
}
Statement::AlterTable { operations, .. } => match operations.first().unwrap() {
AlterTableOperation::AddConstraint(TableConstraint::Index { columns, .. }) => {
columns.first().unwrap().column.expr.clone()
}
AlterTableOperation::AddConstraint(TableConstraint::Unique { columns, .. }) => {
columns.first().unwrap().column.expr.clone()
}
AlterTableOperation::AddConstraint(TableConstraint::PrimaryKey { columns, .. }) => {
columns.first().unwrap().column.expr.clone()
}
AlterTableOperation::AddConstraint(TableConstraint::FulltextOrSpatial {
columns,
..
}) => columns.first().unwrap().column.expr.clone(),
_ => panic!("Expected an index, unique, primary, full text, or spatial constraint (foreign key does not support general key part expressions)"),
},
_ => panic!("Expected CREATE INDEX, ALTER TABLE, or CREATE TABLE, got: {stmt:?}"),
}
}

View file

@ -261,10 +261,10 @@ fn parse_at_at_identifier() {
#[test]
fn parse_begin() {
let sql = r#"BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT 2; END"#;
let sql = r#"BEGIN SELECT 1; EXCEPTION WHEN ERROR THEN SELECT 2; RAISE USING MESSAGE = FORMAT('ERR: %s', 'Bad'); END"#;
let Statement::StartTransaction {
statements,
exception_statements,
exception,
has_end_keyword,
..
} = bigquery().verified_stmt(sql)
@ -272,7 +272,10 @@ fn parse_begin() {
unreachable!();
};
assert_eq!(1, statements.len());
assert_eq!(1, exception_statements.unwrap().len());
assert!(exception.is_some());
let exception = exception.unwrap();
assert_eq!(1, exception.len());
assert!(has_end_keyword);
bigquery().verified_stmt(
@ -637,35 +640,6 @@ fn parse_nested_data_types() {
}
}
#[test]
fn parse_invalid_brackets() {
let sql = "SELECT STRUCT<INT64>>(NULL)";
assert_eq!(
bigquery_and_generic()
.parse_sql_statements(sql)
.unwrap_err(),
ParserError::ParserError("unmatched > in STRUCT literal".to_string())
);
let sql = "SELECT STRUCT<STRUCT<INT64>>>(NULL)";
assert_eq!(
bigquery_and_generic()
.parse_sql_statements(sql)
.unwrap_err(),
ParserError::ParserError("Expected: (, found: >".to_string())
);
let sql = "CREATE TABLE table (x STRUCT<STRUCT<INT64>>>)";
assert_eq!(
bigquery_and_generic()
.parse_sql_statements(sql)
.unwrap_err(),
ParserError::ParserError(
"Expected: ',' or ')' after column definition, found: >".to_string()
)
);
}
#[test]
fn parse_tuple_struct_literal() {
// tuple syntax: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#tuple_syntax
@ -2474,3 +2448,78 @@ fn test_struct_field_options() {
")",
));
}
#[test]
fn test_struct_trailing_and_nested_bracket() {
bigquery().verified_stmt(concat!(
"CREATE TABLE my_table (",
"f0 STRING, ",
"f1 STRUCT<a STRING, b STRUCT<c INT64, d STRING>>, ",
"f2 STRING",
")",
));
// More complex nested structs
bigquery().verified_stmt(concat!(
"CREATE TABLE my_table (",
"f0 STRING, ",
"f1 STRUCT<a STRING, b STRUCT<c INT64, d STRUCT<e STRING>>>, ",
"f2 STRUCT<h STRING, i STRUCT<j INT64, k STRUCT<l STRUCT<m STRING>>>>, ",
"f3 STRUCT<e STRING, f STRUCT<c INT64>>",
")",
));
// Bad case with missing closing bracket
assert_eq!(
ParserError::ParserError("Expected: >, found: )".to_owned()),
bigquery()
.parse_sql_statements("CREATE TABLE my_table(f1 STRUCT<a STRING, b INT64)")
.unwrap_err()
);
// Bad case with redundant closing bracket
assert_eq!(
ParserError::ParserError(
"unmatched > after parsing data type STRUCT<a STRING, b INT64>)".to_owned()
),
bigquery()
.parse_sql_statements("CREATE TABLE my_table(f1 STRUCT<a STRING, b INT64>>)")
.unwrap_err()
);
// Base case with redundant closing bracket in nested struct
assert_eq!(
ParserError::ParserError(
"Expected: ',' or ')' after column definition, found: >".to_owned()
),
bigquery()
.parse_sql_statements("CREATE TABLE my_table(f1 STRUCT<a STRUCT<b INT>>>, c INT64)")
.unwrap_err()
);
let sql = "SELECT STRUCT<INT64>>(NULL)";
assert_eq!(
bigquery_and_generic()
.parse_sql_statements(sql)
.unwrap_err(),
ParserError::ParserError("unmatched > in STRUCT literal".to_string())
);
let sql = "SELECT STRUCT<STRUCT<INT64>>>(NULL)";
assert_eq!(
bigquery_and_generic()
.parse_sql_statements(sql)
.unwrap_err(),
ParserError::ParserError("Expected: (, found: >".to_string())
);
let sql = "CREATE TABLE table (x STRUCT<STRUCT<INT64>>>)";
assert_eq!(
bigquery_and_generic()
.parse_sql_statements(sql)
.unwrap_err(),
ParserError::ParserError(
"Expected: ',' or ')' after column definition, found: >".to_string()
)
);
}

View file

@ -2225,7 +2225,7 @@ fn parse_in_subquery() {
assert_eq!(
Expr::InSubquery {
expr: Box::new(Expr::Identifier(Ident::new("segment"))),
subquery: verified_query("SELECT segm FROM bar").body,
subquery: Box::new(verified_query("SELECT segm FROM bar")),
negated: false,
},
select.selection.unwrap()
@ -2239,7 +2239,9 @@ fn parse_in_union() {
assert_eq!(
Expr::InSubquery {
expr: Box::new(Expr::Identifier(Ident::new("segment"))),
subquery: verified_query("(SELECT segm FROM bar) UNION (SELECT segm FROM bar2)").body,
subquery: Box::new(verified_query(
"(SELECT segm FROM bar) UNION (SELECT segm FROM bar2)"
)),
negated: false,
},
select.selection.unwrap()
@ -8592,8 +8594,11 @@ fn lateral_function() {
#[test]
fn parse_start_transaction() {
let dialects = all_dialects_except(|d|
// BigQuery does not support this syntax
d.is::<BigQueryDialect>());
// BigQuery and Snowflake does not support this syntax
//
// BigQuery: <https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#begin_transaction>
// Snowflake: <https://docs.snowflake.com/en/sql-reference/sql/begin>
d.is::<BigQueryDialect>() || d.is::<SnowflakeDialect>());
match dialects
.verified_stmt("START TRANSACTION READ ONLY, READ WRITE, ISOLATION LEVEL SERIALIZABLE")
{
@ -15300,6 +15305,11 @@ fn parse_return() {
let _ = all_dialects().verified_stmt("RETURN 1");
}
#[test]
fn parse_subquery_limit() {
let _ = all_dialects().verified_stmt("SELECT t1_id, t1_name FROM t1 WHERE t1_id IN (SELECT t2_id FROM t2 WHERE t1_name = t2_name LIMIT 10)");
}
#[test]
fn test_open() {
let open_cursor = "OPEN Employee_Cursor";
@ -15346,3 +15356,65 @@ fn check_enforced() {
"CREATE TABLE t (a INT, b INT, c INT, CHECK (a > 0) NOT ENFORCED, CHECK (b > 0) ENFORCED, CHECK (c > 0))",
);
}
#[test]
fn parse_create_procedure_with_parameter_modes() {
let sql = r#"CREATE PROCEDURE test_proc (IN a INTEGER, OUT b TEXT, INOUT c TIMESTAMP, d BOOL) AS BEGIN SELECT 1; END"#;
match verified_stmt(sql) {
Statement::CreateProcedure {
or_alter,
name,
params,
..
} => {
assert_eq!(or_alter, false);
assert_eq!(name.to_string(), "test_proc");
let fake_span = Span {
start: Location { line: 0, column: 0 },
end: Location { line: 0, column: 0 },
};
assert_eq!(
params,
Some(vec![
ProcedureParam {
name: Ident {
value: "a".into(),
quote_style: None,
span: fake_span,
},
data_type: DataType::Integer(None),
mode: Some(ArgMode::In)
},
ProcedureParam {
name: Ident {
value: "b".into(),
quote_style: None,
span: fake_span,
},
data_type: DataType::Text,
mode: Some(ArgMode::Out)
},
ProcedureParam {
name: Ident {
value: "c".into(),
quote_style: None,
span: fake_span,
},
data_type: DataType::Timestamp(None, TimezoneInfo::None),
mode: Some(ArgMode::InOut)
},
ProcedureParam {
name: Ident {
value: "d".into(),
quote_style: None,
span: fake_span,
},
data_type: DataType::Bool,
mode: None
},
])
);
}
_ => unreachable!(),
}
}

View file

@ -153,7 +153,8 @@ fn parse_create_procedure() {
quote_style: None,
span: Span::empty(),
},
data_type: DataType::Int(None)
data_type: DataType::Int(None),
mode: None,
},
ProcedureParam {
name: Ident {
@ -164,7 +165,8 @@ fn parse_create_procedure() {
data_type: DataType::Varchar(Some(CharacterLength::IntegerLength {
length: 256,
unit: None
}))
})),
mode: None,
}
]),
name: ObjectName::from(vec![Ident {

View file

@ -670,6 +670,20 @@ fn table_constraint_unique_primary_ctor(
characteristics: Option<ConstraintCharacteristics>,
unique_index_type_display: Option<KeyOrIndexDisplay>,
) -> TableConstraint {
let columns = columns
.into_iter()
.map(|ident| IndexColumn {
column: OrderByExpr {
expr: Expr::Identifier(ident),
options: OrderByOptions {
asc: None,
nulls_first: None,
},
with_fill: None,
},
operator_class: None,
})
.collect();
match unique_index_type_display {
Some(index_type_display) => TableConstraint::Unique {
name,
@ -795,6 +809,67 @@ fn parse_create_table_primary_and_unique_key_with_index_options() {
}
}
#[test]
fn parse_prefix_key_part() {
let expected = vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::value(
number("10"),
)))];
for sql in [
"CREATE INDEX idx_index ON t(textcol(10))",
"ALTER TABLE tab ADD INDEX idx_index (textcol(10))",
"ALTER TABLE tab ADD PRIMARY KEY (textcol(10))",
"ALTER TABLE tab ADD UNIQUE KEY (textcol(10))",
"ALTER TABLE tab ADD UNIQUE KEY (textcol(10))",
"ALTER TABLE tab ADD FULLTEXT INDEX (textcol(10))",
"CREATE TABLE t (textcol TEXT, INDEX idx_index (textcol(10)))",
] {
match index_column(mysql_and_generic().verified_stmt(sql)) {
Expr::Function(Function {
name,
args: FunctionArguments::List(FunctionArgumentList { args, .. }),
..
}) => {
assert_eq!(name.to_string(), "textcol");
assert_eq!(args, expected);
}
expr => panic!("unexpected expression {expr} for {sql}"),
}
}
}
#[test]
fn test_functional_key_part() {
assert_eq!(
index_column(
mysql_and_generic()
.verified_stmt("CREATE INDEX idx_index ON t((col COLLATE utf8mb4_bin) DESC)")
),
Expr::Nested(Box::new(Expr::Collate {
expr: Box::new(Expr::Identifier("col".into())),
collation: ObjectName(vec![sqlparser::ast::ObjectNamePart::Identifier(
Ident::new("utf8mb4_bin")
)]),
}))
);
assert_eq!(
index_column(mysql_and_generic().verified_stmt(
r#"CREATE TABLE t (jsoncol JSON, PRIMARY KEY ((CAST(col ->> '$.id' AS UNSIGNED)) ASC))"#
)),
Expr::Nested(Box::new(Expr::Cast {
kind: CastKind::Cast,
expr: Box::new(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("col"))),
op: BinaryOperator::LongArrow,
right: Box::new(Expr::Value(
Value::SingleQuotedString("$.id".to_string()).with_empty_span()
)),
}),
data_type: DataType::Unsigned,
format: None,
})),
);
}
#[test]
fn parse_create_table_primary_and_unique_key_with_index_type() {
let sqls = ["UNIQUE", "PRIMARY KEY"].map(|key_ty| {

View file

@ -6201,3 +6201,34 @@ fn parse_alter_table_replica_identity() {
_ => unreachable!(),
}
}
#[test]
fn parse_ts_datatypes() {
match pg_and_generic().verified_stmt("CREATE TABLE foo (x TSVECTOR)") {
Statement::CreateTable(CreateTable { columns, .. }) => {
assert_eq!(
columns,
vec![ColumnDef {
name: "x".into(),
data_type: DataType::TsVector,
options: vec![],
}]
);
}
_ => unreachable!(),
}
match pg_and_generic().verified_stmt("CREATE TABLE foo (x TSQUERY)") {
Statement::CreateTable(CreateTable { columns, .. }) => {
assert_eq!(
columns,
vec![ColumnDef {
name: "x".into(),
data_type: DataType::TsQuery,
options: vec![],
}]
);
}
_ => unreachable!(),
}
}

View file

@ -4085,6 +4085,89 @@ fn parse_connect_by_root_operator() {
);
}
#[test]
fn test_begin_exception_end() {
for sql in [
"BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN SELECT 2; RAISE; END",
"BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN SELECT 2; RAISE EX_1; END",
"BEGIN SELECT 1; EXCEPTION WHEN FOO THEN SELECT 2; WHEN OTHER THEN SELECT 3; RAISE; END",
"BEGIN BEGIN SELECT 1; EXCEPTION WHEN OTHER THEN SELECT 2; RAISE; END; END",
] {
snowflake().verified_stmt(sql);
}
let sql = r#"
DECLARE
EXCEPTION_1 EXCEPTION (-20001, 'I caught the expected exception.');
EXCEPTION_2 EXCEPTION (-20002, 'Not the expected exception!');
EXCEPTION_3 EXCEPTION (-20003, 'The worst exception...');
BEGIN
BEGIN
SELECT 1;
EXCEPTION
WHEN EXCEPTION_1 THEN
SELECT 1;
WHEN EXCEPTION_2 OR EXCEPTION_3 THEN
SELECT 2;
SELECT 3;
WHEN OTHER THEN
SELECT 4;
RAISE;
END;
END
"#;
// Outer `BEGIN` of the two nested `BEGIN` statements.
let Statement::StartTransaction { mut statements, .. } = snowflake()
.parse_sql_statements(sql)
.unwrap()
.pop()
.unwrap()
else {
unreachable!();
};
// Inner `BEGIN` of the two nested `BEGIN` statements.
let Statement::StartTransaction {
statements,
exception,
has_end_keyword,
..
} = statements.pop().unwrap()
else {
unreachable!();
};
assert_eq!(1, statements.len());
assert!(has_end_keyword);
let exception = exception.unwrap();
assert_eq!(3, exception.len());
assert_eq!(1, exception[0].idents.len());
assert_eq!(1, exception[0].statements.len());
assert_eq!(2, exception[1].idents.len());
assert_eq!(2, exception[1].statements.len());
}
#[test]
fn test_snowflake_fetch_clause_syntax() {
let canonical = "SELECT c1 FROM fetch_test FETCH FIRST 2 ROWS ONLY";
snowflake().verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH 2", canonical);
snowflake()
.verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH FIRST 2", canonical);
snowflake()
.verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH NEXT 2", canonical);
snowflake()
.verified_only_select_with_canonical("SELECT c1 FROM fetch_test FETCH 2 ROW", canonical);
snowflake().verified_only_select_with_canonical(
"SELECT c1 FROM fetch_test FETCH FIRST 2 ROWS",
canonical,
);
}
#[test]
fn test_snowflake_create_view_with_tag() {
let create_view_with_tag =