Merge remote-tracking branch 'upstream/main' into eper/tag-and-policy-object-name

This commit is contained in:
Elia Perantoni 2025-07-04 12:20:14 +02:00
commit 9dcc348a4d
34 changed files with 2612 additions and 565 deletions

View file

@ -63,7 +63,7 @@ $ cargo run --example cli - [--dialectname]
};
let contents = if filename == "-" {
println!("Parsing from stdin using {:?}", dialect);
println!("Parsing from stdin using {dialect:?}");
let mut buf = Vec::new();
stdin()
.read_to_end(&mut buf)

View file

@ -45,25 +45,24 @@ fn basic_queries(c: &mut Criterion) {
let large_statement = {
let expressions = (0..1000)
.map(|n| format!("FN_{}(COL_{})", n, n))
.map(|n| format!("FN_{n}(COL_{n})"))
.collect::<Vec<_>>()
.join(", ");
let tables = (0..1000)
.map(|n| format!("TABLE_{}", n))
.map(|n| format!("TABLE_{n}"))
.collect::<Vec<_>>()
.join(" JOIN ");
let where_condition = (0..1000)
.map(|n| format!("COL_{} = {}", n, n))
.map(|n| format!("COL_{n} = {n}"))
.collect::<Vec<_>>()
.join(" OR ");
let order_condition = (0..1000)
.map(|n| format!("COL_{} DESC", n))
.map(|n| format!("COL_{n} DESC"))
.collect::<Vec<_>>()
.join(", ");
format!(
"SELECT {} FROM {} WHERE {} ORDER BY {}",
expressions, tables, where_condition, order_condition
"SELECT {expressions} FROM {tables} WHERE {where_condition} ORDER BY {order_condition}"
)
};

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 {
@ -658,7 +666,7 @@ impl fmt::Display for DataType {
}
DataType::Enum(vals, bits) => {
match bits {
Some(bits) => write!(f, "ENUM{}", bits),
Some(bits) => write!(f, "ENUM{bits}"),
None => write!(f, "ENUM"),
}?;
write!(f, "(")?;
@ -706,16 +714,16 @@ impl fmt::Display for DataType {
}
// ClickHouse
DataType::Nullable(data_type) => {
write!(f, "Nullable({})", data_type)
write!(f, "Nullable({data_type})")
}
DataType::FixedString(character_length) => {
write!(f, "FixedString({})", character_length)
write!(f, "FixedString({character_length})")
}
DataType::LowCardinality(data_type) => {
write!(f, "LowCardinality({})", data_type)
write!(f, "LowCardinality({data_type})")
}
DataType::Map(key_data_type, value_data_type) => {
write!(f, "Map({}, {})", key_data_type, value_data_type)
write!(f, "Map({key_data_type}, {value_data_type})")
}
DataType::Tuple(fields) => {
write!(f, "Tuple({})", display_comma_separated(fields))
@ -737,7 +745,9 @@ impl fmt::Display for DataType {
DataType::NamedTable { name, columns } => {
write!(f, "{} TABLE ({})", name, display_comma_separated(columns))
}
DataType::GeometricType(kind) => write!(f, "{}", kind),
DataType::GeometricType(kind) => write!(f, "{kind}"),
DataType::TsVector => write!(f, "TSVECTOR"),
DataType::TsQuery => write!(f, "TSQUERY"),
}
}
}
@ -932,7 +942,7 @@ impl fmt::Display for CharacterLength {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CharacterLength::IntegerLength { length, unit } => {
write!(f, "{}", length)?;
write!(f, "{length}")?;
if let Some(unit) = unit {
write!(f, " {unit}")?;
}
@ -987,7 +997,7 @@ impl fmt::Display for BinaryLength {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BinaryLength::IntegerLength { length } => {
write!(f, "{}", length)?;
write!(f, "{length}")?;
}
BinaryLength::Max => {
write!(f, "MAX")?;

View file

@ -173,7 +173,7 @@ impl fmt::Display for AlterRoleOperation {
in_database,
} => {
if let Some(database_name) = in_database {
write!(f, "IN DATABASE {} ", database_name)?;
write!(f, "IN DATABASE {database_name} ")?;
}
match config_value {
@ -187,7 +187,7 @@ impl fmt::Display for AlterRoleOperation {
in_database,
} => {
if let Some(database_name) = in_database {
write!(f, "IN DATABASE {} ", database_name)?;
write!(f, "IN DATABASE {database_name} ")?;
}
match config_name {
@ -218,15 +218,15 @@ impl fmt::Display for Use {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("USE ")?;
match self {
Use::Catalog(name) => write!(f, "CATALOG {}", name),
Use::Schema(name) => write!(f, "SCHEMA {}", name),
Use::Database(name) => write!(f, "DATABASE {}", name),
Use::Warehouse(name) => write!(f, "WAREHOUSE {}", name),
Use::Role(name) => write!(f, "ROLE {}", name),
Use::Catalog(name) => write!(f, "CATALOG {name}"),
Use::Schema(name) => write!(f, "SCHEMA {name}"),
Use::Database(name) => write!(f, "DATABASE {name}"),
Use::Warehouse(name) => write!(f, "WAREHOUSE {name}"),
Use::Role(name) => write!(f, "ROLE {name}"),
Use::SecondaryRoles(secondary_roles) => {
write!(f, "SECONDARY ROLES {}", secondary_roles)
write!(f, "SECONDARY ROLES {secondary_roles}")
}
Use::Object(name) => write!(f, "{}", name),
Use::Object(name) => write!(f, "{name}"),
Use::Default => write!(f, "DEFAULT"),
}
}

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;
@ -57,7 +57,7 @@ impl fmt::Display for ReplicaIdentity {
ReplicaIdentity::None => f.write_str("NONE"),
ReplicaIdentity::Full => f.write_str("FULL"),
ReplicaIdentity::Default => f.write_str("DEFAULT"),
ReplicaIdentity::Index(idx) => write!(f, "USING INDEX {}", idx),
ReplicaIdentity::Index(idx) => write!(f, "USING INDEX {idx}"),
}
}
}
@ -67,8 +67,11 @@ impl fmt::Display for ReplicaIdentity {
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum AlterTableOperation {
/// `ADD <table_constraint>`
AddConstraint(TableConstraint),
/// `ADD <table_constraint> [NOT VALID]`
AddConstraint {
constraint: TableConstraint,
not_valid: bool,
},
/// `ADD [COLUMN] [IF NOT EXISTS] <column_def>`
AddColumn {
/// `[COLUMN]`.
@ -344,6 +347,10 @@ pub enum AlterTableOperation {
equals: bool,
value: ValueWithSpan,
},
/// `VALIDATE CONSTRAINT <name>`
ValidateConstraint {
name: Ident,
},
}
/// An `ALTER Policy` (`Statement::AlterPolicy`) operation
@ -450,7 +457,7 @@ pub enum Owner {
impl fmt::Display for Owner {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Owner::Ident(ident) => write!(f, "{}", ident),
Owner::Ident(ident) => write!(f, "{ident}"),
Owner::CurrentRole => write!(f, "CURRENT_ROLE"),
Owner::CurrentUser => write!(f, "CURRENT_USER"),
Owner::SessionUser => write!(f, "SESSION_USER"),
@ -494,7 +501,16 @@ impl fmt::Display for AlterTableOperation {
display_separated(new_partitions, " "),
ine = if *if_not_exists { " IF NOT EXISTS" } else { "" }
),
AlterTableOperation::AddConstraint(c) => write!(f, "ADD {c}"),
AlterTableOperation::AddConstraint {
not_valid,
constraint,
} => {
write!(f, "ADD {constraint}")?;
if *not_valid {
write!(f, " NOT VALID")?;
}
Ok(())
}
AlterTableOperation::AddColumn {
column_keyword,
if_not_exists,
@ -525,7 +541,7 @@ impl fmt::Display for AlterTableOperation {
if *if_not_exists {
write!(f, " IF NOT EXISTS")?;
}
write!(f, " {} ({})", name, query)
write!(f, " {name} ({query})")
}
AlterTableOperation::Algorithm { equals, algorithm } => {
write!(
@ -540,7 +556,7 @@ impl fmt::Display for AlterTableOperation {
if *if_exists {
write!(f, " IF EXISTS")?;
}
write!(f, " {}", name)
write!(f, " {name}")
}
AlterTableOperation::MaterializeProjection {
if_exists,
@ -551,9 +567,9 @@ impl fmt::Display for AlterTableOperation {
if *if_exists {
write!(f, " IF EXISTS")?;
}
write!(f, " {}", name)?;
write!(f, " {name}")?;
if let Some(partition) = partition {
write!(f, " IN PARTITION {}", partition)?;
write!(f, " IN PARTITION {partition}")?;
}
Ok(())
}
@ -566,9 +582,9 @@ impl fmt::Display for AlterTableOperation {
if *if_exists {
write!(f, " IF EXISTS")?;
}
write!(f, " {}", name)?;
write!(f, " {name}")?;
if let Some(partition) = partition {
write!(f, " IN PARTITION {}", partition)?;
write!(f, " IN PARTITION {partition}")?;
}
Ok(())
}
@ -772,6 +788,9 @@ impl fmt::Display for AlterTableOperation {
AlterTableOperation::ReplicaIdentity { identity } => {
write!(f, "REPLICA IDENTITY {identity}")
}
AlterTableOperation::ValidateConstraint { name } => {
write!(f, "VALIDATE CONSTRAINT {name}")
}
}
}
}
@ -893,7 +912,10 @@ pub enum AlterColumnOperation {
data_type: DataType,
/// PostgreSQL specific
using: Option<Expr>,
/// Set to true if the statement includes the `SET DATA TYPE` keywords
had_set: bool,
},
/// `ADD GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( sequence_options ) ]`
///
/// Note: this is a PostgreSQL-specific operation.
@ -914,12 +936,19 @@ impl fmt::Display for AlterColumnOperation {
AlterColumnOperation::DropDefault => {
write!(f, "DROP DEFAULT")
}
AlterColumnOperation::SetDataType { data_type, using } => {
if let Some(expr) = using {
write!(f, "SET DATA TYPE {data_type} USING {expr}")
} else {
write!(f, "SET DATA TYPE {data_type}")
AlterColumnOperation::SetDataType {
data_type,
using,
had_set,
} => {
if *had_set {
write!(f, "SET DATA ")?;
}
write!(f, "TYPE {data_type}")?;
if let Some(expr) = using {
write!(f, " USING {expr}")?;
}
Ok(())
}
AlterColumnOperation::AddGenerated {
generated_as,
@ -979,7 +1008,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 +1044,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 +1089,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 +1112,7 @@ pub enum TableConstraint {
/// Optional index name.
opt_index_name: Option<Ident>,
/// Referred column identifier list.
columns: Vec<Ident>,
columns: Vec<IndexColumn>,
},
}
@ -1168,7 +1197,7 @@ impl fmt::Display for TableConstraint {
write!(f, " ON UPDATE {action}")?;
}
if let Some(characteristics) = characteristics {
write!(f, " {}", characteristics)?;
write!(f, " {characteristics}")?;
}
Ok(())
}
@ -1308,7 +1337,7 @@ impl fmt::Display for IndexType {
Self::SPGiST => write!(f, "SPGIST"),
Self::BRIN => write!(f, "BRIN"),
Self::Bloom => write!(f, "BLOOM"),
Self::Custom(name) => write!(f, "{}", name),
Self::Custom(name) => write!(f, "{name}"),
}
}
}
@ -1367,11 +1396,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)
}
}
}
@ -1421,17 +1455,41 @@ impl fmt::Display for ColumnDef {
pub struct ViewColumnDef {
pub name: Ident,
pub data_type: Option<DataType>,
pub options: Option<Vec<ColumnOption>>,
pub options: Option<ColumnOptions>,
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum ColumnOptions {
CommaSeparated(Vec<ColumnOption>),
SpaceSeparated(Vec<ColumnOption>),
}
impl ColumnOptions {
pub fn as_slice(&self) -> &[ColumnOption] {
match self {
ColumnOptions::CommaSeparated(options) => options.as_slice(),
ColumnOptions::SpaceSeparated(options) => options.as_slice(),
}
}
}
impl fmt::Display for ViewColumnDef {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)?;
if let Some(data_type) = self.data_type.as_ref() {
write!(f, " {}", data_type)?;
write!(f, " {data_type}")?;
}
if let Some(options) = self.options.as_ref() {
write!(f, " {}", display_comma_separated(options.as_slice()))?;
match options {
ColumnOptions::CommaSeparated(column_options) => {
write!(f, " {}", display_comma_separated(column_options.as_slice()))?;
}
ColumnOptions::SpaceSeparated(column_options) => {
write!(f, " {}", display_separated(column_options.as_slice(), " "))?
}
}
}
Ok(())
}
@ -1816,7 +1874,7 @@ impl fmt::Display for ColumnOption {
} => {
write!(f, "{}", if *is_primary { "PRIMARY KEY" } else { "UNIQUE" })?;
if let Some(characteristics) = characteristics {
write!(f, " {}", characteristics)?;
write!(f, " {characteristics}")?;
}
Ok(())
}
@ -1838,7 +1896,7 @@ impl fmt::Display for ColumnOption {
write!(f, " ON UPDATE {action}")?;
}
if let Some(characteristics) = characteristics {
write!(f, " {}", characteristics)?;
write!(f, " {characteristics}")?;
}
Ok(())
}
@ -1898,7 +1956,7 @@ impl fmt::Display for ColumnOption {
write!(f, "{parameters}")
}
OnConflict(keyword) => {
write!(f, "ON CONFLICT {:?}", keyword)?;
write!(f, "ON CONFLICT {keyword:?}")?;
Ok(())
}
Policy(parameters) => {

View file

@ -55,7 +55,7 @@ impl Display for IndexColumn {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.column)?;
if let Some(operator_class) = &self.operator_class {
write!(f, " {}", operator_class)?;
write!(f, " {operator_class}")?;
}
Ok(())
}
@ -266,7 +266,7 @@ impl Display for CreateTable {
name = self.name,
)?;
if let Some(on_cluster) = &self.on_cluster {
write!(f, " ON CLUSTER {}", on_cluster)?;
write!(f, " ON CLUSTER {on_cluster}")?;
}
if !self.columns.is_empty() || !self.constraints.is_empty() {
f.write_str(" (")?;
@ -383,15 +383,15 @@ impl Display for CreateTable {
match &self.table_options {
options @ CreateTableOptions::With(_)
| options @ CreateTableOptions::Plain(_)
| options @ CreateTableOptions::TableProperties(_) => write!(f, " {}", options)?,
| options @ CreateTableOptions::TableProperties(_) => write!(f, " {options}")?,
_ => (),
}
if let Some(primary_key) = &self.primary_key {
write!(f, " PRIMARY KEY {}", primary_key)?;
write!(f, " PRIMARY KEY {primary_key}")?;
}
if let Some(order_by) = &self.order_by {
write!(f, " ORDER BY {}", order_by)?;
write!(f, " ORDER BY {order_by}")?;
}
if let Some(inherits) = &self.inherits {
write!(f, " INHERITS ({})", display_comma_separated(inherits))?;
@ -403,7 +403,7 @@ impl Display for CreateTable {
write!(f, " CLUSTER BY {cluster_by}")?;
}
if let options @ CreateTableOptions::Options(_) = &self.table_options {
write!(f, " {}", options)?;
write!(f, " {options}")?;
}
if let Some(external_volume) = self.external_volume.as_ref() {
write!(f, " EXTERNAL_VOLUME = '{external_volume}'")?;

View file

@ -67,7 +67,7 @@ impl fmt::Display for KeyValueOptions {
} else {
f.write_str(" ")?;
}
write!(f, "{}", option)?;
write!(f, "{option}")?;
}
}
Ok(())

View file

@ -28,6 +28,7 @@ use helpers::{
stmt_data_loading::{FileStagingCommand, StageLoadSelectItemKind},
};
use core::cmp::Ordering;
use core::ops::Deref;
use core::{
fmt::{self, Display},
@ -60,13 +61,14 @@ pub use self::ddl::{
AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation,
AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterType, AlterTypeAddValue,
AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue,
ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnPolicy, ColumnPolicyProperty,
ConstraintCharacteristics, CreateConnector, CreateDomain, CreateFunction, Deduplicate,
DeferrableInitial, DropBehavior, GeneratedAs, GeneratedExpressionMode, IdentityParameters,
IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder,
IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner, Partition,
ProcedureParam, ReferentialAction, ReplicaIdentity, TableConstraint, TagsColumnOption,
UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef,
ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy,
ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, CreateFunction,
Deduplicate, DeferrableInitial, DropBehavior, GeneratedAs, GeneratedExpressionMode,
IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind,
IdentityPropertyOrder, IndexOption, IndexType, KeyOrIndexDisplay, NullsDistinctOption, Owner,
Partition, ProcedureParam, ReferentialAction, ReplicaIdentity, TableConstraint,
TagsColumnOption, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation,
ViewColumnDef,
};
pub use self::dml::{CreateIndex, CreateTable, Delete, IndexColumn, Insert};
pub use self::operator::{BinaryOperator, UnaryOperator};
@ -172,7 +174,7 @@ fn format_statement_list(f: &mut fmt::Formatter, statements: &[Statement]) -> fm
}
/// An identifier, decomposed into its value or character data and the quote style.
#[derive(Debug, Clone, PartialOrd, Ord)]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct Ident {
@ -214,6 +216,35 @@ impl core::hash::Hash for Ident {
impl Eq for Ident {}
impl PartialOrd for Ident {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Ident {
fn cmp(&self, other: &Self) -> Ordering {
let Ident {
value,
quote_style,
// exhaustiveness check; we ignore spans in ordering
span: _,
} = self;
let Ident {
value: other_value,
quote_style: other_quote_style,
// exhaustiveness check; we ignore spans in ordering
span: _,
} = other;
// First compare by value, then by quote_style
value
.cmp(other_value)
.then_with(|| quote_style.cmp(other_quote_style))
}
}
impl Ident {
/// Create a new identifier with the given value and no quotes and an empty span.
pub fn new<S>(value: S) -> Self
@ -326,7 +357,7 @@ impl ObjectNamePart {
impl fmt::Display for ObjectNamePart {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ObjectNamePart::Identifier(ident) => write!(f, "{}", ident),
ObjectNamePart::Identifier(ident) => write!(f, "{ident}"),
}
}
}
@ -748,7 +779,7 @@ pub enum Expr {
/// `[ NOT ] IN (SELECT ...)`
InSubquery {
expr: Box<Expr>,
subquery: Box<SetExpr>,
subquery: Box<Query>,
negated: bool,
},
/// `[ NOT ] IN UNNEST(array_expression)`
@ -965,7 +996,7 @@ pub enum Expr {
data_type: DataType,
/// The value of the constant.
/// Hint: you can unwrap the string value using `value.into_string()`.
value: Value,
value: ValueWithSpan,
},
/// Scalar function call e.g. `LEFT(foo, 5)`
Function(Function),
@ -1093,6 +1124,8 @@ pub enum Expr {
/// [Databricks](https://docs.databricks.com/en/sql/language-manual/sql-ref-lambda-functions.html)
/// [DuckDb](https://duckdb.org/docs/sql/functions/lambda.html)
Lambda(LambdaFunction),
/// Checks membership of a value in a JSON array
MemberOf(MemberOf),
}
impl Expr {
@ -1179,8 +1212,8 @@ pub enum AccessExpr {
impl fmt::Display for AccessExpr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AccessExpr::Dot(expr) => write!(f, ".{}", expr),
AccessExpr::Subscript(subscript) => write!(f, "[{}]", subscript),
AccessExpr::Dot(expr) => write!(f, ".{expr}"),
AccessExpr::Subscript(subscript) => write!(f, "[{subscript}]"),
}
}
}
@ -1382,12 +1415,12 @@ impl fmt::Display for Expr {
match self {
Expr::Identifier(s) => write!(f, "{s}"),
Expr::Wildcard(_) => f.write_str("*"),
Expr::QualifiedWildcard(prefix, _) => write!(f, "{}.*", prefix),
Expr::QualifiedWildcard(prefix, _) => write!(f, "{prefix}.*"),
Expr::CompoundIdentifier(s) => write!(f, "{}", display_separated(s, ".")),
Expr::CompoundFieldAccess { root, access_chain } => {
write!(f, "{}", root)?;
write!(f, "{root}")?;
for field in access_chain {
write!(f, "{}", field)?;
write!(f, "{field}")?;
}
Ok(())
}
@ -1516,7 +1549,7 @@ impl fmt::Display for Expr {
} => {
let not_ = if *negated { "NOT " } else { "" };
if form.is_none() {
write!(f, "{} IS {}NORMALIZED", expr, not_)
write!(f, "{expr} IS {not_}NORMALIZED")
} else {
write!(
f,
@ -1838,7 +1871,7 @@ impl fmt::Display for Expr {
}
}
Expr::Named { expr, name } => {
write!(f, "{} AS {}", expr, name)
write!(f, "{expr} AS {name}")
}
Expr::Dictionary(fields) => {
write!(f, "{{{}}}", display_comma_separated(fields))
@ -1881,6 +1914,7 @@ impl fmt::Display for Expr {
}
Expr::Prior(expr) => write!(f, "PRIOR {expr}"),
Expr::Lambda(lambda) => write!(f, "{lambda}"),
Expr::MemberOf(member_of) => write!(f, "{member_of}"),
}
}
}
@ -2394,7 +2428,7 @@ impl fmt::Display for ConditionalStatements {
}
Ok(())
}
ConditionalStatements::BeginEnd(bes) => write!(f, "{}", bes),
ConditionalStatements::BeginEnd(bes) => write!(f, "{bes}"),
}
}
}
@ -2914,9 +2948,7 @@ impl Display for Set {
write!(
f,
"SET {modifier}ROLE {role_name}",
modifier = context_modifier
.map(|m| format!("{}", m))
.unwrap_or_default()
modifier = context_modifier.map(|m| format!("{m}")).unwrap_or_default()
)
}
Self::SetSessionParam(kind) => write!(f, "SET {kind}"),
@ -2949,7 +2981,7 @@ impl Display for Set {
charset_name,
collation_name,
} => {
write!(f, "SET NAMES {}", charset_name)?;
write!(f, "SET NAMES {charset_name}")?;
if let Some(collation) = collation_name {
f.write_str(" COLLATE ")?;
@ -2972,7 +3004,7 @@ impl Display for Set {
write!(
f,
"SET {}{}{} = {}",
scope.map(|s| format!("{}", s)).unwrap_or_default(),
scope.map(|s| format!("{s}")).unwrap_or_default(),
if *hivevar { "HIVEVAR:" } else { "" },
variable,
display_comma_separated(values)
@ -2990,6 +3022,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)]
@ -3256,6 +3318,8 @@ pub enum Statement {
secret_type: Ident,
options: Vec<SecretOption>,
},
/// A `CREATE SERVER` statement.
CreateServer(CreateServerStatement),
/// ```sql
/// CREATE POLICY
/// ```
@ -3678,17 +3742,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,
},
@ -3881,6 +3948,7 @@ pub enum Statement {
or_alter: bool,
name: ObjectName,
params: Option<Vec<ProcedureParam>>,
language: Option<Ident>,
body: ConditionalStatements,
},
/// ```sql
@ -4181,7 +4249,7 @@ pub enum Statement {
/// ```sql
/// NOTIFY channel [ , payload ]
/// ```
/// send a notification event together with an optional “payload” string to channel
/// send a notification event together with an optional "payload" string to channel
///
/// See Postgres <https://www.postgresql.org/docs/current/sql-notify.html>
NOTIFY {
@ -4340,7 +4408,7 @@ impl fmt::Display for Statement {
write!(f, "{describe_alias} ")?;
if let Some(format) = hive_format {
write!(f, "{} ", format)?;
write!(f, "{format} ")?;
}
if *has_table_keyword {
write!(f, "TABLE ")?;
@ -4784,6 +4852,7 @@ impl fmt::Display for Statement {
name,
or_alter,
params,
language,
body,
} => {
write!(
@ -4799,6 +4868,10 @@ impl fmt::Display for Statement {
}
}
if let Some(language) = language {
write!(f, " LANGUAGE {language}")?;
}
write!(f, " AS {body}")
}
Statement::CreateMacro {
@ -5107,6 +5180,9 @@ impl fmt::Display for Statement {
write!(f, " )")?;
Ok(())
}
Statement::CreateServer(stmt) => {
write!(f, "{stmt}")
}
Statement::CreatePolicy {
name,
table_name,
@ -5171,7 +5247,7 @@ impl fmt::Display for Statement {
if *only {
write!(f, "ONLY ")?;
}
write!(f, "{name} ", name = name)?;
write!(f, "{name} ")?;
if let Some(cluster) = on_cluster {
write!(f, "ON CLUSTER {cluster} ")?;
}
@ -5249,7 +5325,7 @@ impl fmt::Display for Statement {
)?;
if !session_params.options.is_empty() {
if *set {
write!(f, " {}", session_params)?;
write!(f, " {session_params}")?;
} else {
let options = session_params
.options
@ -5283,7 +5359,7 @@ impl fmt::Display for Statement {
if *purge { " PURGE" } else { "" },
)?;
if let Some(table_name) = table.as_ref() {
write!(f, " ON {}", table_name)?;
write!(f, " ON {table_name}")?;
};
Ok(())
}
@ -5533,12 +5609,12 @@ impl fmt::Display for Statement {
transaction,
modifier,
statements,
exception_statements,
exception,
has_end_keyword,
} => {
if *syntax_begin {
if let Some(modifier) = *modifier {
write!(f, "BEGIN {}", modifier)?;
write!(f, "BEGIN {modifier}")?;
} else {
write!(f, "BEGIN")?;
}
@ -5555,11 +5631,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 {
@ -5575,7 +5650,7 @@ impl fmt::Display for Statement {
if *end_syntax {
write!(f, "END")?;
if let Some(modifier) = *modifier {
write!(f, " {}", modifier)?;
write!(f, " {modifier}")?;
}
if *chain {
write!(f, " AND CHAIN")?;
@ -5674,7 +5749,7 @@ impl fmt::Display for Statement {
write!(f, " GRANTED BY {grantor}")?;
}
if let Some(cascade) = cascade {
write!(f, " {}", cascade)?;
write!(f, " {cascade}")?;
}
Ok(())
}
@ -5853,13 +5928,13 @@ impl fmt::Display for Statement {
if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" },
)?;
if !directory_table_params.options.is_empty() {
write!(f, " DIRECTORY=({})", directory_table_params)?;
write!(f, " DIRECTORY=({directory_table_params})")?;
}
if !file_format.options.is_empty() {
write!(f, " FILE_FORMAT=({})", file_format)?;
write!(f, " FILE_FORMAT=({file_format})")?;
}
if !copy_options.options.is_empty() {
write!(f, " COPY_OPTIONS=({})", copy_options)?;
write!(f, " COPY_OPTIONS=({copy_options})")?;
}
if comment.is_some() {
write!(f, " COMMENT='{}'", comment.as_ref().unwrap())?;
@ -5882,7 +5957,7 @@ impl fmt::Display for Statement {
validation_mode,
partition,
} => {
write!(f, "COPY INTO {}", into)?;
write!(f, "COPY INTO {into}")?;
if let Some(into_columns) = into_columns {
write!(f, " ({})", display_comma_separated(into_columns))?;
}
@ -5898,12 +5973,12 @@ impl fmt::Display for Statement {
)?;
}
if let Some(from_obj_alias) = from_obj_alias {
write!(f, " AS {}", from_obj_alias)?;
write!(f, " AS {from_obj_alias}")?;
}
write!(f, ")")?;
} else if let Some(from_obj) = from_obj {
// Standard data load
write!(f, " FROM {}{}", from_obj, stage_params)?;
write!(f, " FROM {from_obj}{stage_params}")?;
if let Some(from_obj_alias) = from_obj_alias {
write!(f, " AS {from_obj_alias}")?;
}
@ -5916,24 +5991,24 @@ impl fmt::Display for Statement {
write!(f, " FILES = ('{}')", display_separated(files, "', '"))?;
}
if let Some(pattern) = pattern {
write!(f, " PATTERN = '{}'", pattern)?;
write!(f, " PATTERN = '{pattern}'")?;
}
if let Some(partition) = partition {
write!(f, " PARTITION BY {partition}")?;
}
if !file_format.options.is_empty() {
write!(f, " FILE_FORMAT=({})", file_format)?;
write!(f, " FILE_FORMAT=({file_format})")?;
}
if !copy_options.options.is_empty() {
match kind {
CopyIntoSnowflakeKind::Table => {
write!(f, " COPY_OPTIONS=({})", copy_options)?
write!(f, " COPY_OPTIONS=({copy_options})")?
}
CopyIntoSnowflakeKind::Location => write!(f, " {copy_options}")?,
}
}
if let Some(validation_mode) = validation_mode {
write!(f, " VALIDATION_MODE = {}", validation_mode)?;
write!(f, " VALIDATION_MODE = {validation_mode}")?;
}
Ok(())
}
@ -5979,10 +6054,10 @@ impl fmt::Display for Statement {
} => {
write!(f, "OPTIMIZE TABLE {name}")?;
if let Some(on_cluster) = on_cluster {
write!(f, " ON CLUSTER {on_cluster}", on_cluster = on_cluster)?;
write!(f, " ON CLUSTER {on_cluster}")?;
}
if let Some(partition) = partition {
write!(f, " {partition}", partition = partition)?;
write!(f, " {partition}")?;
}
if *include_final {
write!(f, " FINAL")?;
@ -6109,7 +6184,7 @@ impl fmt::Display for SetAssignment {
write!(
f,
"{}{} = {}",
self.scope.map(|s| format!("{}", s)).unwrap_or_default(),
self.scope.map(|s| format!("{s}")).unwrap_or_default(),
self.name,
self.value
)
@ -6838,7 +6913,7 @@ impl fmt::Display for GranteeName {
match self {
GranteeName::ObjectName(name) => name.fmt(f),
GranteeName::UserHost { user, host } => {
write!(f, "{}@{}", user, host)
write!(f, "{user}@{host}")
}
}
}
@ -6853,6 +6928,12 @@ pub enum GrantObjects {
AllSequencesInSchema { schemas: Vec<ObjectName> },
/// Grant privileges on `ALL TABLES IN SCHEMA <schema_name> [, ...]`
AllTablesInSchema { schemas: Vec<ObjectName> },
/// Grant privileges on `FUTURE SCHEMAS IN DATABASE <database_name> [, ...]`
FutureSchemasInDatabase { databases: Vec<ObjectName> },
/// Grant privileges on `FUTURE TABLES IN SCHEMA <schema_name> [, ...]`
FutureTablesInSchema { schemas: Vec<ObjectName> },
/// Grant privileges on `FUTURE VIEWS IN SCHEMA <schema_name> [, ...]`
FutureViewsInSchema { schemas: Vec<ObjectName> },
/// Grant privileges on specific databases
Databases(Vec<ObjectName>),
/// Grant privileges on specific schemas
@ -6921,6 +7002,27 @@ impl fmt::Display for GrantObjects {
display_comma_separated(schemas)
)
}
GrantObjects::FutureSchemasInDatabase { databases } => {
write!(
f,
"FUTURE SCHEMAS IN DATABASE {}",
display_comma_separated(databases)
)
}
GrantObjects::FutureTablesInSchema { schemas } => {
write!(
f,
"FUTURE TABLES IN SCHEMA {}",
display_comma_separated(schemas)
)
}
GrantObjects::FutureViewsInSchema { schemas } => {
write!(
f,
"FUTURE VIEWS IN SCHEMA {}",
display_comma_separated(schemas)
)
}
GrantObjects::ResourceMonitors(objects) => {
write!(f, "RESOURCE MONITOR {}", display_comma_separated(objects))
}
@ -7008,7 +7110,7 @@ pub enum AssignmentTarget {
impl fmt::Display for AssignmentTarget {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AssignmentTarget::ColumnName(column) => write!(f, "{}", column),
AssignmentTarget::ColumnName(column) => write!(f, "{column}"),
AssignmentTarget::Tuple(columns) => write!(f, "({})", display_comma_separated(columns)),
}
}
@ -7253,8 +7355,8 @@ impl fmt::Display for FunctionArguments {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FunctionArguments::None => Ok(()),
FunctionArguments::Subquery(query) => write!(f, "({})", query),
FunctionArguments::List(args) => write!(f, "({})", args),
FunctionArguments::Subquery(query) => write!(f, "({query})"),
FunctionArguments::List(args) => write!(f, "({args})"),
}
}
}
@ -7275,7 +7377,7 @@ pub struct FunctionArgumentList {
impl fmt::Display for FunctionArgumentList {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(duplicate_treatment) = self.duplicate_treatment {
write!(f, "{} ", duplicate_treatment)?;
write!(f, "{duplicate_treatment} ")?;
}
write!(f, "{}", display_comma_separated(&self.args))?;
if !self.clauses.is_empty() {
@ -7335,7 +7437,7 @@ impl fmt::Display for FunctionArgumentClause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FunctionArgumentClause::IgnoreOrRespectNulls(null_treatment) => {
write!(f, "{}", null_treatment)
write!(f, "{null_treatment}")
}
FunctionArgumentClause::OrderBy(order_by) => {
write!(f, "ORDER BY {}", display_comma_separated(order_by))
@ -7791,12 +7893,12 @@ pub enum SqlOption {
impl fmt::Display for SqlOption {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SqlOption::Clustered(c) => write!(f, "{}", c),
SqlOption::Clustered(c) => write!(f, "{c}"),
SqlOption::Ident(ident) => {
write!(f, "{}", ident)
write!(f, "{ident}")
}
SqlOption::KeyValue { key: name, value } => {
write!(f, "{} = {}", name, value)
write!(f, "{name} = {value}")
}
SqlOption::Partition {
column_name,
@ -7836,7 +7938,7 @@ impl fmt::Display for SqlOption {
SqlOption::NamedParenthesizedList(value) => {
write!(f, "{} = ", value.key)?;
if let Some(key) = &value.name {
write!(f, "{}", key)?;
write!(f, "{key}")?;
}
if !value.values.is_empty() {
write!(f, "({})", display_comma_separated(&value.values))?
@ -7879,6 +7981,70 @@ impl fmt::Display for SecretOption {
}
}
/// A `CREATE SERVER` statement.
///
/// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createserver.html)
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct CreateServerStatement {
pub name: ObjectName,
pub if_not_exists: bool,
pub server_type: Option<Ident>,
pub version: Option<Ident>,
pub foreign_data_wrapper: ObjectName,
pub options: Option<Vec<CreateServerOption>>,
}
impl fmt::Display for CreateServerStatement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let CreateServerStatement {
name,
if_not_exists,
server_type,
version,
foreign_data_wrapper,
options,
} = self;
write!(
f,
"CREATE SERVER {if_not_exists}{name} ",
if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" },
)?;
if let Some(st) = server_type {
write!(f, "TYPE {st} ")?;
}
if let Some(v) = version {
write!(f, "VERSION {v} ")?;
}
write!(f, "FOREIGN DATA WRAPPER {foreign_data_wrapper}")?;
if let Some(o) = options {
write!(f, " OPTIONS ({o})", o = display_comma_separated(o))?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct CreateServerOption {
pub key: Ident,
pub value: Ident,
}
impl fmt::Display for CreateServerOption {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} {}", self.key, self.value)
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
@ -7893,7 +8059,7 @@ impl fmt::Display for AttachDuckDBDatabaseOption {
AttachDuckDBDatabaseOption::ReadOnly(Some(true)) => write!(f, "READ_ONLY true"),
AttachDuckDBDatabaseOption::ReadOnly(Some(false)) => write!(f, "READ_ONLY false"),
AttachDuckDBDatabaseOption::ReadOnly(None) => write!(f, "READ_ONLY"),
AttachDuckDBDatabaseOption::Type(t) => write!(f, "TYPE {}", t),
AttachDuckDBDatabaseOption::Type(t) => write!(f, "TYPE {t}"),
}
}
}
@ -9416,10 +9582,10 @@ impl fmt::Display for ShowStatementIn {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.clause)?;
if let Some(parent_type) = &self.parent_type {
write!(f, " {}", parent_type)?;
write!(f, " {parent_type}")?;
}
if let Some(parent_name) = &self.parent_name {
write!(f, " {}", parent_name)?;
write!(f, " {parent_name}")?;
}
Ok(())
}
@ -9500,7 +9666,7 @@ impl fmt::Display for TableObject {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::TableName(table_name) => write!(f, "{table_name}"),
Self::TableFunction(func) => write!(f, "FUNCTION {}", func),
Self::TableFunction(func) => write!(f, "FUNCTION {func}"),
}
}
}
@ -9688,7 +9854,7 @@ pub struct ReturnStatement {
impl fmt::Display for ReturnStatement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.value {
Some(ReturnStatementValue::Expr(expr)) => write!(f, "RETURN {}", expr),
Some(ReturnStatementValue::Expr(expr)) => write!(f, "RETURN {expr}"),
None => write!(f, "RETURN"),
}
}
@ -9737,8 +9903,31 @@ impl fmt::Display for NullInclusion {
}
}
/// Checks membership of a value in a JSON array
///
/// Syntax:
/// ```sql
/// <value> MEMBER OF(<array>)
/// ```
/// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/json-search-functions.html#operator_member-of)
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct MemberOf {
pub value: Box<Expr>,
pub array: Box<Expr>,
}
impl fmt::Display for MemberOf {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} MEMBER OF({})", self.value, self.array)
}
}
#[cfg(test)]
mod tests {
use crate::tokenizer::Location;
use super::*;
#[test]
@ -10034,4 +10223,16 @@ mod tests {
test_steps(OneOrManyWithParens::Many(vec![2]), vec![2], 3);
test_steps(OneOrManyWithParens::Many(vec![3, 4]), vec![3, 4], 4);
}
// Tests that the position in the code of an `Ident` does not affect its
// ordering.
#[test]
fn test_ident_ord() {
let mut a = Ident::with_span(Span::new(Location::new(1, 1), Location::new(1, 1)), "a");
let mut b = Ident::with_span(Span::new(Location::new(2, 2), Location::new(2, 2)), "b");
assert!(a < b);
std::mem::swap(&mut a.span, &mut b.span);
assert!(a < b);
}
}

View file

@ -1047,7 +1047,7 @@ impl fmt::Display for ConnectBy {
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct Setting {
pub key: Ident,
pub value: Value,
pub value: Expr,
}
impl fmt::Display for Setting {
@ -1183,7 +1183,7 @@ impl fmt::Display for TableIndexHints {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} {} ", self.hint_type, self.index_type)?;
if let Some(for_clause) = &self.for_clause {
write!(f, "FOR {} ", for_clause)?;
write!(f, "FOR {for_clause} ")?;
}
write!(f, "({})", display_comma_separated(&self.index_names))
}
@ -1459,7 +1459,7 @@ impl fmt::Display for TableSampleQuantity {
}
write!(f, "{}", self.value)?;
if let Some(unit) = &self.unit {
write!(f, " {}", unit)?;
write!(f, " {unit}")?;
}
if self.parenthesized {
write!(f, ")")?;
@ -1552,7 +1552,7 @@ impl fmt::Display for TableSampleBucket {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "BUCKET {} OUT OF {}", self.bucket, self.total)?;
if let Some(on) = &self.on {
write!(f, " ON {}", on)?;
write!(f, " ON {on}")?;
}
Ok(())
}
@ -1561,19 +1561,19 @@ impl fmt::Display for TableSample {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.modifier)?;
if let Some(name) = &self.name {
write!(f, " {}", name)?;
write!(f, " {name}")?;
}
if let Some(quantity) = &self.quantity {
write!(f, " {}", quantity)?;
write!(f, " {quantity}")?;
}
if let Some(seed) = &self.seed {
write!(f, " {}", seed)?;
write!(f, " {seed}")?;
}
if let Some(bucket) = &self.bucket {
write!(f, " ({})", bucket)?;
write!(f, " ({bucket})")?;
}
if let Some(offset) = &self.offset {
write!(f, " OFFSET {}", offset)?;
write!(f, " OFFSET {offset}")?;
}
Ok(())
}
@ -1651,7 +1651,7 @@ impl fmt::Display for RowsPerMatch {
RowsPerMatch::AllRows(mode) => {
write!(f, "ALL ROWS PER MATCH")?;
if let Some(mode) = mode {
write!(f, " {}", mode)?;
write!(f, " {mode}")?;
}
Ok(())
}
@ -1777,7 +1777,7 @@ impl fmt::Display for MatchRecognizePattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use MatchRecognizePattern::*;
match self {
Symbol(symbol) => write!(f, "{}", symbol),
Symbol(symbol) => write!(f, "{symbol}"),
Exclude(symbol) => write!(f, "{{- {symbol} -}}"),
Permute(symbols) => write!(f, "PERMUTE({})", display_comma_separated(symbols)),
Concat(patterns) => write!(f, "{}", display_separated(patterns, " ")),
@ -2148,7 +2148,7 @@ impl fmt::Display for TableAliasColumnDef {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)?;
if let Some(ref data_type) = self.data_type {
write!(f, " {}", data_type)?;
write!(f, " {data_type}")?;
}
Ok(())
}
@ -2398,7 +2398,7 @@ impl fmt::Display for OrderBy {
write!(f, " {}", display_comma_separated(exprs))?;
}
OrderByKind::All(all) => {
write!(f, " ALL{}", all)?;
write!(f, " ALL{all}")?;
}
}
@ -2429,7 +2429,7 @@ impl fmt::Display for OrderByExpr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}{}", self.expr, self.options)?;
if let Some(ref with_fill) = self.with_fill {
write!(f, " {}", with_fill)?
write!(f, " {with_fill}")?
}
Ok(())
}
@ -2452,13 +2452,13 @@ impl fmt::Display for WithFill {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "WITH FILL")?;
if let Some(ref from) = self.from {
write!(f, " FROM {}", from)?;
write!(f, " FROM {from}")?;
}
if let Some(ref to) = self.to {
write!(f, " TO {}", to)?;
write!(f, " TO {to}")?;
}
if let Some(ref step) = self.step {
write!(f, " STEP {}", step)?;
write!(f, " STEP {step}")?;
}
Ok(())
}
@ -2487,7 +2487,7 @@ impl fmt::Display for InterpolateExpr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.column)?;
if let Some(ref expr) = self.expr {
write!(f, " AS {}", expr)?;
write!(f, " AS {expr}")?;
}
Ok(())
}
@ -2565,7 +2565,7 @@ impl fmt::Display for LimitClause {
Ok(())
}
LimitClause::OffsetCommaLimit { offset, limit } => {
write!(f, " LIMIT {}, {}", offset, limit)
write!(f, " LIMIT {offset}, {limit}")
}
}
}
@ -2684,6 +2684,79 @@ pub enum PipeOperator {
/// Syntax: `|> TABLESAMPLE SYSTEM (10 PERCENT)
/// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#tablesample_pipe_operator>
TableSample { sample: Box<TableSample> },
/// Renames columns in the input table.
///
/// Syntax: `|> RENAME old_name AS new_name, ...`
///
/// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#rename_pipe_operator>
Rename { mappings: Vec<IdentWithAlias> },
/// Combines the input table with one or more tables using UNION.
///
/// Syntax: `|> UNION [ALL|DISTINCT] (<query>), (<query>), ...`
///
/// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#union_pipe_operator>
Union {
set_quantifier: SetQuantifier,
queries: Vec<Query>,
},
/// Returns only the rows that are present in both the input table and the specified tables.
///
/// Syntax: `|> INTERSECT [DISTINCT] (<query>), (<query>), ...`
///
/// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#intersect_pipe_operator>
Intersect {
set_quantifier: SetQuantifier,
queries: Vec<Query>,
},
/// Returns only the rows that are present in the input table but not in the specified tables.
///
/// Syntax: `|> EXCEPT DISTINCT (<query>), (<query>), ...`
///
/// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#except_pipe_operator>
Except {
set_quantifier: SetQuantifier,
queries: Vec<Query>,
},
/// Calls a table function or procedure that returns a table.
///
/// Syntax: `|> CALL function_name(args) [AS alias]`
///
/// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#call_pipe_operator>
Call {
function: Function,
alias: Option<Ident>,
},
/// Pivots data from rows to columns.
///
/// Syntax: `|> PIVOT(aggregate_function(column) FOR pivot_column IN (value1, value2, ...)) [AS alias]`
///
/// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#pivot_pipe_operator>
Pivot {
aggregate_functions: Vec<ExprWithAlias>,
value_column: Vec<Ident>,
value_source: PivotValueSource,
alias: Option<Ident>,
},
/// The `UNPIVOT` pipe operator transforms columns into rows.
///
/// Syntax:
/// ```sql
/// |> UNPIVOT(value_column FOR name_column IN (column1, column2, ...)) [alias]
/// ```
///
/// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#unpivot_pipe_operator>
Unpivot {
value_column: Ident,
name_column: Ident,
unpivot_columns: Vec<Ident>,
alias: Option<Ident>,
},
/// Joins the input table with another table.
///
/// Syntax: `|> [JOIN_TYPE] JOIN <table> [alias] ON <condition>` or `|> [JOIN_TYPE] JOIN <table> [alias] USING (<columns>)`
///
/// See more at <https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax#join_pipe_operator>
Join(Join),
}
impl fmt::Display for PipeOperator {
@ -2702,12 +2775,12 @@ impl fmt::Display for PipeOperator {
write!(f, "DROP {}", display_comma_separated(columns.as_slice()))
}
PipeOperator::As { alias } => {
write!(f, "AS {}", alias)
write!(f, "AS {alias}")
}
PipeOperator::Limit { expr, offset } => {
write!(f, "LIMIT {}", expr)?;
write!(f, "LIMIT {expr}")?;
if let Some(offset) = offset {
write!(f, " OFFSET {}", offset)?;
write!(f, " OFFSET {offset}")?;
}
Ok(())
}
@ -2730,16 +2803,96 @@ impl fmt::Display for PipeOperator {
}
PipeOperator::Where { expr } => {
write!(f, "WHERE {}", expr)
write!(f, "WHERE {expr}")
}
PipeOperator::OrderBy { exprs } => {
write!(f, "ORDER BY {}", display_comma_separated(exprs.as_slice()))
}
PipeOperator::TableSample { sample } => {
write!(f, "{}", sample)
write!(f, "{sample}")
}
PipeOperator::Rename { mappings } => {
write!(f, "RENAME {}", display_comma_separated(mappings))
}
PipeOperator::Union {
set_quantifier,
queries,
} => Self::fmt_set_operation(f, "UNION", set_quantifier, queries),
PipeOperator::Intersect {
set_quantifier,
queries,
} => Self::fmt_set_operation(f, "INTERSECT", set_quantifier, queries),
PipeOperator::Except {
set_quantifier,
queries,
} => Self::fmt_set_operation(f, "EXCEPT", set_quantifier, queries),
PipeOperator::Call { function, alias } => {
write!(f, "CALL {function}")?;
Self::fmt_optional_alias(f, alias)
}
PipeOperator::Pivot {
aggregate_functions,
value_column,
value_source,
alias,
} => {
write!(
f,
"PIVOT({} FOR {} IN ({}))",
display_comma_separated(aggregate_functions),
Expr::CompoundIdentifier(value_column.to_vec()),
value_source
)?;
Self::fmt_optional_alias(f, alias)
}
PipeOperator::Unpivot {
value_column,
name_column,
unpivot_columns,
alias,
} => {
write!(
f,
"UNPIVOT({} FOR {} IN ({}))",
value_column,
name_column,
display_comma_separated(unpivot_columns)
)?;
Self::fmt_optional_alias(f, alias)
}
PipeOperator::Join(join) => write!(f, "{join}"),
}
}
}
impl PipeOperator {
/// Helper function to format optional alias for pipe operators
fn fmt_optional_alias(f: &mut fmt::Formatter<'_>, alias: &Option<Ident>) -> fmt::Result {
if let Some(alias) = alias {
write!(f, " AS {alias}")?;
}
Ok(())
}
/// Helper function to format set operations (UNION, INTERSECT, EXCEPT) with queries
fn fmt_set_operation(
f: &mut fmt::Formatter<'_>,
operation: &str,
set_quantifier: &SetQuantifier,
queries: &[Query],
) -> fmt::Result {
write!(f, "{operation}")?;
match set_quantifier {
SetQuantifier::None => {}
_ => {
write!(f, " {set_quantifier}")?;
}
}
write!(f, " ")?;
let parenthesized_queries: Vec<String> =
queries.iter().map(|query| format!("({query})")).collect();
write!(f, "{}", display_comma_separated(&parenthesized_queries))
}
}
@ -3016,7 +3169,7 @@ pub enum FormatClause {
impl fmt::Display for FormatClause {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
FormatClause::Identifier(ident) => write!(f, "FORMAT {}", ident),
FormatClause::Identifier(ident) => write!(f, "FORMAT {ident}"),
FormatClause::Null => write!(f, "FORMAT NULL"),
}
}
@ -3078,9 +3231,9 @@ impl fmt::Display for ForClause {
without_array_wrapper,
} => {
write!(f, "FOR JSON ")?;
write!(f, "{}", for_json)?;
write!(f, "{for_json}")?;
if let Some(root) = root {
write!(f, ", ROOT('{}')", root)?;
write!(f, ", ROOT('{root}')")?;
}
if *include_null_values {
write!(f, ", INCLUDE_NULL_VALUES")?;
@ -3098,7 +3251,7 @@ impl fmt::Display for ForClause {
r#type,
} => {
write!(f, "FOR XML ")?;
write!(f, "{}", for_xml)?;
write!(f, "{for_xml}")?;
if *binary_base64 {
write!(f, ", BINARY BASE64")?;
}
@ -3106,7 +3259,7 @@ impl fmt::Display for ForClause {
write!(f, ", TYPE")?;
}
if let Some(root) = root {
write!(f, ", ROOT('{}')", root)?;
write!(f, ", ROOT('{root}')")?;
}
if *elements {
write!(f, ", ELEMENTS")?;
@ -3133,7 +3286,7 @@ impl fmt::Display for ForXml {
ForXml::Raw(root) => {
write!(f, "RAW")?;
if let Some(root) = root {
write!(f, "('{}')", root)?;
write!(f, "('{root}')")?;
}
Ok(())
}
@ -3142,7 +3295,7 @@ impl fmt::Display for ForXml {
ForXml::Path(root) => {
write!(f, "PATH")?;
if let Some(root) = root {
write!(f, "('{}')", root)?;
write!(f, "('{root}')")?;
}
Ok(())
}
@ -3205,7 +3358,7 @@ impl fmt::Display for JsonTableColumn {
JsonTableColumn::Named(json_table_named_column) => {
write!(f, "{json_table_named_column}")
}
JsonTableColumn::ForOrdinality(ident) => write!(f, "{} FOR ORDINALITY", ident),
JsonTableColumn::ForOrdinality(ident) => write!(f, "{ident} FOR ORDINALITY"),
JsonTableColumn::Nested(json_table_nested_column) => {
write!(f, "{json_table_nested_column}")
}
@ -3271,10 +3424,10 @@ impl fmt::Display for JsonTableNamedColumn {
self.path
)?;
if let Some(on_empty) = &self.on_empty {
write!(f, " {} ON EMPTY", on_empty)?;
write!(f, " {on_empty} ON EMPTY")?;
}
if let Some(on_error) = &self.on_error {
write!(f, " {} ON ERROR", on_error)?;
write!(f, " {on_error} ON ERROR")?;
}
Ok(())
}
@ -3296,7 +3449,7 @@ impl fmt::Display for JsonTableColumnErrorHandling {
match self {
JsonTableColumnErrorHandling::Null => write!(f, "NULL"),
JsonTableColumnErrorHandling::Default(json_string) => {
write!(f, "DEFAULT {}", json_string)
write!(f, "DEFAULT {json_string}")
}
JsonTableColumnErrorHandling::Error => write!(f, "ERROR"),
}
@ -3429,12 +3582,12 @@ impl fmt::Display for XmlTableColumn {
default,
nullable,
} => {
write!(f, " {}", r#type)?;
write!(f, " {type}")?;
if let Some(p) = path {
write!(f, " PATH {}", p)?;
write!(f, " PATH {p}")?;
}
if let Some(d) = default {
write!(f, " DEFAULT {}", d)?;
write!(f, " DEFAULT {d}")?;
}
if !*nullable {
write!(f, " NOT NULL")?;
@ -3465,7 +3618,7 @@ impl fmt::Display for XmlPassingArgument {
}
write!(f, "{}", self.expr)?;
if let Some(alias) = &self.alias {
write!(f, " AS {}", alias)?;
write!(f, " AS {alias}")?;
}
Ok(())
}

View file

@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
use crate::ast::query::SelectItemQualifiedWildcardKind;
use crate::ast::{query::SelectItemQualifiedWildcardKind, ColumnOptions};
use core::iter;
use crate::tokenizer::Span;
@ -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.
@ -422,6 +423,7 @@ impl Spanned for Statement {
Statement::CreateIndex(create_index) => create_index.span(),
Statement::CreateRole { .. } => Span::empty(),
Statement::CreateSecret { .. } => Span::empty(),
Statement::CreateServer { .. } => Span::empty(),
Statement::CreateConnector { .. } => Span::empty(),
Statement::AlterTable {
name,
@ -650,7 +652,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 +666,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 +702,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 +713,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 +747,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 {
@ -917,6 +925,7 @@ impl Spanned for AlterColumnOperation {
AlterColumnOperation::SetDataType {
data_type: _,
using,
had_set: _,
} => using.as_ref().map_or(Span::empty(), |u| u.span()),
AlterColumnOperation::AddGenerated { .. } => Span::empty(),
}
@ -984,10 +993,13 @@ impl Spanned for ViewColumnDef {
options,
} = self;
union_spans(
core::iter::once(name.span)
.chain(options.iter().flat_map(|i| i.iter().map(|k| k.span()))),
)
name.span.union_opt(&options.as_ref().map(|o| o.span()))
}
}
impl Spanned for ColumnOptions {
fn span(&self) -> Span {
union_spans(self.as_slice().iter().map(|i| i.span()))
}
}
@ -1048,7 +1060,9 @@ impl Spanned for CreateTableOptions {
match self {
CreateTableOptions::None => Span::empty(),
CreateTableOptions::With(vec) => union_spans(vec.iter().map(|i| i.span())),
CreateTableOptions::Options(vec) => union_spans(vec.iter().map(|i| i.span())),
CreateTableOptions::Options(vec) => {
union_spans(vec.as_slice().iter().map(|i| i.span()))
}
CreateTableOptions::Plain(vec) => union_spans(vec.iter().map(|i| i.span())),
CreateTableOptions::TableProperties(vec) => union_spans(vec.iter().map(|i| i.span())),
}
@ -1062,7 +1076,10 @@ impl Spanned for CreateTableOptions {
impl Spanned for AlterTableOperation {
fn span(&self) -> Span {
match self {
AlterTableOperation::AddConstraint(table_constraint) => table_constraint.span(),
AlterTableOperation::AddConstraint {
constraint,
not_valid: _,
} => constraint.span(),
AlterTableOperation::AddColumn {
column_keyword: _,
if_not_exists: _,
@ -1183,6 +1200,7 @@ impl Spanned for AlterTableOperation {
AlterTableOperation::AutoIncrement { value, .. } => value.span(),
AlterTableOperation::Lock { .. } => Span::empty(),
AlterTableOperation::ReplicaIdentity { .. } => Span::empty(),
AlterTableOperation::ValidateConstraint { name } => name.span,
}
}
}
@ -1398,7 +1416,6 @@ impl Spanned for AssignmentTarget {
/// f.e. `IS NULL <expr>` reports as `<expr>::span`.
///
/// Missing spans:
/// - [Expr::TypedString] # missing span for data_type
/// - [Expr::MatchAgainst] # MySQL specific
/// - [Expr::RLike] # MySQL specific
/// - [Expr::Struct] # BigQuery specific
@ -1607,6 +1624,7 @@ impl Spanned for Expr {
Expr::OuterJoin(expr) => expr.span(),
Expr::Prior(expr) => expr.span(),
Expr::Lambda(_) => Span::empty(),
Expr::MemberOf(member_of) => member_of.value.span().union(&member_of.array.span()),
}
}
}

View file

@ -116,7 +116,6 @@ impl From<ValueWithSpan> for Value {
derive(Visit, VisitMut),
visit(with = "visit_value")
)]
pub enum Value {
/// Numeric literal
#[cfg(not(feature = "bigdecimal"))]
@ -551,16 +550,16 @@ impl fmt::Display for EscapeUnicodeStringLiteral<'_> {
write!(f, r#"\\"#)?;
}
x if x.is_ascii() => {
write!(f, "{}", c)?;
write!(f, "{c}")?;
}
_ => {
let codepoint = c as u32;
// if the character fits in 32 bits, we can use the \XXXX format
// otherwise, we need to use the \+XXXXXX format
if codepoint <= 0xFFFF {
write!(f, "\\{:04X}", codepoint)?;
write!(f, "\\{codepoint:04X}")?;
} else {
write!(f, "\\+{:06X}", codepoint)?;
write!(f, "\\+{codepoint:06X}")?;
}
}
}

View file

@ -926,10 +926,10 @@ mod tests {
#[test]
fn overflow() {
let cond = (0..1000)
.map(|n| format!("X = {}", n))
.map(|n| format!("X = {n}"))
.collect::<Vec<_>>()
.join(" OR ");
let sql = format!("SELECT x where {0}", cond);
let sql = format!("SELECT x where {cond}");
let dialect = GenericDialect {};
let tokens = Tokenizer::new(&dialect, sql.as_str()).tokenize().unwrap();

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

@ -52,6 +52,10 @@ impl Dialect for GenericDialect {
true
}
fn supports_left_associative_joins_without_parens(&self) -> bool {
true
}
fn supports_connect_by(&self) -> bool {
true
}
@ -108,6 +112,10 @@ impl Dialect for GenericDialect {
true
}
fn supports_from_first_select(&self) -> bool {
true
}
fn supports_asc_desc_in_column_definition(&self) -> bool {
true
}

View file

@ -278,6 +278,34 @@ pub trait Dialect: Debug + Any {
false
}
/// Indicates whether the dialect supports left-associative join parsing
/// by default when parentheses are omitted in nested joins.
///
/// Most dialects (like MySQL or Postgres) assume **left-associative** precedence,
/// so a query like:
///
/// ```sql
/// SELECT * FROM t1 NATURAL JOIN t5 INNER JOIN t0 ON ...
/// ```
/// is interpreted as:
/// ```sql
/// ((t1 NATURAL JOIN t5) INNER JOIN t0 ON ...)
/// ```
/// and internally represented as a **flat list** of joins.
///
/// In contrast, some dialects (e.g. **Snowflake**) assume **right-associative**
/// precedence and interpret the same query as:
/// ```sql
/// (t1 NATURAL JOIN (t5 INNER JOIN t0 ON ...))
/// ```
/// which results in a **nested join** structure in the AST.
///
/// If this method returns `false`, the parser must build nested join trees
/// even in the absence of parentheses to reflect the correct associativity
fn supports_left_associative_joins_without_parens(&self) -> bool {
true
}
/// Returns true if the dialect supports the `(+)` syntax for OUTER JOIN.
fn supports_outer_join_operator(&self) -> bool {
false
@ -587,7 +615,7 @@ pub trait Dialect: Debug + Any {
}
let token = parser.peek_token();
debug!("get_next_precedence_full() {:?}", token);
debug!("get_next_precedence_full() {token:?}");
match token.token {
Token::Word(w) if w.keyword == Keyword::OR => Ok(p!(Or)),
Token::Word(w) if w.keyword == Keyword::AND => Ok(p!(And)),
@ -621,6 +649,7 @@ pub trait Dialect: Debug + Any {
Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(p!(Like)),
Token::Word(w) if w.keyword == Keyword::MATCH => Ok(p!(Like)),
Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)),
Token::Word(w) if w.keyword == Keyword::MEMBER => Ok(p!(Like)),
_ => Ok(self.prec_unknown()),
},
Token::Word(w) if w.keyword == Keyword::IS => Ok(p!(Is)),
@ -633,6 +662,7 @@ pub trait Dialect: Debug + Any {
Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(p!(Like)),
Token::Word(w) if w.keyword == Keyword::MATCH => Ok(p!(Like)),
Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)),
Token::Word(w) if w.keyword == Keyword::MEMBER => Ok(p!(Like)),
Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(p!(Between)),
Token::Word(w) if w.keyword == Keyword::DIV => Ok(p!(MulDivModOp)),
Token::Period => Ok(p!(Period)),
@ -1028,6 +1058,19 @@ pub trait Dialect: Debug + Any {
fn supports_set_names(&self) -> bool {
false
}
fn supports_space_separated_column_options(&self) -> bool {
false
}
/// Returns true if the dialect supports the `USING` clause in an `ALTER COLUMN` statement.
/// Example:
/// ```sql
/// ALTER TABLE tbl ALTER COLUMN col SET DATA TYPE <type> USING <exp>`
/// ```
fn supports_alter_column_type_using(&self) -> bool {
false
}
}
/// This represents the operators for which precedence must be defined

View file

@ -104,7 +104,7 @@ impl Dialect for PostgreSqlDialect {
fn get_next_precedence(&self, parser: &Parser) -> Option<Result<u8, ParserError>> {
let token = parser.peek_token();
debug!("get_next_precedence() {:?}", token);
debug!("get_next_precedence() {token:?}");
// we only return some custom value here when the behaviour (not merely the numeric value) differs
// from the default implementation
@ -258,4 +258,8 @@ impl Dialect for PostgreSqlDialect {
fn supports_set_names(&self) -> bool {
true
}
fn supports_alter_column_type_using(&self) -> bool {
true
}
}

View file

@ -80,13 +80,15 @@ impl Dialect for RedshiftSqlDialect {
}
fn is_identifier_start(&self, ch: char) -> bool {
// Extends Postgres dialect with sharp
PostgreSqlDialect {}.is_identifier_start(ch) || ch == '#'
// Extends Postgres dialect with sharp and UTF-8 multibyte chars
// https://docs.aws.amazon.com/redshift/latest/dg/r_names.html
PostgreSqlDialect {}.is_identifier_start(ch) || ch == '#' || !ch.is_ascii()
}
fn is_identifier_part(&self, ch: char) -> bool {
// Extends Postgres dialect with sharp
PostgreSqlDialect {}.is_identifier_part(ch) || ch == '#'
// Extends Postgres dialect with sharp and UTF-8 multibyte chars
// https://docs.aws.amazon.com/redshift/latest/dg/r_names.html
PostgreSqlDialect {}.is_identifier_part(ch) || ch == '#' || !ch.is_ascii()
}
/// redshift has `CONVERT(type, value)` instead of `CONVERT(value, type)`

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]) {
@ -279,6 +283,10 @@ impl Dialect for SnowflakeDialect {
true
}
fn supports_left_associative_joins_without_parens(&self) -> bool {
false
}
fn is_reserved_for_identifier(&self, kw: Keyword) -> bool {
// Unreserve some keywords that Snowflake accepts as identifiers
// See: https://docs.snowflake.com/en/sql-reference/reserved-keywords
@ -352,6 +360,10 @@ impl Dialect for SnowflakeDialect {
fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] {
&RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR
}
fn supports_space_separated_column_options(&self) -> bool {
true
}
}
fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result<Statement, ParserError> {

View file

@ -395,6 +395,7 @@ define_keywords!(
FUNCTION,
FUNCTIONS,
FUSION,
FUTURE,
GENERAL,
GENERATE,
GENERATED,
@ -646,6 +647,7 @@ define_keywords!(
ORDER,
ORDINALITY,
ORGANIZATION,
OTHER,
OUT,
OUTER,
OUTPUT,
@ -814,6 +816,7 @@ define_keywords!(
SERDE,
SERDEPROPERTIES,
SERIALIZABLE,
SERVER,
SERVICE,
SESSION,
SESSION_USER,
@ -934,6 +937,8 @@ define_keywords!(
TRY,
TRY_CAST,
TRY_CONVERT,
TSQUERY,
TSVECTOR,
TUPLE,
TYPE,
UBIGINT,
@ -977,6 +982,7 @@ define_keywords!(
UUID,
VACUUM,
VALID,
VALIDATE,
VALIDATION_MODE,
VALUE,
VALUES,
@ -1012,6 +1018,7 @@ define_keywords!(
WITHOUT,
WITHOUT_ARRAY_WRAPPER,
WORK,
WRAPPER,
WRITE,
XML,
XMLNAMESPACES,

View file

@ -436,7 +436,7 @@ impl<'a> Parser<'a> {
///
/// See example on [`Parser::new()`] for an example
pub fn try_with_sql(self, sql: &str) -> Result<Self, ParserError> {
debug!("Parsing sql '{}'...", sql);
debug!("Parsing sql '{sql}'...");
let tokens = Tokenizer::new(self.dialect, sql)
.with_unescape(self.options.unescape)
.tokenize_with_location()?;
@ -1226,10 +1226,10 @@ impl<'a> Parser<'a> {
expr = self.parse_compound_expr(expr, vec![])?;
debug!("prefix: {:?}", expr);
debug!("prefix: {expr:?}");
loop {
let next_precedence = self.get_next_precedence()?;
debug!("next precedence: {:?}", next_precedence);
debug!("next precedence: {next_precedence:?}");
if precedence >= next_precedence {
break;
@ -1521,7 +1521,7 @@ impl<'a> Parser<'a> {
DataType::Custom(..) => parser_err!("dummy", loc),
data_type => Ok(Expr::TypedString {
data_type,
value: parser.parse_value()?.value,
value: parser.parse_value()?,
}),
}
})?;
@ -1631,8 +1631,7 @@ impl<'a> Parser<'a> {
Token::QuestionPipe => UnaryOperator::QuestionPipe,
_ => {
return Err(ParserError::ParserError(format!(
"Unexpected token in unary operator parsing: {:?}",
tok
"Unexpected token in unary operator parsing: {tok:?}"
)))
}
};
@ -1709,10 +1708,9 @@ impl<'a> Parser<'a> {
}
fn parse_geometric_type(&mut self, kind: GeometricTypeKind) -> Result<Expr, ParserError> {
let value: Value = self.parse_value()?.value;
Ok(Expr::TypedString {
data_type: DataType::GeometricType(kind),
value,
value: self.parse_value()?,
})
}
@ -2577,7 +2575,7 @@ impl<'a> Parser<'a> {
trim_characters: None,
})
} else if self.consume_token(&Token::Comma)
&& dialect_of!(self is SnowflakeDialect | BigQueryDialect | GenericDialect)
&& dialect_of!(self is DuckDbDialect | SnowflakeDialect | BigQueryDialect | GenericDialect)
{
let characters = self.parse_comma_separated(Parser::parse_expr)?;
self.expect_token(&Token::RParen)?;
@ -2771,7 +2769,7 @@ impl<'a> Parser<'a> {
if self.dialect.supports_dictionary_syntax() {
self.prev_token(); // Put back the '{'
return self.parse_duckdb_struct_literal();
return self.parse_dictionary();
}
self.expected("an expression", token)
@ -3034,7 +3032,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.
@ -3047,16 +3044,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((
@ -3147,7 +3138,7 @@ impl<'a> Parser<'a> {
Ok(fields)
}
/// DuckDB specific: Parse a duckdb [dictionary]
/// DuckDB and ClickHouse specific: Parse a duckdb [dictionary] or a clickhouse [map] setting
///
/// Syntax:
///
@ -3156,18 +3147,18 @@ impl<'a> Parser<'a> {
/// ```
///
/// [dictionary]: https://duckdb.org/docs/sql/data_types/struct#creating-structs
fn parse_duckdb_struct_literal(&mut self) -> Result<Expr, ParserError> {
/// [map]: https://clickhouse.com/docs/operations/settings/settings#additional_table_filters
fn parse_dictionary(&mut self) -> Result<Expr, ParserError> {
self.expect_token(&Token::LBrace)?;
let fields =
self.parse_comma_separated0(Self::parse_duckdb_dictionary_field, Token::RBrace)?;
let fields = self.parse_comma_separated0(Self::parse_dictionary_field, Token::RBrace)?;
self.expect_token(&Token::RBrace)?;
Ok(Expr::Dictionary(fields))
}
/// Parse a field for a duckdb [dictionary]
/// Parse a field for a duckdb [dictionary] or a clickhouse [map] setting
///
/// Syntax
///
@ -3176,7 +3167,8 @@ impl<'a> Parser<'a> {
/// ```
///
/// [dictionary]: https://duckdb.org/docs/sql/data_types/struct#creating-structs
fn parse_duckdb_dictionary_field(&mut self) -> Result<DictionaryField, ParserError> {
/// [map]: https://clickhouse.com/docs/operations/settings/settings#additional_table_filters
fn parse_dictionary_field(&mut self) -> Result<DictionaryField, ParserError> {
let key = self.parse_identifier()?;
self.expect_token(&Token::Colon)?;
@ -3616,6 +3608,19 @@ impl<'a> Parser<'a> {
self.expected("IN or BETWEEN after NOT", self.peek_token())
}
}
Keyword::MEMBER => {
if self.parse_keyword(Keyword::OF) {
self.expect_token(&Token::LParen)?;
let array = self.parse_expr()?;
self.expect_token(&Token::RParen)?;
Ok(Expr::MemberOf(MemberOf {
value: Box::new(expr),
array: Box::new(array),
}))
} else {
self.expected("OF after MEMBER", self.peek_token())
}
}
// Can only happen if `get_next_precedence` got out of sync with this function
_ => parser_err!(
format!("No infix parser for token {:?}", tok.token),
@ -3824,7 +3829,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,
@ -4669,6 +4674,8 @@ impl<'a> Parser<'a> {
self.parse_create_procedure(or_alter)
} else if self.parse_keyword(Keyword::CONNECTOR) {
self.parse_create_connector()
} else if self.parse_keyword(Keyword::SERVER) {
self.parse_pg_create_server()
} else {
self.expected("an object type after CREATE", self.peek_token())
}
@ -6875,9 +6882,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)?;
@ -7633,9 +7638,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> {
@ -8077,7 +8095,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 {
@ -8099,7 +8117,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 {
@ -8177,7 +8195,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,
@ -8206,7 +8224,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,
@ -8473,7 +8491,11 @@ impl<'a> Parser<'a> {
pub fn parse_alter_table_operation(&mut self) -> Result<AlterTableOperation, ParserError> {
let operation = if self.parse_keyword(Keyword::ADD) {
if let Some(constraint) = self.parse_optional_table_constraint()? {
AlterTableOperation::AddConstraint(constraint)
let not_valid = self.parse_keywords(&[Keyword::NOT, Keyword::VALID]);
AlterTableOperation::AddConstraint {
constraint,
not_valid,
}
} else if dialect_of!(self is ClickHouseDialect|GenericDialect)
&& self.parse_keyword(Keyword::PROJECTION)
{
@ -8730,16 +8752,10 @@ impl<'a> Parser<'a> {
}
} else if self.parse_keywords(&[Keyword::DROP, Keyword::DEFAULT]) {
AlterColumnOperation::DropDefault {}
} else if self.parse_keywords(&[Keyword::SET, Keyword::DATA, Keyword::TYPE])
|| (is_postgresql && self.parse_keyword(Keyword::TYPE))
{
let data_type = self.parse_data_type()?;
let using = if is_postgresql && self.parse_keyword(Keyword::USING) {
Some(self.parse_expr()?)
} else {
None
};
AlterColumnOperation::SetDataType { data_type, using }
} else if self.parse_keywords(&[Keyword::SET, Keyword::DATA, Keyword::TYPE]) {
self.parse_set_data_type(true)?
} else if self.parse_keyword(Keyword::TYPE) {
self.parse_set_data_type(false)?
} else if self.parse_keywords(&[Keyword::ADD, Keyword::GENERATED]) {
let generated_as = if self.parse_keyword(Keyword::ALWAYS) {
Some(GeneratedAs::Always)
@ -8888,6 +8904,9 @@ impl<'a> Parser<'a> {
};
AlterTableOperation::ReplicaIdentity { identity }
} else if self.parse_keywords(&[Keyword::VALIDATE, Keyword::CONSTRAINT]) {
let name = self.parse_identifier()?;
AlterTableOperation::ValidateConstraint { name }
} else {
let options: Vec<SqlOption> =
self.parse_options_with_keywords(&[Keyword::SET, Keyword::TBLPROPERTIES])?;
@ -8905,6 +8924,22 @@ impl<'a> Parser<'a> {
Ok(operation)
}
fn parse_set_data_type(&mut self, had_set: bool) -> Result<AlterColumnOperation, ParserError> {
let data_type = self.parse_data_type()?;
let using = if self.dialect.supports_alter_column_type_using()
&& self.parse_keyword(Keyword::USING)
{
Some(self.parse_expr()?)
} else {
None
};
Ok(AlterColumnOperation::SetDataType {
data_type,
using,
had_set,
})
}
fn parse_part_or_partition(&mut self) -> Result<Partition, ParserError> {
let keyword = self.expect_one_of_keywords(&[Keyword::PART, Keyword::PARTITION])?;
match keyword {
@ -9916,6 +9951,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)?;
@ -9978,6 +10019,48 @@ impl<'a> Parser<'a> {
Ok(IdentWithAlias { ident, alias })
}
/// Parse `identifier [AS] identifier` where the AS keyword is optional
fn parse_identifier_with_optional_alias(&mut self) -> Result<IdentWithAlias, ParserError> {
let ident = self.parse_identifier()?;
let _after_as = self.parse_keyword(Keyword::AS);
let alias = self.parse_identifier()?;
Ok(IdentWithAlias { ident, alias })
}
/// Parse comma-separated list of parenthesized queries for pipe operators
fn parse_pipe_operator_queries(&mut self) -> Result<Vec<Query>, ParserError> {
self.parse_comma_separated(|parser| {
parser.expect_token(&Token::LParen)?;
let query = parser.parse_query()?;
parser.expect_token(&Token::RParen)?;
Ok(*query)
})
}
/// Parse set quantifier for pipe operators that require DISTINCT. E.g. INTERSECT and EXCEPT
fn parse_distinct_required_set_quantifier(
&mut self,
operator_name: &str,
) -> Result<SetQuantifier, ParserError> {
let quantifier = self.parse_set_quantifier(&Some(SetOperator::Intersect));
match quantifier {
SetQuantifier::Distinct | SetQuantifier::DistinctByName => Ok(quantifier),
_ => Err(ParserError::ParserError(format!(
"{operator_name} pipe operator requires DISTINCT modifier",
))),
}
}
/// Parse optional identifier alias (with or without AS keyword)
fn parse_identifier_optional_alias(&mut self) -> Result<Option<Ident>, ParserError> {
if self.parse_keyword(Keyword::AS) {
Ok(Some(self.parse_identifier()?))
} else {
// Check if the next token is an identifier (implicit alias)
self.maybe_parse(|parser| parser.parse_identifier())
}
}
/// Optionally parses an alias for a select list item
fn maybe_parse_select_item_alias(&mut self) -> Result<Option<Ident>, ParserError> {
fn validator(explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool {
@ -10569,17 +10652,7 @@ impl<'a> Parser<'a> {
/// Parses a column definition within a view.
fn parse_view_column(&mut self) -> Result<ViewColumnDef, ParserError> {
let name = self.parse_identifier()?;
let options = if (dialect_of!(self is BigQueryDialect | GenericDialect)
&& self.parse_keyword(Keyword::OPTIONS))
|| (dialect_of!(self is SnowflakeDialect | GenericDialect)
&& self.parse_keyword(Keyword::COMMENT))
{
self.prev_token();
self.parse_optional_column_option()?
.map(|option| vec![option])
} else {
None
};
let options = self.parse_view_column_options()?;
let data_type = if dialect_of!(self is ClickHouseDialect) {
Some(self.parse_data_type()?)
} else {
@ -10592,6 +10665,25 @@ impl<'a> Parser<'a> {
})
}
fn parse_view_column_options(&mut self) -> Result<Option<ColumnOptions>, ParserError> {
let mut options = Vec::new();
loop {
let option = self.parse_optional_column_option()?;
if let Some(option) = option {
options.push(option);
} else {
break;
}
}
if options.is_empty() {
Ok(None)
} else if self.dialect.supports_space_separated_column_options() {
Ok(Some(ColumnOptions::SpaceSeparated(options)))
} else {
Ok(Some(ColumnOptions::CommaSeparated(options)))
}
}
/// Parses a parenthesized comma-separated list of unqualified, possibly quoted identifiers.
/// For example: `(col1, "col 2", ...)`
pub fn parse_parenthesized_column_list(
@ -10602,6 +10694,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(
@ -11107,6 +11207,19 @@ impl<'a> Parser<'a> {
Keyword::AGGREGATE,
Keyword::ORDER,
Keyword::TABLESAMPLE,
Keyword::RENAME,
Keyword::UNION,
Keyword::INTERSECT,
Keyword::EXCEPT,
Keyword::CALL,
Keyword::PIVOT,
Keyword::UNPIVOT,
Keyword::JOIN,
Keyword::INNER,
Keyword::LEFT,
Keyword::RIGHT,
Keyword::FULL,
Keyword::CROSS,
])?;
match kw {
Keyword::SELECT => {
@ -11173,6 +11286,121 @@ impl<'a> Parser<'a> {
let sample = self.parse_table_sample(TableSampleModifier::TableSample)?;
pipe_operators.push(PipeOperator::TableSample { sample });
}
Keyword::RENAME => {
let mappings =
self.parse_comma_separated(Parser::parse_identifier_with_optional_alias)?;
pipe_operators.push(PipeOperator::Rename { mappings });
}
Keyword::UNION => {
let set_quantifier = self.parse_set_quantifier(&Some(SetOperator::Union));
let queries = self.parse_pipe_operator_queries()?;
pipe_operators.push(PipeOperator::Union {
set_quantifier,
queries,
});
}
Keyword::INTERSECT => {
let set_quantifier =
self.parse_distinct_required_set_quantifier("INTERSECT")?;
let queries = self.parse_pipe_operator_queries()?;
pipe_operators.push(PipeOperator::Intersect {
set_quantifier,
queries,
});
}
Keyword::EXCEPT => {
let set_quantifier = self.parse_distinct_required_set_quantifier("EXCEPT")?;
let queries = self.parse_pipe_operator_queries()?;
pipe_operators.push(PipeOperator::Except {
set_quantifier,
queries,
});
}
Keyword::CALL => {
let function_name = self.parse_object_name(false)?;
let function_expr = self.parse_function(function_name)?;
if let Expr::Function(function) = function_expr {
let alias = self.parse_identifier_optional_alias()?;
pipe_operators.push(PipeOperator::Call { function, alias });
} else {
return Err(ParserError::ParserError(
"Expected function call after CALL".to_string(),
));
}
}
Keyword::PIVOT => {
self.expect_token(&Token::LParen)?;
let aggregate_functions =
self.parse_comma_separated(Self::parse_aliased_function_call)?;
self.expect_keyword_is(Keyword::FOR)?;
let value_column = self.parse_period_separated(|p| p.parse_identifier())?;
self.expect_keyword_is(Keyword::IN)?;
self.expect_token(&Token::LParen)?;
let value_source = if self.parse_keyword(Keyword::ANY) {
let order_by = if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) {
self.parse_comma_separated(Parser::parse_order_by_expr)?
} else {
vec![]
};
PivotValueSource::Any(order_by)
} else if self.peek_sub_query() {
PivotValueSource::Subquery(self.parse_query()?)
} else {
PivotValueSource::List(
self.parse_comma_separated(Self::parse_expr_with_alias)?,
)
};
self.expect_token(&Token::RParen)?;
self.expect_token(&Token::RParen)?;
let alias = self.parse_identifier_optional_alias()?;
pipe_operators.push(PipeOperator::Pivot {
aggregate_functions,
value_column,
value_source,
alias,
});
}
Keyword::UNPIVOT => {
self.expect_token(&Token::LParen)?;
let value_column = self.parse_identifier()?;
self.expect_keyword(Keyword::FOR)?;
let name_column = self.parse_identifier()?;
self.expect_keyword(Keyword::IN)?;
self.expect_token(&Token::LParen)?;
let unpivot_columns = self.parse_comma_separated(Parser::parse_identifier)?;
self.expect_token(&Token::RParen)?;
self.expect_token(&Token::RParen)?;
let alias = self.parse_identifier_optional_alias()?;
pipe_operators.push(PipeOperator::Unpivot {
value_column,
name_column,
unpivot_columns,
alias,
});
}
Keyword::JOIN
| Keyword::INNER
| Keyword::LEFT
| Keyword::RIGHT
| Keyword::FULL
| Keyword::CROSS => {
self.prev_token();
let mut joins = self.parse_joins()?;
if joins.len() != 1 {
return Err(ParserError::ParserError(
"Join pipe operator must have a single join".to_string(),
));
}
let join = joins.swap_remove(0);
pipe_operators.push(PipeOperator::Join(join))
}
unhandled => {
return Err(ParserError::ParserError(format!(
"`expect_one_of_keywords` further up allowed unhandled keyword: {unhandled:?}"
@ -11190,7 +11418,7 @@ impl<'a> Parser<'a> {
let key_values = self.parse_comma_separated(|p| {
let key = p.parse_identifier()?;
p.expect_token(&Token::Eq)?;
let value = p.parse_value()?.value;
let value = p.parse_expr()?;
Ok(Setting { key, value })
})?;
Some(key_values)
@ -12468,7 +12696,11 @@ impl<'a> Parser<'a> {
};
let mut relation = self.parse_table_factor()?;
if self.peek_parens_less_nested_join() {
if !self
.dialect
.supports_left_associative_joins_without_parens()
&& self.peek_parens_less_nested_join()
{
let joins = self.parse_joins()?;
relation = TableFactor::NestedJoin {
table_with_joins: Box::new(TableWithJoins { relation, joins }),
@ -13628,7 +13860,7 @@ impl<'a> Parser<'a> {
let ident = self.parse_identifier()?;
if let GranteeName::ObjectName(namespace) = name {
name = GranteeName::ObjectName(ObjectName::from(vec![Ident::new(
format!("{}:{}", namespace, ident),
format!("{namespace}:{ident}"),
)]));
};
}
@ -13665,6 +13897,33 @@ impl<'a> Parser<'a> {
Some(GrantObjects::AllTablesInSchema {
schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?,
})
} else if self.parse_keywords(&[
Keyword::FUTURE,
Keyword::SCHEMAS,
Keyword::IN,
Keyword::DATABASE,
]) {
Some(GrantObjects::FutureSchemasInDatabase {
databases: self.parse_comma_separated(|p| p.parse_object_name(false))?,
})
} else if self.parse_keywords(&[
Keyword::FUTURE,
Keyword::TABLES,
Keyword::IN,
Keyword::SCHEMA,
]) {
Some(GrantObjects::FutureTablesInSchema {
schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?,
})
} else if self.parse_keywords(&[
Keyword::FUTURE,
Keyword::VIEWS,
Keyword::IN,
Keyword::SCHEMA,
]) {
Some(GrantObjects::FutureViewsInSchema {
schemas: self.parse_comma_separated(|p| p.parse_object_name(false))?,
})
} else if self.parse_keywords(&[
Keyword::ALL,
Keyword::SEQUENCES,
@ -14598,7 +14857,7 @@ impl<'a> Parser<'a> {
self.dialect
.get_reserved_keywords_for_select_item_operator(),
)
.map(|keyword| Ident::new(format!("{:?}", keyword)));
.map(|keyword| Ident::new(format!("{keyword:?}")));
match self.parse_wildcard_expr()? {
Expr::QualifiedWildcard(prefix, token) => Ok(SelectItem::QualifiedWildcard(
@ -15018,7 +15277,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()
@ -15027,16 +15287,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,
@ -15099,7 +15359,7 @@ impl<'a> Parser<'a> {
transaction: Some(BeginTransactionKind::Transaction),
modifier: None,
statements: vec![],
exception_statements: None,
exception: None,
has_end_keyword: false,
})
}
@ -15131,11 +15391,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
@ -15706,6 +16011,49 @@ impl<'a> Parser<'a> {
Ok(sequence_options)
}
/// Parse a `CREATE SERVER` statement.
///
/// See [Statement::CreateServer]
pub fn parse_pg_create_server(&mut self) -> Result<Statement, ParserError> {
let ine = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
let name = self.parse_object_name(false)?;
let server_type = if self.parse_keyword(Keyword::TYPE) {
Some(self.parse_identifier()?)
} else {
None
};
let version = if self.parse_keyword(Keyword::VERSION) {
Some(self.parse_identifier()?)
} else {
None
};
self.expect_keywords(&[Keyword::FOREIGN, Keyword::DATA, Keyword::WRAPPER])?;
let foreign_data_wrapper = self.parse_object_name(false)?;
let mut options = None;
if self.parse_keyword(Keyword::OPTIONS) {
self.expect_token(&Token::LParen)?;
options = Some(self.parse_comma_separated(|p| {
let key = p.parse_identifier()?;
let value = p.parse_identifier()?;
Ok(CreateServerOption { key, value })
})?);
self.expect_token(&Token::RParen)?;
}
Ok(Statement::CreateServer(CreateServerStatement {
name,
if_not_exists: ine,
server_type,
version,
foreign_data_wrapper,
options,
}))
}
/// The index of the first unprocessed token.
pub fn index(&self) -> usize {
self.index
@ -15729,6 +16077,13 @@ impl<'a> Parser<'a> {
pub fn parse_create_procedure(&mut self, or_alter: bool) -> Result<Statement, ParserError> {
let name = self.parse_object_name(false)?;
let params = self.parse_optional_procedure_parameters()?;
let language = if self.parse_keyword(Keyword::LANGUAGE) {
Some(self.parse_identifier()?)
} else {
None
};
self.expect_keyword_is(Keyword::AS)?;
let body = self.parse_conditional_statements(&[Keyword::END])?;
@ -15737,6 +16092,7 @@ impl<'a> Parser<'a> {
name,
or_alter,
params,
language,
body,
})
}
@ -16483,6 +16839,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 {})]);
@ -16493,7 +16863,7 @@ mod tests {
display_as_key: false,
name: None,
index_type: None,
columns: vec![Ident::new("c1")],
columns: vec![mk_expected_col("c1")],
}
);
@ -16504,7 +16874,7 @@ mod tests {
display_as_key: true,
name: None,
index_type: None,
columns: vec![Ident::new("c1")],
columns: vec![mk_expected_col("c1")],
}
);
@ -16515,7 +16885,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")],
}
);
@ -16526,7 +16896,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")],
}
);
@ -16537,7 +16907,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")],
}
);
@ -16548,7 +16918,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")],
}
);
@ -16559,7 +16929,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

@ -270,7 +270,7 @@ impl TestedDialects {
tokenizer = tokenizer.with_unescape(options.unescape);
}
let tokens = tokenizer.tokenize().unwrap();
assert_eq!(expected, tokens, "Tokenized differently for {:?}", dialect);
assert_eq!(expected, tokens, "Tokenized differently for {dialect:?}");
});
}
}
@ -366,6 +366,11 @@ pub fn number(n: &str) -> Value {
Value::Number(n.parse().unwrap(), false)
}
/// Creates a [Value::SingleQuotedString]
pub fn single_quoted_string(s: impl Into<String>) -> Value {
Value::SingleQuotedString(s.into())
}
pub fn table_alias(name: impl Into<String>) -> Option<TableAlias> {
Some(TableAlias {
name: Ident::new(name),
@ -448,3 +453,52 @@ 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 { constraint, .. } => {
match constraint {
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)"),
}
}
_ => panic!("Expected a constraint"),
},
_ => panic!("Expected CREATE INDEX, ALTER TABLE, or CREATE TABLE, got: {stmt:?}"),
}
}

View file

@ -1751,7 +1751,7 @@ impl<'a> Tokenizer<'a> {
(None, Some(tok)) => Ok(Some(tok)),
(None, None) => self.tokenizer_error(
chars.location(),
format!("Expected a valid binary operator after '{}'", prefix),
format!("Expected a valid binary operator after '{prefix}'"),
),
}
}
@ -1809,7 +1809,7 @@ impl<'a> Tokenizer<'a> {
chars.next();
let mut temp = String::new();
let end_delimiter = format!("${}$", value);
let end_delimiter = format!("${value}$");
loop {
match chars.next() {
@ -2402,13 +2402,13 @@ fn take_char_from_hex_digits(
location: chars.location(),
})?;
let digit = next_char.to_digit(16).ok_or_else(|| TokenizerError {
message: format!("Invalid hex digit in escaped unicode string: {}", next_char),
message: format!("Invalid hex digit in escaped unicode string: {next_char}"),
location: chars.location(),
})?;
result = result * 16 + digit;
}
char::from_u32(result).ok_or_else(|| TokenizerError {
message: format!("Invalid unicode character: {:x}", result),
message: format!("Invalid unicode character: {result:x}"),
location: chars.location(),
})
}
@ -3504,7 +3504,7 @@ mod tests {
}
fn check_unescape(s: &str, expected: Option<&str>) {
let s = format!("'{}'", s);
let s = format!("'{s}'");
let mut state = State {
peekable: s.chars().peekable(),
line: 0,

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(
@ -352,14 +355,16 @@ fn parse_create_view_with_options() {
ViewColumnDef {
name: Ident::new("age"),
data_type: None,
options: Some(vec![ColumnOption::Options(vec![SqlOption::KeyValue {
key: Ident::new("description"),
value: Expr::Value(
Value::DoubleQuotedString("field age".to_string()).with_span(
Span::new(Location::new(1, 42), Location::new(1, 52))
)
),
}])]),
options: Some(ColumnOptions::CommaSeparated(vec![ColumnOption::Options(
vec![SqlOption::KeyValue {
key: Ident::new("description"),
value: Expr::Value(
Value::DoubleQuotedString("field age".to_string()).with_span(
Span::new(Location::new(1, 42), Location::new(1, 52))
)
),
}]
)])),
},
],
columns
@ -635,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
@ -930,7 +906,10 @@ fn parse_typed_struct_syntax_bigquery() {
&Expr::Struct {
values: vec![Expr::TypedString {
data_type: DataType::Datetime(None),
value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into())
value: ValueWithSpan {
value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()),
span: Span::empty(),
},
}],
fields: vec![StructField {
field_name: None,
@ -989,9 +968,12 @@ fn parse_typed_struct_syntax_bigquery() {
&Expr::Struct {
values: vec![Expr::TypedString {
data_type: DataType::JSON,
value: Value::SingleQuotedString(
r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into()
)
value: ValueWithSpan {
value: Value::SingleQuotedString(
r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into()
),
span: Span::empty(),
}
}],
fields: vec![StructField {
field_name: None,
@ -1022,7 +1004,12 @@ fn parse_typed_struct_syntax_bigquery() {
&Expr::Struct {
values: vec![Expr::TypedString {
data_type: DataType::Timestamp(None, TimezoneInfo::None),
value: Value::SingleQuotedString("2008-12-25 15:30:00 America/Los_Angeles".into())
value: ValueWithSpan {
value: Value::SingleQuotedString(
"2008-12-25 15:30:00 America/Los_Angeles".into()
),
span: Span::empty(),
},
}],
fields: vec![StructField {
field_name: None,
@ -1037,7 +1024,10 @@ fn parse_typed_struct_syntax_bigquery() {
&Expr::Struct {
values: vec![Expr::TypedString {
data_type: DataType::Time(None, TimezoneInfo::None),
value: Value::SingleQuotedString("15:30:00".into())
value: ValueWithSpan {
value: Value::SingleQuotedString("15:30:00".into()),
span: Span::empty(),
}
}],
fields: vec![StructField {
field_name: None,
@ -1055,7 +1045,10 @@ fn parse_typed_struct_syntax_bigquery() {
&Expr::Struct {
values: vec![Expr::TypedString {
data_type: DataType::Numeric(ExactNumberInfo::None),
value: Value::SingleQuotedString("1".into())
value: ValueWithSpan {
value: Value::SingleQuotedString("1".into()),
span: Span::empty(),
}
}],
fields: vec![StructField {
field_name: None,
@ -1069,7 +1062,10 @@ fn parse_typed_struct_syntax_bigquery() {
&Expr::Struct {
values: vec![Expr::TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: Value::SingleQuotedString("1".into())
value: ValueWithSpan {
value: Value::SingleQuotedString("1".into()),
span: Span::empty(),
}
}],
fields: vec![StructField {
field_name: None,
@ -1243,7 +1239,10 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
&Expr::Struct {
values: vec![Expr::TypedString {
data_type: DataType::Datetime(None),
value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into())
value: ValueWithSpan {
value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()),
span: Span::empty(),
}
}],
fields: vec![StructField {
field_name: None,
@ -1302,9 +1301,12 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
&Expr::Struct {
values: vec![Expr::TypedString {
data_type: DataType::JSON,
value: Value::SingleQuotedString(
r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into()
)
value: ValueWithSpan {
value: Value::SingleQuotedString(
r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into()
),
span: Span::empty(),
}
}],
fields: vec![StructField {
field_name: None,
@ -1335,7 +1337,12 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
&Expr::Struct {
values: vec![Expr::TypedString {
data_type: DataType::Timestamp(None, TimezoneInfo::None),
value: Value::SingleQuotedString("2008-12-25 15:30:00 America/Los_Angeles".into())
value: ValueWithSpan {
value: Value::SingleQuotedString(
"2008-12-25 15:30:00 America/Los_Angeles".into()
),
span: Span::empty(),
}
}],
fields: vec![StructField {
field_name: None,
@ -1350,7 +1357,10 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
&Expr::Struct {
values: vec![Expr::TypedString {
data_type: DataType::Time(None, TimezoneInfo::None),
value: Value::SingleQuotedString("15:30:00".into())
value: ValueWithSpan {
value: Value::SingleQuotedString("15:30:00".into()),
span: Span::empty(),
}
}],
fields: vec![StructField {
field_name: None,
@ -1368,7 +1378,10 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
&Expr::Struct {
values: vec![Expr::TypedString {
data_type: DataType::Numeric(ExactNumberInfo::None),
value: Value::SingleQuotedString("1".into())
value: ValueWithSpan {
value: Value::SingleQuotedString("1".into()),
span: Span::empty(),
}
}],
fields: vec![StructField {
field_name: None,
@ -1382,7 +1395,10 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
&Expr::Struct {
values: vec![Expr::TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: Value::SingleQuotedString("1".into())
value: ValueWithSpan {
value: Value::SingleQuotedString("1".into()),
span: Span::empty(),
}
}],
fields: vec![StructField {
field_name: None,
@ -2417,7 +2433,10 @@ fn test_triple_quote_typed_strings() {
assert_eq!(
Expr::TypedString {
data_type: DataType::JSON,
value: Value::TripleDoubleQuotedString(r#"{"foo":"bar's"}"#.into())
value: ValueWithSpan {
value: Value::TripleDoubleQuotedString(r#"{"foo":"bar's"}"#.into()),
span: Span::empty(),
}
},
expr
);
@ -2472,3 +2491,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

@ -28,7 +28,7 @@ use test_utils::*;
use sqlparser::ast::Expr::{BinaryOp, Identifier};
use sqlparser::ast::SelectItem::UnnamedExpr;
use sqlparser::ast::TableFactor::Table;
use sqlparser::ast::Value::Number;
use sqlparser::ast::Value::Boolean;
use sqlparser::ast::*;
use sqlparser::dialect::ClickHouseDialect;
use sqlparser::dialect::GenericDialect;
@ -914,7 +914,7 @@ fn parse_create_view_with_fields_data_types() {
}]),
vec![]
)),
options: None
options: None,
},
ViewColumnDef {
name: "f".into(),
@ -926,7 +926,7 @@ fn parse_create_view_with_fields_data_types() {
}]),
vec![]
)),
options: None
options: None,
},
]
);
@ -965,38 +965,103 @@ fn parse_limit_by() {
#[test]
fn parse_settings_in_query() {
match clickhouse_and_generic()
.verified_stmt(r#"SELECT * FROM t SETTINGS max_threads = 1, max_block_size = 10000"#)
{
Statement::Query(query) => {
assert_eq!(
query.settings,
Some(vec![
Setting {
key: Ident::new("max_threads"),
value: Number("1".parse().unwrap(), false)
},
Setting {
key: Ident::new("max_block_size"),
value: Number("10000".parse().unwrap(), false)
},
])
);
fn check_settings(sql: &str, expected: Vec<Setting>) {
match clickhouse_and_generic().verified_stmt(sql) {
Statement::Query(q) => {
assert_eq!(q.settings, Some(expected));
}
_ => unreachable!(),
}
_ => unreachable!(),
}
for (sql, expected_settings) in [
(
r#"SELECT * FROM t SETTINGS max_threads = 1, max_block_size = 10000"#,
vec![
Setting {
key: Ident::new("max_threads"),
value: Expr::value(number("1")),
},
Setting {
key: Ident::new("max_block_size"),
value: Expr::value(number("10000")),
},
],
),
(
r#"SELECT * FROM t SETTINGS additional_table_filters = {'table_1': 'x != 2'}"#,
vec![Setting {
key: Ident::new("additional_table_filters"),
value: Expr::Dictionary(vec![DictionaryField {
key: Ident::with_quote('\'', "table_1"),
value: Expr::value(single_quoted_string("x != 2")).into(),
}]),
}],
),
(
r#"SELECT * FROM t SETTINGS additional_result_filter = 'x != 2', query_plan_optimize_lazy_materialization = false"#,
vec![
Setting {
key: Ident::new("additional_result_filter"),
value: Expr::value(single_quoted_string("x != 2")),
},
Setting {
key: Ident::new("query_plan_optimize_lazy_materialization"),
value: Expr::value(Boolean(false)),
},
],
),
] {
check_settings(sql, expected_settings);
}
let invalid_cases = vec![
"SELECT * FROM t SETTINGS a",
"SELECT * FROM t SETTINGS a=",
"SELECT * FROM t SETTINGS a=1, b",
"SELECT * FROM t SETTINGS a=1, b=",
"SELECT * FROM t SETTINGS a=1, b=c",
("SELECT * FROM t SETTINGS a", "Expected: =, found: EOF"),
(
"SELECT * FROM t SETTINGS a=",
"Expected: an expression, found: EOF",
),
("SELECT * FROM t SETTINGS a=1, b", "Expected: =, found: EOF"),
(
"SELECT * FROM t SETTINGS a=1, b=",
"Expected: an expression, found: EOF",
),
(
"SELECT * FROM t SETTINGS a = {",
"Expected: identifier, found: EOF",
),
(
"SELECT * FROM t SETTINGS a = {'b'",
"Expected: :, found: EOF",
),
(
"SELECT * FROM t SETTINGS a = {'b': ",
"Expected: an expression, found: EOF",
),
(
"SELECT * FROM t SETTINGS a = {'b': 'c',}",
"Expected: identifier, found: }",
),
(
"SELECT * FROM t SETTINGS a = {'b': 'c', 'd'}",
"Expected: :, found: }",
),
(
"SELECT * FROM t SETTINGS a = {'b': 'c', 'd': }",
"Expected: an expression, found: }",
),
(
"SELECT * FROM t SETTINGS a = {ANY(b)}",
"Expected: :, found: (",
),
];
for sql in invalid_cases {
clickhouse_and_generic()
.parse_sql_statements(sql)
.expect_err("Expected: SETTINGS key = value, found: ");
for (sql, error_msg) in invalid_cases {
assert_eq!(
clickhouse_and_generic()
.parse_sql_statements(sql)
.unwrap_err(),
ParserError(error_msg.to_string())
);
}
}
#[test]
@ -1345,7 +1410,7 @@ fn parse_use() {
for object_name in &valid_object_names {
// Test single identifier without quotes
assert_eq!(
clickhouse().verified_stmt(&format!("USE {}", object_name)),
clickhouse().verified_stmt(&format!("USE {object_name}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::new(
object_name.to_string()
)])))
@ -1353,7 +1418,7 @@ fn parse_use() {
for &quote in &quote_styles {
// Test single identifier with different type of quotes
assert_eq!(
clickhouse().verified_stmt(&format!("USE {0}{1}{0}", quote, object_name)),
clickhouse().verified_stmt(&format!("USE {quote}{object_name}{quote}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote(
quote,
object_name.to_string(),
@ -1367,7 +1432,7 @@ fn parse_use() {
fn test_query_with_format_clause() {
let format_options = vec!["TabSeparated", "JSONCompact", "NULL"];
for format in &format_options {
let sql = format!("SELECT * FROM t FORMAT {}", format);
let sql = format!("SELECT * FROM t FORMAT {format}");
match clickhouse_and_generic().verified_stmt(&sql) {
Statement::Query(query) => {
if *format == "NULL" {
@ -1550,11 +1615,11 @@ fn parse_select_table_function_settings() {
settings: Some(vec![
Setting {
key: "s0".into(),
value: Value::Number("3".parse().unwrap(), false),
value: Expr::value(number("3")),
},
Setting {
key: "s1".into(),
value: Value::SingleQuotedString("s".into()),
value: Expr::value(single_quoted_string("s")),
},
]),
},
@ -1575,11 +1640,11 @@ fn parse_select_table_function_settings() {
settings: Some(vec![
Setting {
key: "s0".into(),
value: Value::Number("3".parse().unwrap(), false),
value: Expr::value(number("3")),
},
Setting {
key: "s1".into(),
value: Value::SingleQuotedString("s".into()),
value: Expr::value(single_quoted_string("s")),
},
]),
},
@ -1589,7 +1654,6 @@ fn parse_select_table_function_settings() {
"SELECT * FROM t(SETTINGS a=)",
"SELECT * FROM t(SETTINGS a=1, b)",
"SELECT * FROM t(SETTINGS a=1, b=)",
"SELECT * FROM t(SETTINGS a=1, b=c)",
];
for sql in invalid_cases {
clickhouse_and_generic()

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()
@ -3561,7 +3563,7 @@ fn test_double_value() {
for (input, expected) in test_cases {
for (i, expr) in input.iter().enumerate() {
if let Statement::Query(query) =
dialects.one_statement_parses_to(&format!("SELECT {}", expr), "")
dialects.one_statement_parses_to(&format!("SELECT {expr}"), "")
{
if let SetExpr::Select(select) = *query.body {
assert_eq!(expected[i], select.projection[0]);
@ -4021,13 +4023,13 @@ fn parse_create_table_column_constraint_characteristics() {
syntax
};
let sql = format!("CREATE TABLE t (a int UNIQUE {})", syntax);
let sql = format!("CREATE TABLE t (a int UNIQUE {syntax})");
let expected_clause = if syntax.is_empty() {
String::new()
} else {
format!(" {syntax}")
};
let expected = format!("CREATE TABLE t (a INT UNIQUE{})", expected_clause);
let expected = format!("CREATE TABLE t (a INT UNIQUE{expected_clause})");
let ast = one_statement_parses_to(&sql, &expected);
let expected_value = if deferrable.is_some() || initially.is_some() || enforced.is_some() {
@ -4954,7 +4956,7 @@ fn parse_alter_table_constraints() {
match alter_table_op(verified_stmt(&format!(
"ALTER TABLE tab ADD {constraint_text}"
))) {
AlterTableOperation::AddConstraint(constraint) => {
AlterTableOperation::AddConstraint { constraint, .. } => {
assert_eq!(constraint_text, constraint.to_string());
}
_ => unreachable!(),
@ -5055,22 +5057,21 @@ fn parse_alter_table_alter_column_type() {
AlterColumnOperation::SetDataType {
data_type: DataType::Text,
using: None,
had_set: true,
}
);
}
_ => unreachable!(),
}
verified_stmt(&format!("{alter_stmt} ALTER COLUMN is_active TYPE TEXT"));
let dialect = TestedDialects::new(vec![Box::new(GenericDialect {})]);
let dialects = all_dialects_where(|d| d.supports_alter_column_type_using());
dialects.verified_stmt(&format!(
"{alter_stmt} ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'"
));
let res =
dialect.parse_sql_statements(&format!("{alter_stmt} ALTER COLUMN is_active TYPE TEXT"));
assert_eq!(
ParserError::ParserError("Expected: SET/DROP NOT NULL, SET DEFAULT, or SET DATA TYPE after ALTER COLUMN, found: TYPE".to_string()),
res.unwrap_err()
);
let res = dialect.parse_sql_statements(&format!(
let dialects = all_dialects_except(|d| d.supports_alter_column_type_using());
let res = dialects.parse_sql_statements(&format!(
"{alter_stmt} ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'"
));
assert_eq!(
@ -5850,7 +5851,10 @@ fn parse_literal_date() {
assert_eq!(
&Expr::TypedString {
data_type: DataType::Date,
value: Value::SingleQuotedString("1999-01-01".into()),
value: ValueWithSpan {
value: Value::SingleQuotedString("1999-01-01".into()),
span: Span::empty(),
}
},
expr_from_projection(only(&select.projection)),
);
@ -5863,7 +5867,10 @@ fn parse_literal_time() {
assert_eq!(
&Expr::TypedString {
data_type: DataType::Time(None, TimezoneInfo::None),
value: Value::SingleQuotedString("01:23:34".into()),
value: ValueWithSpan {
value: Value::SingleQuotedString("01:23:34".into()),
span: Span::empty(),
},
},
expr_from_projection(only(&select.projection)),
);
@ -5876,7 +5883,10 @@ fn parse_literal_datetime() {
assert_eq!(
&Expr::TypedString {
data_type: DataType::Datetime(None),
value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()),
value: ValueWithSpan {
value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()),
span: Span::empty(),
},
},
expr_from_projection(only(&select.projection)),
);
@ -5889,7 +5899,10 @@ fn parse_literal_timestamp_without_time_zone() {
assert_eq!(
&Expr::TypedString {
data_type: DataType::Timestamp(None, TimezoneInfo::None),
value: Value::SingleQuotedString("1999-01-01 01:23:34".into()),
value: ValueWithSpan {
value: Value::SingleQuotedString("1999-01-01 01:23:34".into()),
span: Span::empty(),
},
},
expr_from_projection(only(&select.projection)),
);
@ -5904,7 +5917,10 @@ fn parse_literal_timestamp_with_time_zone() {
assert_eq!(
&Expr::TypedString {
data_type: DataType::Timestamp(None, TimezoneInfo::Tz),
value: Value::SingleQuotedString("1999-01-01 01:23:34Z".into()),
value: ValueWithSpan {
value: Value::SingleQuotedString("1999-01-01 01:23:34Z".into()),
span: Span::empty(),
},
},
expr_from_projection(only(&select.projection)),
);
@ -6476,8 +6492,9 @@ fn parse_json_keyword() {
assert_eq!(
&Expr::TypedString {
data_type: DataType::JSON,
value: Value::SingleQuotedString(
r#"{
value: ValueWithSpan {
value: Value::SingleQuotedString(
r#"{
"id": 10,
"type": "fruit",
"name": "apple",
@ -6497,8 +6514,10 @@ fn parse_json_keyword() {
]
}
}"#
.to_string()
)
.to_string()
),
span: Span::empty(),
}
},
expr_from_projection(only(&select.projection)),
);
@ -6510,7 +6529,10 @@ fn parse_typed_strings() {
assert_eq!(
Expr::TypedString {
data_type: DataType::JSON,
value: Value::SingleQuotedString(r#"{"foo":"bar"}"#.into())
value: ValueWithSpan {
value: Value::SingleQuotedString(r#"{"foo":"bar"}"#.into()),
span: Span::empty(),
}
},
expr
);
@ -6528,7 +6550,10 @@ fn parse_bignumeric_keyword() {
assert_eq!(
&Expr::TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: Value::SingleQuotedString(r#"0"#.into())
value: ValueWithSpan {
value: Value::SingleQuotedString(r#"0"#.into()),
span: Span::empty(),
}
},
expr_from_projection(only(&select.projection)),
);
@ -6539,7 +6564,10 @@ fn parse_bignumeric_keyword() {
assert_eq!(
&Expr::TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: Value::SingleQuotedString(r#"123456"#.into())
value: ValueWithSpan {
value: Value::SingleQuotedString(r#"123456"#.into()),
span: Span::empty(),
}
},
expr_from_projection(only(&select.projection)),
);
@ -6550,7 +6578,10 @@ fn parse_bignumeric_keyword() {
assert_eq!(
&Expr::TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: Value::SingleQuotedString(r#"-3.14"#.into())
value: ValueWithSpan {
value: Value::SingleQuotedString(r#"-3.14"#.into()),
span: Span::empty(),
}
},
expr_from_projection(only(&select.projection)),
);
@ -6561,7 +6592,10 @@ fn parse_bignumeric_keyword() {
assert_eq!(
&Expr::TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: Value::SingleQuotedString(r#"-0.54321"#.into())
value: ValueWithSpan {
value: Value::SingleQuotedString(r#"-0.54321"#.into()),
span: Span::empty(),
}
},
expr_from_projection(only(&select.projection)),
);
@ -6572,7 +6606,10 @@ fn parse_bignumeric_keyword() {
assert_eq!(
&Expr::TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: Value::SingleQuotedString(r#"1.23456e05"#.into())
value: ValueWithSpan {
value: Value::SingleQuotedString(r#"1.23456e05"#.into()),
span: Span::empty(),
}
},
expr_from_projection(only(&select.projection)),
);
@ -6583,7 +6620,10 @@ fn parse_bignumeric_keyword() {
assert_eq!(
&Expr::TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: Value::SingleQuotedString(r#"-9.876e-3"#.into())
value: ValueWithSpan {
value: Value::SingleQuotedString(r#"-9.876e-3"#.into()),
span: Span::empty(),
}
},
expr_from_projection(only(&select.projection)),
);
@ -7497,7 +7537,7 @@ fn parse_cte_in_data_modification_statements() {
assert_eq!(query.with.unwrap().to_string(), "WITH x AS (SELECT 1)");
assert!(matches!(*query.body, SetExpr::Update(_)));
}
other => panic!("Expected: UPDATE, got: {:?}", other),
other => panic!("Expected: UPDATE, got: {other:?}"),
}
match verified_stmt("WITH t (x) AS (SELECT 9) DELETE FROM q WHERE id IN (SELECT x FROM t)") {
@ -7505,7 +7545,7 @@ fn parse_cte_in_data_modification_statements() {
assert_eq!(query.with.unwrap().to_string(), "WITH t (x) AS (SELECT 9)");
assert!(matches!(*query.body, SetExpr::Delete(_)));
}
other => panic!("Expected: DELETE, got: {:?}", other),
other => panic!("Expected: DELETE, got: {other:?}"),
}
match verified_stmt("WITH x AS (SELECT 42) INSERT INTO t SELECT foo FROM x") {
@ -7513,7 +7553,7 @@ fn parse_cte_in_data_modification_statements() {
assert_eq!(query.with.unwrap().to_string(), "WITH x AS (SELECT 42)");
assert!(matches!(*query.body, SetExpr::Insert(_)));
}
other => panic!("Expected: INSERT, got: {:?}", other),
other => panic!("Expected: INSERT, got: {other:?}"),
}
}
@ -7760,7 +7800,6 @@ fn parse_trim() {
Box::new(MySqlDialect {}),
//Box::new(BigQueryDialect {}),
Box::new(SQLiteDialect {}),
Box::new(DuckDbDialect {}),
]);
assert_eq!(
@ -7988,7 +8027,7 @@ fn parse_create_view_with_columns() {
.map(|name| ViewColumnDef {
name,
data_type: None,
options: None
options: None,
})
.collect::<Vec<_>>()
);
@ -8592,8 +8631,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")
{
@ -9381,9 +9423,11 @@ fn parse_grant() {
verified_stmt("GRANT SELECT ON VIEW view1 TO ROLE role1");
verified_stmt("GRANT EXEC ON my_sp TO runner");
verified_stmt("GRANT UPDATE ON my_table TO updater_role AS dbo");
all_dialects_where(|d| d.identifier_quote_style("none") == Some('['))
.verified_stmt("GRANT SELECT ON [my_table] TO [public]");
verified_stmt("GRANT SELECT ON FUTURE SCHEMAS IN DATABASE db1 TO ROLE role1");
verified_stmt("GRANT SELECT ON FUTURE TABLES IN SCHEMA db1.sc1 TO ROLE role1");
verified_stmt("GRANT SELECT ON FUTURE VIEWS IN SCHEMA db1.sc1 TO ROLE role1");
}
#[test]
@ -10038,7 +10082,7 @@ fn parse_offset_and_limit() {
#[test]
fn parse_time_functions() {
fn test_time_function(func_name: &'static str) {
let sql = format!("SELECT {}()", func_name);
let sql = format!("SELECT {func_name}()");
let select = verified_only_select(&sql);
let select_localtime_func_call_ast = Function {
name: ObjectName::from(vec![Ident::new(func_name)]),
@ -10060,7 +10104,7 @@ fn parse_time_functions() {
);
// Validating Parenthesis
let sql_without_parens = format!("SELECT {}", func_name);
let sql_without_parens = format!("SELECT {func_name}");
let mut ast_without_parens = select_localtime_func_call_ast;
ast_without_parens.args = FunctionArguments::None;
assert_eq!(
@ -11061,10 +11105,17 @@ fn parse_non_latin_identifiers() {
Box::new(RedshiftSqlDialect {}),
Box::new(MySqlDialect {}),
]);
supported_dialects.verified_stmt("SELECT a.説明 FROM test.public.inter01 AS a");
supported_dialects.verified_stmt("SELECT a.説明 FROM inter01 AS a, inter01_transactions AS b WHERE a.説明 = b.取引 GROUP BY a.説明");
supported_dialects.verified_stmt("SELECT 説明, hühnervögel, garçon, Москва, 東京 FROM inter01");
let supported_dialects = TestedDialects::new(vec![
Box::new(GenericDialect {}),
Box::new(DuckDbDialect {}),
Box::new(PostgreSqlDialect {}),
Box::new(MsSqlDialect {}),
Box::new(MySqlDialect {}),
]);
assert!(supported_dialects
.parse_sql_statements("SELECT 💝 FROM table1")
.is_err());
@ -14301,7 +14352,7 @@ fn overflow() {
let expr = std::iter::repeat_n("1", 1000)
.collect::<Vec<_>>()
.join(" + ");
let sql = format!("SELECT {}", expr);
let sql = format!("SELECT {expr}");
let mut statements = Parser::parse_sql(&GenericDialect {}, sql.as_str()).unwrap();
let statement = statements.pop().unwrap();
@ -14601,7 +14652,7 @@ fn test_conditional_statement_span() {
else_block.unwrap().span()
);
}
stmt => panic!("Unexpected statement: {:?}", stmt),
stmt => panic!("Unexpected statement: {stmt:?}"),
}
}
@ -14821,7 +14872,10 @@ fn test_geometry_type() {
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::Point),
value: Value::SingleQuotedString("1,2".to_string()),
value: ValueWithSpan {
value: Value::SingleQuotedString("1,2".to_string()),
span: Span::empty(),
},
}
);
@ -14830,7 +14884,10 @@ fn test_geometry_type() {
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::Line),
value: Value::SingleQuotedString("1,2,3,4".to_string()),
value: ValueWithSpan {
value: Value::SingleQuotedString("1,2,3,4".to_string()),
span: Span::empty(),
},
}
);
@ -14839,7 +14896,10 @@ fn test_geometry_type() {
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::GeometricPath),
value: Value::SingleQuotedString("1,2,3,4".to_string()),
value: ValueWithSpan {
value: Value::SingleQuotedString("1,2,3,4".to_string()),
span: Span::empty(),
},
}
);
let sql = "box '1,2,3,4'";
@ -14847,7 +14907,10 @@ fn test_geometry_type() {
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::GeometricBox),
value: Value::SingleQuotedString("1,2,3,4".to_string()),
value: ValueWithSpan {
value: Value::SingleQuotedString("1,2,3,4".to_string()),
span: Span::empty(),
},
}
);
@ -14856,7 +14919,10 @@ fn test_geometry_type() {
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::Circle),
value: Value::SingleQuotedString("1,2,3".to_string()),
value: ValueWithSpan {
value: Value::SingleQuotedString("1,2,3".to_string()),
span: Span::empty(),
},
}
);
@ -14865,7 +14931,10 @@ fn test_geometry_type() {
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::Polygon),
value: Value::SingleQuotedString("1,2,3,4".to_string()),
value: ValueWithSpan {
value: Value::SingleQuotedString("1,2,3,4".to_string()),
span: Span::empty(),
},
}
);
let sql = "lseg '1,2,3,4'";
@ -14873,7 +14942,10 @@ fn test_geometry_type() {
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::LineSegment),
value: Value::SingleQuotedString("1,2,3,4".to_string()),
value: ValueWithSpan {
value: Value::SingleQuotedString("1,2,3,4".to_string()),
span: Span::empty(),
},
}
);
}
@ -15210,10 +15282,426 @@ fn parse_pipeline_operator() {
dialects.verified_stmt("SELECT * FROM tbl |> TABLESAMPLE SYSTEM (50 PERCENT)");
dialects.verified_stmt("SELECT * FROM tbl |> TABLESAMPLE SYSTEM (50) REPEATABLE (10)");
// rename pipe operator
dialects.verified_stmt("SELECT * FROM users |> RENAME old_name AS new_name");
dialects.verified_stmt("SELECT * FROM users |> RENAME id AS user_id, name AS user_name");
dialects.verified_query_with_canonical(
"SELECT * FROM users |> RENAME id user_id",
"SELECT * FROM users |> RENAME id AS user_id",
);
// union pipe operator
dialects.verified_stmt("SELECT * FROM users |> UNION ALL (SELECT * FROM admins)");
dialects.verified_stmt("SELECT * FROM users |> UNION DISTINCT (SELECT * FROM admins)");
dialects.verified_stmt("SELECT * FROM users |> UNION (SELECT * FROM admins)");
// union pipe operator with multiple queries
dialects.verified_stmt(
"SELECT * FROM users |> UNION ALL (SELECT * FROM admins), (SELECT * FROM guests)",
);
dialects.verified_stmt("SELECT * FROM users |> UNION DISTINCT (SELECT * FROM admins), (SELECT * FROM guests), (SELECT * FROM employees)");
dialects.verified_stmt(
"SELECT * FROM users |> UNION (SELECT * FROM admins), (SELECT * FROM guests)",
);
// union pipe operator with BY NAME modifier
dialects.verified_stmt("SELECT * FROM users |> UNION BY NAME (SELECT * FROM admins)");
dialects.verified_stmt("SELECT * FROM users |> UNION ALL BY NAME (SELECT * FROM admins)");
dialects.verified_stmt("SELECT * FROM users |> UNION DISTINCT BY NAME (SELECT * FROM admins)");
// union pipe operator with BY NAME and multiple queries
dialects.verified_stmt(
"SELECT * FROM users |> UNION BY NAME (SELECT * FROM admins), (SELECT * FROM guests)",
);
// intersect pipe operator (BigQuery requires DISTINCT modifier for INTERSECT)
dialects.verified_stmt("SELECT * FROM users |> INTERSECT DISTINCT (SELECT * FROM admins)");
// intersect pipe operator with BY NAME modifier
dialects
.verified_stmt("SELECT * FROM users |> INTERSECT DISTINCT BY NAME (SELECT * FROM admins)");
// intersect pipe operator with multiple queries
dialects.verified_stmt(
"SELECT * FROM users |> INTERSECT DISTINCT (SELECT * FROM admins), (SELECT * FROM guests)",
);
// intersect pipe operator with BY NAME and multiple queries
dialects.verified_stmt("SELECT * FROM users |> INTERSECT DISTINCT BY NAME (SELECT * FROM admins), (SELECT * FROM guests)");
// except pipe operator (BigQuery requires DISTINCT modifier for EXCEPT)
dialects.verified_stmt("SELECT * FROM users |> EXCEPT DISTINCT (SELECT * FROM admins)");
// except pipe operator with BY NAME modifier
dialects.verified_stmt("SELECT * FROM users |> EXCEPT DISTINCT BY NAME (SELECT * FROM admins)");
// except pipe operator with multiple queries
dialects.verified_stmt(
"SELECT * FROM users |> EXCEPT DISTINCT (SELECT * FROM admins), (SELECT * FROM guests)",
);
// except pipe operator with BY NAME and multiple queries
dialects.verified_stmt("SELECT * FROM users |> EXCEPT DISTINCT BY NAME (SELECT * FROM admins), (SELECT * FROM guests)");
// call pipe operator
dialects.verified_stmt("SELECT * FROM users |> CALL my_function()");
dialects.verified_stmt("SELECT * FROM users |> CALL process_data(5, 'test')");
dialects.verified_stmt(
"SELECT * FROM users |> CALL namespace.function_name(col1, col2, 'literal')",
);
// call pipe operator with complex arguments
dialects.verified_stmt("SELECT * FROM users |> CALL transform_data(col1 + col2)");
dialects.verified_stmt("SELECT * FROM users |> CALL analyze_data('param1', 100, true)");
// call pipe operator with aliases
dialects.verified_stmt("SELECT * FROM input_table |> CALL tvf1(arg1) AS al");
dialects.verified_stmt("SELECT * FROM users |> CALL process_data(5) AS result_table");
dialects.verified_stmt("SELECT * FROM users |> CALL namespace.func() AS my_alias");
// multiple call pipe operators in sequence
dialects.verified_stmt("SELECT * FROM input_table |> CALL tvf1(arg1) |> CALL tvf2(arg2, arg3)");
dialects.verified_stmt(
"SELECT * FROM data |> CALL transform(col1) |> CALL validate() |> CALL process(param)",
);
// multiple call pipe operators with aliases
dialects.verified_stmt(
"SELECT * FROM input_table |> CALL tvf1(arg1) AS step1 |> CALL tvf2(arg2) AS step2",
);
dialects.verified_stmt(
"SELECT * FROM data |> CALL preprocess() AS clean_data |> CALL analyze(mode) AS results",
);
// call pipe operators mixed with other pipe operators
dialects.verified_stmt(
"SELECT * FROM users |> CALL transform() |> WHERE status = 'active' |> CALL process(param)",
);
dialects.verified_stmt(
"SELECT * FROM data |> CALL preprocess() AS clean |> SELECT col1, col2 |> CALL validate()",
);
// pivot pipe operator
dialects.verified_stmt(
"SELECT * FROM monthly_sales |> PIVOT(SUM(amount) FOR quarter IN ('Q1', 'Q2', 'Q3', 'Q4'))",
);
dialects.verified_stmt("SELECT * FROM sales_data |> PIVOT(AVG(revenue) FOR region IN ('North', 'South', 'East', 'West'))");
// pivot pipe operator with multiple aggregate functions
dialects.verified_stmt("SELECT * FROM data |> PIVOT(SUM(sales) AS total_sales, COUNT(*) AS num_transactions FOR month IN ('Jan', 'Feb', 'Mar'))");
// pivot pipe operator with compound column names
dialects.verified_stmt("SELECT * FROM sales |> PIVOT(SUM(amount) FOR product.category IN ('Electronics', 'Clothing'))");
// pivot pipe operator mixed with other pipe operators
dialects.verified_stmt("SELECT * FROM sales_data |> WHERE year = 2023 |> PIVOT(SUM(revenue) FOR quarter IN ('Q1', 'Q2', 'Q3', 'Q4'))");
// pivot pipe operator with aliases
dialects.verified_stmt("SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) AS quarterly_sales");
dialects.verified_stmt("SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) AS avg_by_category");
dialects.verified_stmt("SELECT * FROM sales |> PIVOT(COUNT(*) AS transactions, SUM(amount) AS total FOR region IN ('North', 'South')) AS regional_summary");
// pivot pipe operator with implicit aliases (without AS keyword)
dialects.verified_query_with_canonical(
"SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) quarterly_sales",
"SELECT * FROM monthly_sales |> PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2')) AS quarterly_sales",
);
dialects.verified_query_with_canonical(
"SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) avg_by_category",
"SELECT * FROM data |> PIVOT(AVG(price) FOR category IN ('A', 'B', 'C')) AS avg_by_category",
);
// unpivot pipe operator basic usage
dialects
.verified_stmt("SELECT * FROM sales |> UNPIVOT(revenue FOR quarter IN (Q1, Q2, Q3, Q4))");
dialects.verified_stmt("SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C))");
dialects.verified_stmt(
"SELECT * FROM metrics |> UNPIVOT(measurement FOR metric_type IN (cpu, memory, disk))",
);
// unpivot pipe operator with multiple columns
dialects.verified_stmt("SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (jan, feb, mar, apr, may, jun))");
dialects.verified_stmt(
"SELECT * FROM report |> UNPIVOT(score FOR subject IN (math, science, english, history))",
);
// unpivot pipe operator mixed with other pipe operators
dialects.verified_stmt("SELECT * FROM sales_data |> WHERE year = 2023 |> UNPIVOT(revenue FOR quarter IN (Q1, Q2, Q3, Q4))");
// unpivot pipe operator with aliases
dialects.verified_stmt("SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) AS unpivoted_sales");
dialects.verified_stmt(
"SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) AS transformed_data",
);
dialects.verified_stmt("SELECT * FROM metrics |> UNPIVOT(measurement FOR metric_type IN (cpu, memory)) AS metric_measurements");
// unpivot pipe operator with implicit aliases (without AS keyword)
dialects.verified_query_with_canonical(
"SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) unpivoted_sales",
"SELECT * FROM quarterly_sales |> UNPIVOT(amount FOR period IN (Q1, Q2)) AS unpivoted_sales",
);
dialects.verified_query_with_canonical(
"SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) transformed_data",
"SELECT * FROM data |> UNPIVOT(value FOR category IN (A, B, C)) AS transformed_data",
);
// many pipes
dialects.verified_stmt(
"SELECT * FROM CustomerOrders |> AGGREGATE SUM(cost) AS total_cost GROUP BY customer_id, state, item_type |> EXTEND COUNT(*) OVER (PARTITION BY customer_id) AS num_orders |> WHERE num_orders > 1 |> AGGREGATE AVG(total_cost) AS average GROUP BY state DESC, item_type ASC",
);
// join pipe operator - INNER JOIN
dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id");
dialects.verified_stmt("SELECT * FROM users |> INNER JOIN orders ON users.id = orders.user_id");
// join pipe operator - LEFT JOIN
dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders ON users.id = orders.user_id");
dialects.verified_stmt(
"SELECT * FROM users |> LEFT OUTER JOIN orders ON users.id = orders.user_id",
);
// join pipe operator - RIGHT JOIN
dialects.verified_stmt("SELECT * FROM users |> RIGHT JOIN orders ON users.id = orders.user_id");
dialects.verified_stmt(
"SELECT * FROM users |> RIGHT OUTER JOIN orders ON users.id = orders.user_id",
);
// join pipe operator - FULL JOIN
dialects.verified_stmt("SELECT * FROM users |> FULL JOIN orders ON users.id = orders.user_id");
dialects.verified_query_with_canonical(
"SELECT * FROM users |> FULL OUTER JOIN orders ON users.id = orders.user_id",
"SELECT * FROM users |> FULL JOIN orders ON users.id = orders.user_id",
);
// join pipe operator - CROSS JOIN
dialects.verified_stmt("SELECT * FROM users |> CROSS JOIN orders");
// join pipe operator with USING
dialects.verified_query_with_canonical(
"SELECT * FROM users |> JOIN orders USING (user_id)",
"SELECT * FROM users |> JOIN orders USING(user_id)",
);
dialects.verified_query_with_canonical(
"SELECT * FROM users |> LEFT JOIN orders USING (user_id, order_date)",
"SELECT * FROM users |> LEFT JOIN orders USING(user_id, order_date)",
);
// join pipe operator with alias
dialects.verified_query_with_canonical(
"SELECT * FROM users |> JOIN orders o ON users.id = o.user_id",
"SELECT * FROM users |> JOIN orders AS o ON users.id = o.user_id",
);
dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders AS o ON users.id = o.user_id");
// join pipe operator with complex ON condition
dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id AND orders.status = 'active'");
dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders ON users.id = orders.user_id AND orders.amount > 100");
// multiple join pipe operators
dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id |> JOIN products ON orders.product_id = products.id");
dialects.verified_stmt("SELECT * FROM users |> LEFT JOIN orders ON users.id = orders.user_id |> RIGHT JOIN products ON orders.product_id = products.id");
// join pipe operator with other pipe operators
dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id |> WHERE orders.amount > 100");
dialects.verified_stmt("SELECT * FROM users |> WHERE users.active = true |> LEFT JOIN orders ON users.id = orders.user_id");
dialects.verified_stmt("SELECT * FROM users |> JOIN orders ON users.id = orders.user_id |> SELECT users.name, orders.amount");
}
#[test]
fn parse_pipeline_operator_negative_tests() {
let dialects = all_dialects_where(|d| d.supports_pipe_operator());
// Test that plain EXCEPT without DISTINCT fails
assert_eq!(
ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements("SELECT * FROM users |> EXCEPT (SELECT * FROM admins)")
.unwrap_err()
);
// Test that EXCEPT ALL fails
assert_eq!(
ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements("SELECT * FROM users |> EXCEPT ALL (SELECT * FROM admins)")
.unwrap_err()
);
// Test that EXCEPT BY NAME without DISTINCT fails
assert_eq!(
ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements("SELECT * FROM users |> EXCEPT BY NAME (SELECT * FROM admins)")
.unwrap_err()
);
// Test that EXCEPT ALL BY NAME fails
assert_eq!(
ParserError::ParserError("EXCEPT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements(
"SELECT * FROM users |> EXCEPT ALL BY NAME (SELECT * FROM admins)"
)
.unwrap_err()
);
// Test that plain INTERSECT without DISTINCT fails
assert_eq!(
ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements("SELECT * FROM users |> INTERSECT (SELECT * FROM admins)")
.unwrap_err()
);
// Test that INTERSECT ALL fails
assert_eq!(
ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements("SELECT * FROM users |> INTERSECT ALL (SELECT * FROM admins)")
.unwrap_err()
);
// Test that INTERSECT BY NAME without DISTINCT fails
assert_eq!(
ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements("SELECT * FROM users |> INTERSECT BY NAME (SELECT * FROM admins)")
.unwrap_err()
);
// Test that INTERSECT ALL BY NAME fails
assert_eq!(
ParserError::ParserError("INTERSECT pipe operator requires DISTINCT modifier".to_string()),
dialects
.parse_sql_statements(
"SELECT * FROM users |> INTERSECT ALL BY NAME (SELECT * FROM admins)"
)
.unwrap_err()
);
// Test that CALL without function name fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> CALL")
.is_err());
// Test that CALL without parentheses fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> CALL my_function")
.is_err());
// Test that CALL with invalid function syntax fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> CALL 123invalid")
.is_err());
// Test that CALL with malformed arguments fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> CALL my_function(,)")
.is_err());
// Test that CALL with invalid alias syntax fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> CALL my_function() AS")
.is_err());
// Test that PIVOT without parentheses fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> PIVOT SUM(amount) FOR month IN ('Jan')")
.is_err());
// Test that PIVOT without FOR keyword fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) month IN ('Jan'))")
.is_err());
// Test that PIVOT without IN keyword fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month ('Jan'))")
.is_err());
// Test that PIVOT with empty IN list fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month IN ())")
.is_err());
// Test that PIVOT with invalid alias syntax fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> PIVOT(SUM(amount) FOR month IN ('Jan')) AS")
.is_err());
// Test UNPIVOT negative cases
// Test that UNPIVOT without parentheses fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT value FOR name IN col1, col2")
.is_err());
// Test that UNPIVOT without FOR keyword fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value name IN (col1, col2))")
.is_err());
// Test that UNPIVOT without IN keyword fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name (col1, col2))")
.is_err());
// Test that UNPIVOT with missing value column fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT(FOR name IN (col1, col2))")
.is_err());
// Test that UNPIVOT with missing name column fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR IN (col1, col2))")
.is_err());
// Test that UNPIVOT with empty IN list fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN ())")
.is_err());
// Test that UNPIVOT with invalid alias syntax fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN (col1, col2)) AS")
.is_err());
// Test that UNPIVOT with missing closing parenthesis fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> UNPIVOT(value FOR name IN (col1, col2)")
.is_err());
// Test that JOIN without table name fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> JOIN ON users.id = orders.user_id")
.is_err());
// Test that CROSS JOIN with ON condition fails
assert!(dialects
.parse_sql_statements(
"SELECT * FROM users |> CROSS JOIN orders ON users.id = orders.user_id"
)
.is_err());
// Test that CROSS JOIN with USING condition fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> CROSS JOIN orders USING (user_id)")
.is_err());
// Test that JOIN with empty USING list fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> JOIN orders USING ()")
.is_err());
// Test that JOIN with malformed ON condition fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> JOIN orders ON")
.is_err());
// Test that JOIN with invalid USING syntax fails
assert!(dialects
.parse_sql_statements("SELECT * FROM users |> JOIN orders USING user_id")
.is_err());
}
#[test]
@ -15300,6 +15788,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 +15839,118 @@ 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 join_precedence() {
all_dialects_except(|d| !d.supports_left_associative_joins_without_parens())
.verified_query_with_canonical(
"SELECT *
FROM t1
NATURAL JOIN t5
INNER JOIN t0 ON (t0.v1 + t5.v0) > 0
WHERE t0.v1 = t1.v0",
// canonical string without parentheses
"SELECT * FROM t1 NATURAL JOIN t5 INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 WHERE t0.v1 = t1.v0",
);
all_dialects_except(|d| d.supports_left_associative_joins_without_parens()).verified_query_with_canonical(
"SELECT *
FROM t1
NATURAL JOIN t5
INNER JOIN t0 ON (t0.v1 + t5.v0) > 0
WHERE t0.v1 = t1.v0",
// canonical string with parentheses
"SELECT * FROM t1 NATURAL JOIN (t5 INNER JOIN t0 ON (t0.v1 + t5.v0) > 0) WHERE t0.v1 = t1.v0",
);
}
#[test]
fn parse_create_procedure_with_language() {
let sql = r#"CREATE PROCEDURE test_proc LANGUAGE sql AS BEGIN SELECT 1; END"#;
match verified_stmt(sql) {
Statement::CreateProcedure {
or_alter,
name,
params,
language,
..
} => {
assert_eq!(or_alter, false);
assert_eq!(name.to_string(), "test_proc");
assert_eq!(params, Some(vec![]));
assert_eq!(
language,
Some(Ident {
value: "sql".into(),
quote_style: None,
span: Span {
start: Location::empty(),
end: Location::empty()
}
})
);
}
_ => unreachable!(),
}
}
#[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

@ -19,6 +19,7 @@ use sqlparser::ast::helpers::attached_token::AttachedToken;
use sqlparser::ast::*;
use sqlparser::dialect::{DatabricksDialect, GenericDialect};
use sqlparser::parser::ParserError;
use sqlparser::tokenizer::Span;
use test_utils::*;
#[macro_use]
@ -213,7 +214,7 @@ fn parse_use() {
for object_name in &valid_object_names {
// Test single identifier without quotes
assert_eq!(
databricks().verified_stmt(&format!("USE {}", object_name)),
databricks().verified_stmt(&format!("USE {object_name}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::new(
object_name.to_string()
)])))
@ -221,7 +222,7 @@ fn parse_use() {
for &quote in &quote_styles {
// Test single identifier with different type of quotes
assert_eq!(
databricks().verified_stmt(&format!("USE {0}{1}{0}", quote, object_name)),
databricks().verified_stmt(&format!("USE {quote}{object_name}{quote}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote(
quote,
object_name.to_string(),
@ -233,21 +234,21 @@ fn parse_use() {
for &quote in &quote_styles {
// Test single identifier with keyword and different type of quotes
assert_eq!(
databricks().verified_stmt(&format!("USE CATALOG {0}my_catalog{0}", quote)),
databricks().verified_stmt(&format!("USE CATALOG {quote}my_catalog{quote}")),
Statement::Use(Use::Catalog(ObjectName::from(vec![Ident::with_quote(
quote,
"my_catalog".to_string(),
)])))
);
assert_eq!(
databricks().verified_stmt(&format!("USE DATABASE {0}my_database{0}", quote)),
databricks().verified_stmt(&format!("USE DATABASE {quote}my_database{quote}")),
Statement::Use(Use::Database(ObjectName::from(vec![Ident::with_quote(
quote,
"my_database".to_string(),
)])))
);
assert_eq!(
databricks().verified_stmt(&format!("USE SCHEMA {0}my_schema{0}", quote)),
databricks().verified_stmt(&format!("USE SCHEMA {quote}my_schema{quote}")),
Statement::Use(Use::Schema(ObjectName::from(vec![Ident::with_quote(
quote,
"my_schema".to_string(),
@ -328,7 +329,10 @@ fn data_type_timestamp_ntz() {
databricks().verified_expr("TIMESTAMP_NTZ '2025-03-29T18:52:00'"),
Expr::TypedString {
data_type: DataType::TimestampNtz,
value: Value::SingleQuotedString("2025-03-29T18:52:00".to_owned())
value: ValueWithSpan {
value: Value::SingleQuotedString("2025-03-29T18:52:00".to_owned()),
span: Span::empty(),
}
}
);
@ -357,6 +361,6 @@ fn data_type_timestamp_ntz() {
}]
);
}
s => panic!("Unexpected statement: {:?}", s),
s => panic!("Unexpected statement: {s:?}"),
}
}

View file

@ -24,6 +24,7 @@ use test_utils::*;
use sqlparser::ast::*;
use sqlparser::dialect::{DuckDbDialect, GenericDialect};
use sqlparser::parser::ParserError;
fn duckdb() -> TestedDialects {
TestedDialects::new(vec![Box::new(DuckDbDialect {})])
@ -368,7 +369,7 @@ fn test_duckdb_specific_int_types() {
("HUGEINT", DataType::HugeInt),
];
for (dtype_string, data_type) in duckdb_dtypes {
let sql = format!("SELECT 123::{}", dtype_string);
let sql = format!("SELECT 123::{dtype_string}");
let select = duckdb().verified_only_select(&sql);
assert_eq!(
&Expr::Cast {
@ -792,7 +793,7 @@ fn parse_use() {
for object_name in &valid_object_names {
// Test single identifier without quotes
assert_eq!(
duckdb().verified_stmt(&format!("USE {}", object_name)),
duckdb().verified_stmt(&format!("USE {object_name}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::new(
object_name.to_string()
)])))
@ -800,7 +801,7 @@ fn parse_use() {
for &quote in &quote_styles {
// Test single identifier with different type of quotes
assert_eq!(
duckdb().verified_stmt(&format!("USE {0}{1}{0}", quote, object_name)),
duckdb().verified_stmt(&format!("USE {quote}{object_name}{quote}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote(
quote,
object_name.to_string(),
@ -812,7 +813,9 @@ fn parse_use() {
for &quote in &quote_styles {
// Test double identifier with different type of quotes
assert_eq!(
duckdb().verified_stmt(&format!("USE {0}CATALOG{0}.{0}my_schema{0}", quote)),
duckdb().verified_stmt(&format!(
"USE {quote}CATALOG{quote}.{quote}my_schema{quote}"
)),
Statement::Use(Use::Object(ObjectName::from(vec![
Ident::with_quote(quote, "CATALOG"),
Ident::with_quote(quote, "my_schema")
@ -828,3 +831,32 @@ fn parse_use() {
])))
);
}
#[test]
fn test_duckdb_trim() {
let real_sql = r#"SELECT customer_id, TRIM(item_price_id, '"', "a") AS item_price_id FROM models_staging.subscriptions"#;
assert_eq!(duckdb().verified_stmt(real_sql).to_string(), real_sql);
let sql_only_select = "SELECT TRIM('xyz', 'a')";
let select = duckdb().verified_only_select(sql_only_select);
assert_eq!(
&Expr::Trim {
expr: Box::new(Expr::Value(
Value::SingleQuotedString("xyz".to_owned()).with_empty_span()
)),
trim_where: None,
trim_what: None,
trim_characters: Some(vec![Expr::Value(
Value::SingleQuotedString("a".to_owned()).with_empty_span()
)]),
},
expr_from_projection(only(&select.projection))
);
// missing comma separation
let error_sql = "SELECT TRIM('xyz' 'a')";
assert_eq!(
ParserError::ParserError("Expected: ), found: 'a'".to_owned()),
duckdb().parse_sql_statements(error_sql).unwrap_err()
);
}

View file

@ -524,7 +524,7 @@ fn parse_use() {
for object_name in &valid_object_names {
// Test single identifier without quotes
assert_eq!(
hive().verified_stmt(&format!("USE {}", object_name)),
hive().verified_stmt(&format!("USE {object_name}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::new(
object_name.to_string()
)])))
@ -532,7 +532,7 @@ fn parse_use() {
for &quote in &quote_styles {
// Test single identifier with different type of quotes
assert_eq!(
hive().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)),
hive().verified_stmt(&format!("USE {quote}{object_name}{quote}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote(
quote,
object_name.to_string(),

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,14 +165,16 @@ fn parse_create_procedure() {
data_type: DataType::Varchar(Some(CharacterLength::IntegerLength {
length: 256,
unit: None
}))
})),
mode: None,
}
]),
name: ObjectName::from(vec![Ident {
value: "test".into(),
quote_style: None,
span: Span::empty(),
}])
}]),
language: None,
}
)
}
@ -1670,7 +1673,7 @@ fn parse_use() {
for object_name in &valid_object_names {
// Test single identifier without quotes
assert_eq!(
ms().verified_stmt(&format!("USE {}", object_name)),
ms().verified_stmt(&format!("USE {object_name}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::new(
object_name.to_string()
)])))
@ -1678,7 +1681,7 @@ fn parse_use() {
for &quote in &quote_styles {
// Test single identifier with different type of quotes
assert_eq!(
ms().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)),
ms().verified_stmt(&format!("USE {quote}{object_name}{quote}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote(
quote,
object_name.to_string(),
@ -2184,7 +2187,7 @@ fn parse_mssql_if_else() {
"IF 1 = 1 BEGIN SET @A = 1; END ELSE SET @A = 2;"
);
}
_ => panic!("Unexpected statements: {:?}", stmts),
_ => panic!("Unexpected statements: {stmts:?}"),
}
}
@ -2234,7 +2237,7 @@ fn test_mssql_if_statements_span() {
Span::new(Location::new(1, 21), Location::new(1, 36))
);
}
stmt => panic!("Unexpected statement: {:?}", stmt),
stmt => panic!("Unexpected statement: {stmt:?}"),
}
// Blocks
@ -2255,7 +2258,7 @@ fn test_mssql_if_statements_span() {
Span::new(Location::new(1, 32), Location::new(1, 57))
);
}
stmt => panic!("Unexpected statement: {:?}", stmt),
stmt => panic!("Unexpected statement: {stmt:?}"),
}
}

View file

@ -593,7 +593,7 @@ fn parse_use() {
for object_name in &valid_object_names {
// Test single identifier without quotes
assert_eq!(
mysql_and_generic().verified_stmt(&format!("USE {}", object_name)),
mysql_and_generic().verified_stmt(&format!("USE {object_name}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::new(
object_name.to_string()
)])))
@ -601,8 +601,7 @@ fn parse_use() {
for &quote in &quote_styles {
// Test single identifier with different type of quotes
assert_eq!(
mysql_and_generic()
.verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)),
mysql_and_generic().verified_stmt(&format!("USE {quote}{object_name}{quote}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote(
quote,
object_name.to_string(),
@ -670,6 +669,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 +808,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| {
@ -2188,11 +2262,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() {
Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => {
assert_eq!(&[Ident::new("t"), Ident::new("15to29")], &parts[..]);
}
proj => panic!("Unexpected projection: {:?}", proj),
proj => panic!("Unexpected projection: {proj:?}"),
},
body => panic!("Unexpected statement body: {:?}", body),
body => panic!("Unexpected statement body: {body:?}"),
},
stmt => panic!("Unexpected statement: {:?}", stmt),
stmt => panic!("Unexpected statement: {stmt:?}"),
}
// Case 2: Qualified column name that starts with digits and on its own represents a number.
@ -2202,11 +2276,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() {
Some(SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts))) => {
assert_eq!(&[Ident::new("t"), Ident::new("15e29")], &parts[..]);
}
proj => panic!("Unexpected projection: {:?}", proj),
proj => panic!("Unexpected projection: {proj:?}"),
},
body => panic!("Unexpected statement body: {:?}", body),
body => panic!("Unexpected statement body: {body:?}"),
},
stmt => panic!("Unexpected statement: {:?}", stmt),
stmt => panic!("Unexpected statement: {stmt:?}"),
}
// Case 3: Unqualified, the same token is parsed as a number.
@ -2220,11 +2294,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() {
Some(SelectItem::UnnamedExpr(Expr::Value(ValueWithSpan { value, .. }))) => {
assert_eq!(&number("15e29"), value);
}
proj => panic!("Unexpected projection: {:?}", proj),
proj => panic!("Unexpected projection: {proj:?}"),
},
body => panic!("Unexpected statement body: {:?}", body),
body => panic!("Unexpected statement body: {body:?}"),
},
stmt => panic!("Unexpected statement: {:?}", stmt),
stmt => panic!("Unexpected statement: {stmt:?}"),
}
// Case 4: Quoted simple identifier.
@ -2234,11 +2308,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() {
Some(SelectItem::UnnamedExpr(Expr::Identifier(name))) => {
assert_eq!(&Ident::with_quote('`', "15e29"), name);
}
proj => panic!("Unexpected projection: {:?}", proj),
proj => panic!("Unexpected projection: {proj:?}"),
},
body => panic!("Unexpected statement body: {:?}", body),
body => panic!("Unexpected statement body: {body:?}"),
},
stmt => panic!("Unexpected statement: {:?}", stmt),
stmt => panic!("Unexpected statement: {stmt:?}"),
}
// Case 5: Quoted compound identifier.
@ -2251,11 +2325,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() {
&parts[..]
);
}
proj => panic!("Unexpected projection: {:?}", proj),
proj => panic!("Unexpected projection: {proj:?}"),
},
body => panic!("Unexpected statement body: {:?}", body),
body => panic!("Unexpected statement body: {body:?}"),
},
stmt => panic!("Unexpected statement: {:?}", stmt),
stmt => panic!("Unexpected statement: {stmt:?}"),
}
// Case 6: Multi-level compound identifiers.
@ -2272,11 +2346,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() {
&parts[..]
);
}
proj => panic!("Unexpected projection: {:?}", proj),
proj => panic!("Unexpected projection: {proj:?}"),
},
body => panic!("Unexpected statement body: {:?}", body),
body => panic!("Unexpected statement body: {body:?}"),
},
stmt => panic!("Unexpected statement: {:?}", stmt),
stmt => panic!("Unexpected statement: {stmt:?}"),
}
// Case 7: Multi-level compound quoted identifiers.
@ -2293,11 +2367,11 @@ fn parse_qualified_identifiers_with_numeric_prefix() {
&parts[..]
);
}
proj => panic!("Unexpected projection: {:?}", proj),
proj => panic!("Unexpected projection: {proj:?}"),
},
body => panic!("Unexpected statement body: {:?}", body),
body => panic!("Unexpected statement body: {body:?}"),
},
stmt => panic!("Unexpected statement: {:?}", stmt),
stmt => panic!("Unexpected statement: {stmt:?}"),
}
}
@ -4035,3 +4109,28 @@ fn parse_alter_table_drop_index() {
AlterTableOperation::DropIndex { name } if name.value == "idx_index"
);
}
#[test]
fn parse_json_member_of() {
mysql().verified_stmt(r#"SELECT 17 MEMBER OF('[23, "abc", 17, "ab", 10]')"#);
let sql = r#"SELECT 'ab' MEMBER OF('[23, "abc", 17, "ab", 10]')"#;
let stmt = mysql().verified_stmt(sql);
match stmt {
Statement::Query(query) => {
let select = query.body.as_select().unwrap();
assert_eq!(
select.projection,
vec![SelectItem::UnnamedExpr(Expr::MemberOf(MemberOf {
value: Box::new(Expr::Value(
Value::SingleQuotedString("ab".to_string()).into()
)),
array: Box::new(Expr::Value(
Value::SingleQuotedString(r#"[23, "abc", 17, "ab", 10]"#.to_string())
.into()
)),
}))]
);
}
_ => panic!("Unexpected statement {stmt}"),
}
}

View file

@ -606,9 +606,10 @@ fn parse_alter_table_constraints_unique_nulls_distinct() {
.verified_stmt("ALTER TABLE t ADD CONSTRAINT b UNIQUE NULLS NOT DISTINCT (c)")
{
Statement::AlterTable { operations, .. } => match &operations[0] {
AlterTableOperation::AddConstraint(TableConstraint::Unique {
nulls_distinct, ..
}) => {
AlterTableOperation::AddConstraint {
constraint: TableConstraint::Unique { nulls_distinct, .. },
..
} => {
assert_eq!(nulls_distinct, &NullsDistinctOption::NotDistinct)
}
_ => unreachable!(),
@ -764,10 +765,7 @@ fn parse_drop_extension() {
#[test]
fn parse_alter_table_alter_column() {
pg().one_statement_parses_to(
"ALTER TABLE tab ALTER COLUMN is_active TYPE TEXT USING 'text'",
"ALTER TABLE tab ALTER COLUMN is_active SET DATA TYPE TEXT USING 'text'",
);
pg().verified_stmt("ALTER TABLE tab ALTER COLUMN is_active TYPE TEXT USING 'text'");
match alter_table_op(
pg().verified_stmt(
@ -783,6 +781,7 @@ fn parse_alter_table_alter_column() {
AlterColumnOperation::SetDataType {
data_type: DataType::Text,
using: Some(using_expr),
had_set: true,
}
);
}
@ -2535,12 +2534,12 @@ fn parse_create_indices_with_operator_classes() {
for expected_operator_class in &operator_classes {
let single_column_sql_statement = format!(
"CREATE INDEX the_index_name ON users USING {expected_index_type} (concat_users_name(first_name, last_name){})",
expected_operator_class.as_ref().map(|oc| format!(" {}", oc))
expected_operator_class.as_ref().map(|oc| format!(" {oc}"))
.unwrap_or_default()
);
let multi_column_sql_statement = format!(
"CREATE INDEX the_index_name ON users USING {expected_index_type} (column_name,concat_users_name(first_name, last_name){})",
expected_operator_class.as_ref().map(|oc| format!(" {}", oc))
expected_operator_class.as_ref().map(|oc| format!(" {oc}"))
.unwrap_or_default()
);
@ -3273,7 +3272,7 @@ fn test_fn_arg_with_value_operator() {
assert!(matches!(
&args[..],
&[FunctionArg::ExprNamed { operator: FunctionArgOperator::Value, .. }]
), "Invalid function argument: {:?}", args);
), "Invalid function argument: {args:?}");
}
other => panic!("Expected: JSON_OBJECT('name' VALUE 'value') to be parsed as a function, but got {other:?}"),
}
@ -5258,7 +5257,10 @@ fn parse_at_time_zone() {
left: Box::new(Expr::AtTimeZone {
timestamp: Box::new(Expr::TypedString {
data_type: DataType::Timestamp(None, TimezoneInfo::None),
value: Value::SingleQuotedString("2001-09-28 01:00".to_string()),
value: ValueWithSpan {
value: Value::SingleQuotedString("2001-09-28 01:00".to_string()),
span: Span::empty(),
},
}),
time_zone: Box::new(Expr::Cast {
kind: CastKind::DoubleColon,
@ -5679,7 +5681,7 @@ fn parse_drop_trigger() {
"DROP TRIGGER{} check_update ON table_name{}",
if if_exists { " IF EXISTS" } else { "" },
option
.map(|o| format!(" {}", o))
.map(|o| format!(" {o}"))
.unwrap_or_else(|| "".to_string())
);
assert_eq!(
@ -5773,8 +5775,7 @@ fn parse_trigger_related_functions() {
// Now we parse the statements and check if they are parsed correctly.
let mut statements = pg()
.parse_sql_statements(&format!(
"{}{}{}{}",
sql_table_creation, sql_create_function, sql_create_trigger, sql_drop_trigger
"{sql_table_creation}{sql_create_function}{sql_create_trigger}{sql_drop_trigger}"
))
.unwrap();
@ -6201,3 +6202,153 @@ 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!(),
}
}
#[test]
fn parse_alter_table_constraint_not_valid() {
match pg_and_generic().verified_stmt(
"ALTER TABLE foo ADD CONSTRAINT bar FOREIGN KEY (baz) REFERENCES other(ref) NOT VALID",
) {
Statement::AlterTable { operations, .. } => {
assert_eq!(
operations,
vec![AlterTableOperation::AddConstraint {
constraint: TableConstraint::ForeignKey {
name: Some("bar".into()),
index_name: None,
columns: vec!["baz".into()],
foreign_table: ObjectName::from(vec!["other".into()]),
referred_columns: vec!["ref".into()],
on_delete: None,
on_update: None,
characteristics: None,
},
not_valid: true,
}]
);
}
_ => unreachable!(),
}
}
#[test]
fn parse_alter_table_validate_constraint() {
match pg_and_generic().verified_stmt("ALTER TABLE foo VALIDATE CONSTRAINT bar") {
Statement::AlterTable { operations, .. } => {
assert_eq!(
operations,
vec![AlterTableOperation::ValidateConstraint { name: "bar".into() }]
);
}
_ => unreachable!(),
}
}
#[test]
fn parse_create_server() {
let test_cases = vec![
(
"CREATE SERVER myserver FOREIGN DATA WRAPPER postgres_fdw",
CreateServerStatement {
name: ObjectName::from(vec!["myserver".into()]),
if_not_exists: false,
server_type: None,
version: None,
foreign_data_wrapper: ObjectName::from(vec!["postgres_fdw".into()]),
options: None,
},
),
(
"CREATE SERVER IF NOT EXISTS myserver TYPE 'server_type' VERSION 'server_version' FOREIGN DATA WRAPPER postgres_fdw",
CreateServerStatement {
name: ObjectName::from(vec!["myserver".into()]),
if_not_exists: true,
server_type: Some(Ident {
value: "server_type".to_string(),
quote_style: Some('\''),
span: Span::empty(),
}),
version: Some(Ident {
value: "server_version".to_string(),
quote_style: Some('\''),
span: Span::empty(),
}),
foreign_data_wrapper: ObjectName::from(vec!["postgres_fdw".into()]),
options: None,
}
),
(
"CREATE SERVER myserver2 FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'foo', dbname 'foodb', port '5432')",
CreateServerStatement {
name: ObjectName::from(vec!["myserver2".into()]),
if_not_exists: false,
server_type: None,
version: None,
foreign_data_wrapper: ObjectName::from(vec!["postgres_fdw".into()]),
options: Some(vec![
CreateServerOption {
key: "host".into(),
value: Ident {
value: "foo".to_string(),
quote_style: Some('\''),
span: Span::empty(),
},
},
CreateServerOption {
key: "dbname".into(),
value: Ident {
value: "foodb".to_string(),
quote_style: Some('\''),
span: Span::empty(),
},
},
CreateServerOption {
key: "port".into(),
value: Ident {
value: "5432".to_string(),
quote_style: Some('\''),
span: Span::empty(),
},
},
]),
}
)
];
for (sql, expected) in test_cases {
let Statement::CreateServer(stmt) = pg_and_generic().verified_stmt(sql) else {
unreachable!()
};
assert_eq!(stmt, expected);
}
}

View file

@ -402,3 +402,8 @@ fn parse_extract_single_quotes() {
fn parse_string_literal_backslash_escape() {
redshift().one_statement_parses_to(r#"SELECT 'l\'auto'"#, "SELECT 'l''auto'");
}
#[test]
fn parse_utf8_multibyte_idents() {
redshift().verified_stmt("SELECT 🚀.city AS 🎸 FROM customers AS 🚀");
}

View file

@ -2528,10 +2528,7 @@ fn test_snowflake_stage_object_names_into_location() {
.zip(allowed_object_names.iter_mut())
{
let (formatted_name, object_name) = it;
let sql = format!(
"COPY INTO {} FROM 'gcs://mybucket/./../a.csv'",
formatted_name
);
let sql = format!("COPY INTO {formatted_name} FROM 'gcs://mybucket/./../a.csv'");
match snowflake().verified_stmt(&sql) {
Statement::CopyIntoSnowflake { into, .. } => {
assert_eq!(into.0, object_name.0)
@ -2554,10 +2551,7 @@ fn test_snowflake_stage_object_names_into_table() {
.zip(allowed_object_names.iter_mut())
{
let (formatted_name, object_name) = it;
let sql = format!(
"COPY INTO {} FROM 'gcs://mybucket/./../a.csv'",
formatted_name
);
let sql = format!("COPY INTO {formatted_name} FROM 'gcs://mybucket/./../a.csv'");
match snowflake().verified_stmt(&sql) {
Statement::CopyIntoSnowflake { into, .. } => {
assert_eq!(into.0, object_name.0)
@ -3038,7 +3032,7 @@ fn parse_use() {
for object_name in &valid_object_names {
// Test single identifier without quotes
assert_eq!(
snowflake().verified_stmt(&format!("USE {}", object_name)),
snowflake().verified_stmt(&format!("USE {object_name}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::new(
object_name.to_string()
)])))
@ -3046,7 +3040,7 @@ fn parse_use() {
for &quote in &quote_styles {
// Test single identifier with different type of quotes
assert_eq!(
snowflake().verified_stmt(&format!("USE {}{}{}", quote, object_name, quote)),
snowflake().verified_stmt(&format!("USE {quote}{object_name}{quote}")),
Statement::Use(Use::Object(ObjectName::from(vec![Ident::with_quote(
quote,
object_name.to_string(),
@ -3058,7 +3052,9 @@ fn parse_use() {
for &quote in &quote_styles {
// Test double identifier with different type of quotes
assert_eq!(
snowflake().verified_stmt(&format!("USE {0}CATALOG{0}.{0}my_schema{0}", quote)),
snowflake().verified_stmt(&format!(
"USE {quote}CATALOG{quote}.{quote}my_schema{quote}"
)),
Statement::Use(Use::Object(ObjectName::from(vec![
Ident::with_quote(quote, "CATALOG"),
Ident::with_quote(quote, "my_schema")
@ -3077,35 +3073,37 @@ fn parse_use() {
for &quote in &quote_styles {
// Test single and double identifier with keyword and different type of quotes
assert_eq!(
snowflake().verified_stmt(&format!("USE DATABASE {0}my_database{0}", quote)),
snowflake().verified_stmt(&format!("USE DATABASE {quote}my_database{quote}")),
Statement::Use(Use::Database(ObjectName::from(vec![Ident::with_quote(
quote,
"my_database".to_string(),
)])))
);
assert_eq!(
snowflake().verified_stmt(&format!("USE SCHEMA {0}my_schema{0}", quote)),
snowflake().verified_stmt(&format!("USE SCHEMA {quote}my_schema{quote}")),
Statement::Use(Use::Schema(ObjectName::from(vec![Ident::with_quote(
quote,
"my_schema".to_string(),
)])))
);
assert_eq!(
snowflake().verified_stmt(&format!("USE SCHEMA {0}CATALOG{0}.{0}my_schema{0}", quote)),
snowflake().verified_stmt(&format!(
"USE SCHEMA {quote}CATALOG{quote}.{quote}my_schema{quote}"
)),
Statement::Use(Use::Schema(ObjectName::from(vec![
Ident::with_quote(quote, "CATALOG"),
Ident::with_quote(quote, "my_schema")
])))
);
assert_eq!(
snowflake().verified_stmt(&format!("USE ROLE {0}my_role{0}", quote)),
snowflake().verified_stmt(&format!("USE ROLE {quote}my_role{quote}")),
Statement::Use(Use::Role(ObjectName::from(vec![Ident::with_quote(
quote,
"my_role".to_string(),
)])))
);
assert_eq!(
snowflake().verified_stmt(&format!("USE WAREHOUSE {0}my_wh{0}", quote)),
snowflake().verified_stmt(&format!("USE WAREHOUSE {quote}my_wh{quote}")),
Statement::Use(Use::Warehouse(ObjectName::from(vec![Ident::with_quote(
quote,
"my_wh".to_string(),
@ -3142,7 +3140,7 @@ fn view_comment_option_should_be_after_column_list() {
"CREATE OR REPLACE VIEW v (a COMMENT 'a comment', b, c COMMENT 'c comment') COMMENT = 'Comment' AS SELECT a FROM t",
"CREATE OR REPLACE VIEW v (a COMMENT 'a comment', b, c COMMENT 'c comment') WITH (foo = bar) COMMENT = 'Comment' AS SELECT a FROM t",
] {
snowflake_and_generic()
snowflake()
.verified_stmt(sql);
}
}
@ -3151,7 +3149,7 @@ fn view_comment_option_should_be_after_column_list() {
fn parse_view_column_descriptions() {
let sql = "CREATE OR REPLACE VIEW v (a COMMENT 'Comment', b) AS SELECT a, b FROM table1";
match snowflake_and_generic().verified_stmt(sql) {
match snowflake().verified_stmt(sql) {
Statement::CreateView { name, columns, .. } => {
assert_eq!(name.to_string(), "v");
assert_eq!(
@ -3160,7 +3158,9 @@ fn parse_view_column_descriptions() {
ViewColumnDef {
name: Ident::new("a"),
data_type: None,
options: Some(vec![ColumnOption::Comment("Comment".to_string())]),
options: Some(ColumnOptions::SpaceSeparated(vec![ColumnOption::Comment(
"Comment".to_string()
)])),
},
ViewColumnDef {
name: Ident::new("b"),
@ -3645,7 +3645,7 @@ fn test_alter_session_followed_by_statement() {
.unwrap();
match stmts[..] {
[Statement::AlterSession { .. }, Statement::Query { .. }] => {}
_ => panic!("Unexpected statements: {:?}", stmts),
_ => panic!("Unexpected statements: {stmts:?}"),
}
}
@ -4101,6 +4101,96 @@ 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_multiple_column_options() {
let create_view_with_tag =
r#"CREATE VIEW X (COL WITH TAG (pii='email') COMMENT 'foobar') AS SELECT * FROM Y"#;
snowflake().verified_stmt(create_view_with_tag);
}
#[test]
fn test_snowflake_create_view_with_composite_tag() {
let create_view_with_tag =

View file

@ -324,7 +324,7 @@ fn parse_create_table_on_conflict_col() {
Keyword::IGNORE,
Keyword::REPLACE,
] {
let sql = format!("CREATE TABLE t1 (a INT, b INT ON CONFLICT {:?})", keyword);
let sql = format!("CREATE TABLE t1 (a INT, b INT ON CONFLICT {keyword:?})");
match sqlite_and_generic().verified_stmt(&sql) {
Statement::CreateTable(CreateTable { columns, .. }) => {
assert_eq!(
@ -410,7 +410,7 @@ fn parse_window_function_with_filter() {
"count",
"user_defined_function",
] {
let sql = format!("SELECT {}(x) FILTER (WHERE y) OVER () FROM t", func_name);
let sql = format!("SELECT {func_name}(x) FILTER (WHERE y) OVER () FROM t");
let select = sqlite().verified_only_select(&sql);
assert_eq!(select.to_string(), sql);
assert_eq!(
@ -444,7 +444,7 @@ fn parse_window_function_with_filter() {
fn parse_attach_database() {
let sql = "ATTACH DATABASE 'test.db' AS test";
let verified_stmt = sqlite().verified_stmt(sql);
assert_eq!(sql, format!("{}", verified_stmt));
assert_eq!(sql, format!("{verified_stmt}"));
match verified_stmt {
Statement::AttachDatabase {
schema_name,