From bc6e1d6e1a960c0c220c712a16b5046f563a5f6e Mon Sep 17 00:00:00 2001 From: Andriy Romanov Date: Thu, 30 Oct 2025 02:20:41 -0700 Subject: [PATCH 1/8] Added TIMESTAMP_NTZ type support with precision to Snowflake dialect (#2080) --- src/ast/data_type.rs | 6 ++++-- src/parser/mod.rs | 4 +++- tests/sqlparser_databricks.rs | 6 +++--- tests/sqlparser_snowflake.rs | 15 +++++++++++++++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index f8523e84..6da6a90d 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -375,7 +375,7 @@ pub enum DataType { /// Databricks timestamp without time zone. See [1]. /// /// [1]: https://docs.databricks.com/aws/en/sql/language-manual/data-types/timestamp-ntz-type - TimestampNtz, + TimestampNtz(Option), /// Interval type. Interval { /// [PostgreSQL] fields specification like `INTERVAL YEAR TO MONTH`. @@ -676,7 +676,9 @@ impl fmt::Display for DataType { DataType::Timestamp(precision, timezone_info) => { format_datetime_precision_and_tz(f, "TIMESTAMP", precision, timezone_info) } - DataType::TimestampNtz => write!(f, "TIMESTAMP_NTZ"), + DataType::TimestampNtz(precision) => { + format_type_with_optional_length(f, "TIMESTAMP_NTZ", precision, false) + } DataType::Datetime64(precision, timezone) => { format_clickhouse_datetime_precision_and_timezone( f, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b44171c7..1248c918 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10542,7 +10542,9 @@ impl<'a> Parser<'a> { self.parse_optional_precision()?, TimezoneInfo::Tz, )), - Keyword::TIMESTAMP_NTZ => Ok(DataType::TimestampNtz), + Keyword::TIMESTAMP_NTZ => { + Ok(DataType::TimestampNtz(self.parse_optional_precision()?)) + } Keyword::TIME => { let precision = self.parse_optional_precision()?; let tz = if self.parse_keyword(Keyword::WITH) { diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index e01611b6..92b63533 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -328,7 +328,7 @@ fn data_type_timestamp_ntz() { assert_eq!( databricks().verified_expr("TIMESTAMP_NTZ '2025-03-29T18:52:00'"), Expr::TypedString(TypedString { - data_type: DataType::TimestampNtz, + data_type: DataType::TimestampNtz(None), value: ValueWithSpan { value: Value::SingleQuotedString("2025-03-29T18:52:00".to_owned()), span: Span::empty(), @@ -345,7 +345,7 @@ fn data_type_timestamp_ntz() { expr: Box::new(Expr::Nested(Box::new(Expr::Identifier( "created_at".into() )))), - data_type: DataType::TimestampNtz, + data_type: DataType::TimestampNtz(None), format: None } ); @@ -357,7 +357,7 @@ fn data_type_timestamp_ntz() { columns, vec![ColumnDef { name: "x".into(), - data_type: DataType::TimestampNtz, + data_type: DataType::TimestampNtz(None), options: vec![], }] ); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index e7a12834..2be5eae8 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4638,6 +4638,21 @@ fn test_create_database() { assert!(err.contains("Expected"), "Unexpected error: {err}"); } +#[test] +fn test_timestamp_ntz_with_precision() { + snowflake().verified_stmt("SELECT CAST('2024-01-01 01:00:00' AS TIMESTAMP_NTZ(1))"); + snowflake().verified_stmt("SELECT CAST('2024-01-01 01:00:00' AS TIMESTAMP_NTZ(9))"); + + let select = + snowflake().verified_only_select("SELECT CAST('2024-01-01 01:00:00' AS TIMESTAMP_NTZ(9))"); + match expr_from_projection(only(&select.projection)) { + Expr::Cast { data_type, .. } => { + assert_eq!(*data_type, DataType::TimestampNtz(Some(9))); + } + _ => unreachable!(), + } +} + #[test] fn test_drop_constraints() { snowflake().verified_stmt("ALTER TABLE tbl DROP PRIMARY KEY"); From 308a7231bcbc5c1c8ab71fe38f17b1a21632a6c6 Mon Sep 17 00:00:00 2001 From: Alexander Beedie Date: Thu, 30 Oct 2025 15:57:25 +0400 Subject: [PATCH 2/8] Make `BitwiseNot` ("~") available for all dialects, not just PostgreSQL (#2081) --- src/ast/operator.rs | 50 ++++++++++++++++++------------------- src/parser/mod.rs | 6 +++-- tests/sqlparser_common.rs | 19 ++++++++++++++ tests/sqlparser_postgres.rs | 2 -- 4 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index d0bb05e3..58c401f7 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -33,35 +33,35 @@ use super::display_separated; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum UnaryOperator { + /// `@-@` Length or circumference (PostgreSQL/Redshift geometric operator) + /// see + AtDashAt, + /// Unary logical not operator: e.g. `! false` (Hive-specific) + BangNot, + /// Bitwise Not, e.g. `~9` + BitwiseNot, + /// `@@` Center (PostgreSQL/Redshift geometric operator) + /// see + DoubleAt, + /// `#` Number of points in path or polygon (PostgreSQL/Redshift geometric operator) + /// see + Hash, /// Plus, e.g. `+9` Plus, /// Minus, e.g. `-9` Minus, /// Not, e.g. `NOT(true)` Not, - /// Bitwise Not, e.g. `~9` (PostgreSQL-specific) - PGBitwiseNot, - /// Square root, e.g. `|/9` (PostgreSQL-specific) - PGSquareRoot, + /// Absolute value, e.g. `@ -9` (PostgreSQL-specific) + PGAbs, /// Cube root, e.g. `||/27` (PostgreSQL-specific) PGCubeRoot, /// Factorial, e.g. `9!` (PostgreSQL-specific) PGPostfixFactorial, /// Factorial, e.g. `!!9` (PostgreSQL-specific) PGPrefixFactorial, - /// Absolute value, e.g. `@ -9` (PostgreSQL-specific) - PGAbs, - /// Unary logical not operator: e.g. `! false` (Hive-specific) - BangNot, - /// `#` Number of points in path or polygon (PostgreSQL/Redshift geometric operator) - /// see - Hash, - /// `@-@` Length or circumference (PostgreSQL/Redshift geometric operator) - /// see - AtDashAt, - /// `@@` Center (PostgreSQL/Redshift geometric operator) - /// see - DoubleAt, + /// Square root, e.g. `|/9` (PostgreSQL-specific) + PGSquareRoot, /// `?-` Is horizontal? (PostgreSQL/Redshift geometric operator) /// see QuestionDash, @@ -73,19 +73,19 @@ pub enum UnaryOperator { impl fmt::Display for UnaryOperator { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(match self { - UnaryOperator::Plus => "+", + UnaryOperator::AtDashAt => "@-@", + UnaryOperator::BangNot => "!", + UnaryOperator::BitwiseNot => "~", + UnaryOperator::DoubleAt => "@@", + UnaryOperator::Hash => "#", UnaryOperator::Minus => "-", UnaryOperator::Not => "NOT", - UnaryOperator::PGBitwiseNot => "~", - UnaryOperator::PGSquareRoot => "|/", + UnaryOperator::PGAbs => "@", UnaryOperator::PGCubeRoot => "||/", UnaryOperator::PGPostfixFactorial => "!", UnaryOperator::PGPrefixFactorial => "!!", - UnaryOperator::PGAbs => "@", - UnaryOperator::BangNot => "!", - UnaryOperator::Hash => "#", - UnaryOperator::AtDashAt => "@-@", - UnaryOperator::DoubleAt => "@@", + UnaryOperator::PGSquareRoot => "|/", + UnaryOperator::Plus => "+", UnaryOperator::QuestionDash => "?-", UnaryOperator::QuestionPipe => "?|", }) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1248c918..9a01e510 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1633,7 +1633,6 @@ impl<'a> Parser<'a> { | tok @ Token::PGSquareRoot | tok @ Token::PGCubeRoot | tok @ Token::AtSign - | tok @ Token::Tilde if dialect_is!(dialect is PostgreSqlDialect) => { let op = match tok { @@ -1641,7 +1640,6 @@ impl<'a> Parser<'a> { Token::PGSquareRoot => UnaryOperator::PGSquareRoot, Token::PGCubeRoot => UnaryOperator::PGCubeRoot, Token::AtSign => UnaryOperator::PGAbs, - Token::Tilde => UnaryOperator::PGBitwiseNot, _ => unreachable!(), }; Ok(Expr::UnaryOp { @@ -1651,6 +1649,10 @@ impl<'a> Parser<'a> { ), }) } + Token::Tilde => Ok(Expr::UnaryOp { + op: UnaryOperator::BitwiseNot, + expr: Box::new(self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?), + }), tok @ Token::Sharp | tok @ Token::AtDashAt | tok @ Token::AtAt diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 99b7ac3f..f1ba5df0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -17613,3 +17613,22 @@ fn test_parse_alter_user() { } verified_stmt("ALTER USER u1 SET DEFAULT_SECONDARY_ROLES=('ALL'), PASSWORD='secret', WORKLOAD_IDENTITY=(TYPE=AWS, ARN='arn:aws:iam::123456789:r1/')"); } + +#[test] +fn parse_generic_unary_ops() { + let unary_ops = &[ + ("~", UnaryOperator::BitwiseNot), + ("-", UnaryOperator::Minus), + ("+", UnaryOperator::Plus), + ]; + for (str_op, op) in unary_ops { + let select = verified_only_select(&format!("SELECT {}expr", &str_op)); + assert_eq!( + UnnamedExpr(UnaryOp { + op: *op, + expr: Box::new(Identifier(Ident::new("expr"))), + }), + select.projection[0] + ); + } +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index bcc15428..9ba0fb97 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2144,13 +2144,11 @@ fn parse_ampersand_arobase() { #[test] fn parse_pg_unary_ops() { let pg_unary_ops = &[ - ("~", UnaryOperator::PGBitwiseNot), ("|/", UnaryOperator::PGSquareRoot), ("||/", UnaryOperator::PGCubeRoot), ("!!", UnaryOperator::PGPrefixFactorial), ("@", UnaryOperator::PGAbs), ]; - for (str_op, op) in pg_unary_ops { let select = pg().verified_only_select(&format!("SELECT {}a", &str_op)); assert_eq!( From eabde4b41e9607722c23b96699bbd83e0e9bd343 Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Tue, 11 Nov 2025 03:10:32 -0500 Subject: [PATCH 3/8] feat: Add RESET to the base dialect #2078 (#2079) --- src/ast/mod.rs | 56 ++++++++++++++++++++++++++++++++++----- src/ast/spans.rs | 1 + src/parser/mod.rs | 13 +++++++++ tests/sqlparser_common.rs | 23 ++++++++++++++++ 4 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 176d3654..4636e4ba 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2787,10 +2787,11 @@ impl fmt::Display for Declare { } /// Sql options of a `CREATE TABLE` statement. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum CreateTableOptions { + #[default] None, /// Options specified using the `WITH` keyword. /// e.g. `WITH (description = "123")` @@ -2819,12 +2820,6 @@ pub enum CreateTableOptions { TableProperties(Vec), } -impl Default for CreateTableOptions { - fn default() -> Self { - Self::None - } -} - impl fmt::Display for CreateTableOptions { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -4263,6 +4258,14 @@ pub enum Statement { /// ``` /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_VACUUM_command.html) Vacuum(VacuumStatement), + /// Restore the value of a run-time parameter to the default value. + /// + /// ```sql + /// RESET configuration_parameter; + /// RESET ALL; + /// ``` + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-reset.html) + Reset(ResetStatement), } impl From for Statement { @@ -5757,6 +5760,7 @@ impl fmt::Display for Statement { Statement::AlterSchema(s) => write!(f, "{s}"), Statement::Vacuum(s) => write!(f, "{s}"), Statement::AlterUser(s) => write!(f, "{s}"), + Statement::Reset(s) => write!(f, "{s}"), } } } @@ -10519,6 +10523,38 @@ impl fmt::Display for VacuumStatement { } } +/// Variants of the RESET statement +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum Reset { + /// Resets all session parameters to their default values. + ALL, + + /// Resets a specific session parameter to its default value. + ConfigurationParameter(ObjectName), +} + +/// Resets a session parameter to its default value. +/// ```sql +/// RESET { ALL | } +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ResetStatement { + pub reset: Reset, +} + +impl fmt::Display for ResetStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self.reset { + Reset::ALL => write!(f, "RESET ALL"), + Reset::ConfigurationParameter(param) => write!(f, "RESET {}", param), + } + } +} + impl From for Statement { fn from(s: Set) -> Self { Self::Set(s) @@ -10759,6 +10795,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(r: ResetStatement) -> Self { + Self::Reset(r) + } +} + #[cfg(test)] mod tests { use crate::tokenizer::Location; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 7d2a0009..34edabd9 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -475,6 +475,7 @@ impl Spanned for Statement { Statement::AlterSchema(s) => s.span(), Statement::Vacuum(..) => Span::empty(), Statement::AlterUser(..) => Span::empty(), + Statement::Reset(..) => Span::empty(), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9a01e510..f43329be 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -656,6 +656,7 @@ impl<'a> Parser<'a> { self.prev_token(); self.parse_vacuum() } + Keyword::RESET => self.parse_reset(), _ => self.expected("an SQL statement", next_token), }, Token::LParen => { @@ -17727,6 +17728,18 @@ impl<'a> Parser<'a> { _ => self.expected("expected option value", self.peek_token()), } } + + /// Parses a RESET statement + fn parse_reset(&mut self) -> Result { + if self.parse_keyword(Keyword::ALL) { + return Ok(Statement::Reset(ResetStatement { reset: Reset::ALL })); + } + + let obj = self.parse_object_name(false)?; + Ok(Statement::Reset(ResetStatement { + reset: Reset::ConfigurationParameter(obj), + })) + } } fn maybe_prefixed_expr(expr: Expr, prefix: Option) -> Expr { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f1ba5df0..9ea91c64 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -17632,3 +17632,26 @@ fn parse_generic_unary_ops() { ); } } + +#[test] +fn parse_reset_statement() { + match verified_stmt("RESET some_parameter") { + Statement::Reset(ResetStatement { + reset: Reset::ConfigurationParameter(o), + }) => assert_eq!(o, ObjectName::from(vec!["some_parameter".into()])), + _ => unreachable!(), + } + match verified_stmt("RESET some_extension.some_parameter") { + Statement::Reset(ResetStatement { + reset: Reset::ConfigurationParameter(o), + }) => assert_eq!( + o, + ObjectName::from(vec!["some_extension".into(), "some_parameter".into()]) + ), + _ => unreachable!(), + } + match verified_stmt("RESET ALL") { + Statement::Reset(ResetStatement { reset }) => assert_eq!(reset, Reset::ALL), + _ => unreachable!(), + } +} From c439ee9419a8b3e91ae6fc406a2fa88bb2bf1bdd Mon Sep 17 00:00:00 2001 From: Andriy Romanov Date: Tue, 11 Nov 2025 00:10:37 -0800 Subject: [PATCH 4/8] Add snowflake dynamic table parsing (#2083) --- src/ast/ddl.rs | 48 ++++++++++++++++++++++++++----- src/ast/mod.rs | 2 +- src/ast/spans.rs | 3 ++ src/dialect/snowflake.rs | 55 ++++++++++++++++++++++++++++++++---- src/keywords.rs | 1 + src/parser/mod.rs | 6 +++- src/test_utils.rs | 2 +- tests/sqlparser_mysql.rs | 4 +-- tests/sqlparser_snowflake.rs | 8 ++++++ 9 files changed, 112 insertions(+), 17 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index fd481213..1ae24a7f 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -365,6 +365,18 @@ pub enum AlterTableOperation { DropClusteringKey, SuspendRecluster, ResumeRecluster, + /// `REFRESH` + /// + /// Note: this is Snowflake specific for dynamic tables + Refresh, + /// `SUSPEND` + /// + /// Note: this is Snowflake specific for dynamic tables + Suspend, + /// `RESUME` + /// + /// Note: this is Snowflake specific for dynamic tables + Resume, /// `ALGORITHM [=] { DEFAULT | INSTANT | INPLACE | COPY }` /// /// [MySQL]-specific table alter algorithm. @@ -845,6 +857,15 @@ impl fmt::Display for AlterTableOperation { write!(f, "RESUME RECLUSTER")?; Ok(()) } + AlterTableOperation::Refresh => { + write!(f, "REFRESH") + } + AlterTableOperation::Suspend => { + write!(f, "SUSPEND") + } + AlterTableOperation::Resume => { + write!(f, "RESUME") + } AlterTableOperation::AutoIncrement { equals, value } => { write!( f, @@ -3532,6 +3553,20 @@ impl Spanned for DropExtension { } } +/// Table type for ALTER TABLE statements. +/// Used to distinguish between regular tables, Iceberg tables, and Dynamic tables. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterTableType { + /// Iceberg table type + /// + Iceberg, + /// Dynamic table type + /// + Dynamic, +} + /// ALTER TABLE statement #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -3548,19 +3583,18 @@ pub struct AlterTable { /// For example: `ALTER TABLE table_name ON CLUSTER cluster_name ADD COLUMN c UInt32` /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/alter/update) pub on_cluster: Option, - /// Snowflake "ICEBERG" clause for Iceberg tables - /// - pub iceberg: bool, + /// Table type: None for regular tables, Some(AlterTableType) for Iceberg or Dynamic tables + pub table_type: Option, /// Token that represents the end of the statement (semicolon or EOF) pub end_token: AttachedToken, } impl fmt::Display for AlterTable { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.iceberg { - write!(f, "ALTER ICEBERG TABLE ")?; - } else { - write!(f, "ALTER TABLE ")?; + match &self.table_type { + Some(AlterTableType::Iceberg) => write!(f, "ALTER ICEBERG TABLE ")?, + Some(AlterTableType::Dynamic) => write!(f, "ALTER DYNAMIC TABLE ")?, + None => write!(f, "ALTER TABLE ")?, } if self.if_exists { diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4636e4ba..b32697f6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -61,7 +61,7 @@ pub use self::dcl::{ pub use self::ddl::{ AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, AlterTableLock, - AlterTableOperation, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, + AlterTableOperation, AlterTableType, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, CreateExtension, CreateFunction, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 34edabd9..80244e69 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1108,6 +1108,9 @@ impl Spanned for AlterTableOperation { AlterTableOperation::DropClusteringKey => Span::empty(), AlterTableOperation::SuspendRecluster => Span::empty(), AlterTableOperation::ResumeRecluster => Span::empty(), + AlterTableOperation::Refresh => Span::empty(), + AlterTableOperation::Suspend => Span::empty(), + AlterTableOperation::Resume => Span::empty(), AlterTableOperation::Algorithm { .. } => Span::empty(), AlterTableOperation::AutoIncrement { value, .. } => value.span(), AlterTableOperation::Lock { .. } => Span::empty(), diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 825fd45f..bb0d4f16 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -17,6 +17,7 @@ #[cfg(not(feature = "std"))] use crate::alloc::string::ToString; +use crate::ast::helpers::attached_token::AttachedToken; use crate::ast::helpers::key_value_options::{ KeyValueOption, KeyValueOptionKind, KeyValueOptions, KeyValueOptionsDelimiter, }; @@ -26,11 +27,12 @@ use crate::ast::helpers::stmt_data_loading::{ FileStagingCommand, StageLoadSelectItem, StageLoadSelectItemKind, StageParamsObject, }; use crate::ast::{ - CatalogSyncNamespaceMode, ColumnOption, ColumnPolicy, ColumnPolicyProperty, ContactEntry, - CopyIntoSnowflakeKind, CreateTableLikeKind, DollarQuotedString, Ident, IdentityParameters, - IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, - InitializeKind, ObjectName, ObjectNamePart, RefreshModeKind, RowAccessPolicy, ShowObjects, - SqlOption, Statement, StorageSerializationPolicy, TagsColumnOption, Value, WrappedCollection, + AlterTable, AlterTableOperation, AlterTableType, CatalogSyncNamespaceMode, ColumnOption, + ColumnPolicy, ColumnPolicyProperty, ContactEntry, CopyIntoSnowflakeKind, CreateTableLikeKind, + DollarQuotedString, Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, + IdentityPropertyKind, IdentityPropertyOrder, InitializeKind, ObjectName, ObjectNamePart, + RefreshModeKind, RowAccessPolicy, ShowObjects, SqlOption, Statement, + StorageSerializationPolicy, TagsColumnOption, Value, WrappedCollection, }; use crate::dialect::{Dialect, Precedence}; use crate::keywords::Keyword; @@ -214,6 +216,11 @@ impl Dialect for SnowflakeDialect { return Some(parser.parse_begin_exception_end()); } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::DYNAMIC, Keyword::TABLE]) { + // ALTER DYNAMIC TABLE + return Some(parse_alter_dynamic_table(parser)); + } + if parser.parse_keywords(&[Keyword::ALTER, Keyword::SESSION]) { // ALTER SESSION let set = match parser.parse_one_of_keywords(&[Keyword::SET, Keyword::UNSET]) { @@ -604,6 +611,44 @@ fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result +fn parse_alter_dynamic_table(parser: &mut Parser) -> Result { + // Use parse_object_name(true) to support IDENTIFIER() function + let table_name = parser.parse_object_name(true)?; + + // Parse the operation (REFRESH, SUSPEND, or RESUME) + let operation = if parser.parse_keyword(Keyword::REFRESH) { + AlterTableOperation::Refresh + } else if parser.parse_keyword(Keyword::SUSPEND) { + AlterTableOperation::Suspend + } else if parser.parse_keyword(Keyword::RESUME) { + AlterTableOperation::Resume + } else { + return parser.expected( + "REFRESH, SUSPEND, or RESUME after ALTER DYNAMIC TABLE", + parser.peek_token(), + ); + }; + + let end_token = if parser.peek_token_ref().token == Token::SemiColon { + parser.peek_token_ref().clone() + } else { + parser.get_current_token().clone() + }; + + Ok(Statement::AlterTable(AlterTable { + name: table_name, + if_exists: false, + only: false, + operations: vec![operation], + location: None, + on_cluster: None, + table_type: Some(AlterTableType::Dynamic), + end_token: AttachedToken(end_token), + })) +} + /// Parse snowflake alter session. /// fn parse_alter_session(parser: &mut Parser, set: bool) -> Result { diff --git a/src/keywords.rs b/src/keywords.rs index 319c5782..dc4ecd2f 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -783,6 +783,7 @@ define_keywords!( REF, REFERENCES, REFERENCING, + REFRESH, REFRESH_MODE, REGCLASS, REGEXP, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f43329be..026f6249 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9462,7 +9462,11 @@ impl<'a> Parser<'a> { operations, location, on_cluster, - iceberg, + table_type: if iceberg { + Some(AlterTableType::Iceberg) + } else { + None + }, end_token: AttachedToken(end_token), } .into()) diff --git a/src/test_utils.rs b/src/test_utils.rs index a8c8afd5..b6100d49 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -347,7 +347,7 @@ pub fn alter_table_op_with_name(stmt: Statement, expected_name: &str) -> AlterTa assert_eq!(alter_table.name.to_string(), expected_name); assert!(!alter_table.if_exists); assert!(!alter_table.only); - assert!(!alter_table.iceberg); + assert_eq!(alter_table.table_type, None); only(alter_table.operations) } _ => panic!("Expected ALTER TABLE statement"), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index e43df87a..86c1013c 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2746,14 +2746,14 @@ fn parse_alter_table_add_column() { if_exists, only, operations, - iceberg, + table_type, location: _, on_cluster: _, end_token: _, }) => { assert_eq!(name.to_string(), "tab"); assert!(!if_exists); - assert!(!iceberg); + assert_eq!(table_type, None); assert!(!only); assert_eq!( operations, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 2be5eae8..f187af1b 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -4662,3 +4662,11 @@ fn test_drop_constraints() { snowflake().verified_stmt("ALTER TABLE tbl DROP FOREIGN KEY k1 RESTRICT"); snowflake().verified_stmt("ALTER TABLE tbl DROP CONSTRAINT c1 CASCADE"); } + +#[test] +fn test_alter_dynamic_table() { + snowflake().verified_stmt("ALTER DYNAMIC TABLE MY_DYNAMIC_TABLE REFRESH"); + snowflake().verified_stmt("ALTER DYNAMIC TABLE my_database.my_schema.my_dynamic_table REFRESH"); + snowflake().verified_stmt("ALTER DYNAMIC TABLE my_dyn_table SUSPEND"); + snowflake().verified_stmt("ALTER DYNAMIC TABLE my_dyn_table RESUME"); +} From f69407b344a6ca80266289c827bb5c23b202adfc Mon Sep 17 00:00:00 2001 From: etgarperets Date: Tue, 11 Nov 2025 10:10:42 +0200 Subject: [PATCH 5/8] Add support for `INSERT INTO VALUE` (#2085) --- src/ast/query.rs | 8 +++++++- src/ast/spans.rs | 1 + src/parser/mod.rs | 21 ++++++++++++++++----- tests/sqlparser_bigquery.rs | 3 +++ tests/sqlparser_common.rs | 31 ++++++++++++++++++++++++------- tests/sqlparser_databricks.rs | 1 + tests/sqlparser_mysql.rs | 9 +++++++++ tests/sqlparser_postgres.rs | 3 +++ 8 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 599b013a..33c92614 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -3135,12 +3135,18 @@ pub struct Values { /// Was there an explicit ROWs keyword (MySQL)? /// pub explicit_row: bool, + // MySql supports both VALUES and VALUE keywords. + // + pub value_keyword: bool, pub rows: Vec>, } impl fmt::Display for Values { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("VALUES")?; + match self.value_keyword { + true => f.write_str("VALUE")?, + false => f.write_str("VALUES")?, + }; let prefix = if self.explicit_row { "ROW" } else { "" }; let mut delim = ""; for row in &self.rows { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 80244e69..719e261c 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -223,6 +223,7 @@ impl Spanned for Values { fn span(&self) -> Span { let Values { explicit_row: _, // bool, + value_keyword: _, rows, } = self; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 026f6249..9615343c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -12533,7 +12533,10 @@ impl<'a> Parser<'a> { SetExpr::Query(subquery) } else if self.parse_keyword(Keyword::VALUES) { let is_mysql = dialect_of!(self is MySqlDialect); - SetExpr::Values(self.parse_values(is_mysql)?) + SetExpr::Values(self.parse_values(is_mysql, false)?) + } else if self.parse_keyword(Keyword::VALUE) { + let is_mysql = dialect_of!(self is MySqlDialect); + SetExpr::Values(self.parse_values(is_mysql, true)?) } else if self.parse_keyword(Keyword::TABLE) { SetExpr::Table(Box::new(self.parse_as_table()?)) } else { @@ -13837,7 +13840,7 @@ impl<'a> Parser<'a> { // Snowflake and Databricks allow syntax like below: // SELECT * FROM VALUES (1, 'a'), (2, 'b') AS t (col1, col2) // where there are no parentheses around the VALUES clause. - let values = SetExpr::Values(self.parse_values(false)?); + let values = SetExpr::Values(self.parse_values(false, false)?); let alias = self.maybe_parse_table_alias()?; Ok(TableFactor::Derived { lateral: false, @@ -16504,7 +16507,11 @@ impl<'a> Parser<'a> { }) } - pub fn parse_values(&mut self, allow_empty: bool) -> Result { + pub fn parse_values( + &mut self, + allow_empty: bool, + value_keyword: bool, + ) -> Result { let mut explicit_row = false; let rows = self.parse_comma_separated(|parser| { @@ -16522,7 +16529,11 @@ impl<'a> Parser<'a> { Ok(exprs) } })?; - Ok(Values { explicit_row, rows }) + Ok(Values { + explicit_row, + rows, + value_keyword, + }) } pub fn parse_start_transaction(&mut self) -> Result { @@ -16937,7 +16948,7 @@ impl<'a> Parser<'a> { MergeInsertKind::Row } else { self.expect_keyword_is(Keyword::VALUES)?; - let values = self.parse_values(is_mysql)?; + let values = self.parse_values(is_mysql, false)?; MergeInsertKind::Values(values) }; MergeAction::Insert(MergeInsertExpr { columns, kind }) diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 03a0ac81..0ef1c4f0 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1807,6 +1807,7 @@ fn parse_merge() { let insert_action = MergeAction::Insert(MergeInsertExpr { columns: vec![Ident::new("product"), Ident::new("quantity")], kind: MergeInsertKind::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![Expr::value(number("1")), Expr::value(number("2"))]], }), @@ -1951,6 +1952,7 @@ fn parse_merge() { action: MergeAction::Insert(MergeInsertExpr { columns: vec![Ident::new("a"), Ident::new("b"),], kind: MergeInsertKind::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::value(number("1")), @@ -1965,6 +1967,7 @@ fn parse_merge() { action: MergeAction::Insert(MergeInsertExpr { columns: vec![], kind: MergeInsertKind::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::value(number("1")), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9ea91c64..a235c392 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -106,19 +106,19 @@ fn parse_insert_values() { let rows2 = vec![row.clone(), row]; let sql = "INSERT customer VALUES (1, 2, 3)"; - check_one(sql, "customer", &[], &rows1); + check_one(sql, "customer", &[], &rows1, false); let sql = "INSERT INTO customer VALUES (1, 2, 3)"; - check_one(sql, "customer", &[], &rows1); + check_one(sql, "customer", &[], &rows1, false); let sql = "INSERT INTO customer VALUES (1, 2, 3), (1, 2, 3)"; - check_one(sql, "customer", &[], &rows2); + check_one(sql, "customer", &[], &rows2, false); let sql = "INSERT INTO public.customer VALUES (1, 2, 3)"; - check_one(sql, "public.customer", &[], &rows1); + check_one(sql, "public.customer", &[], &rows1, false); let sql = "INSERT INTO db.public.customer VALUES (1, 2, 3)"; - check_one(sql, "db.public.customer", &[], &rows1); + check_one(sql, "db.public.customer", &[], &rows1, false); let sql = "INSERT INTO public.customer (id, name, active) VALUES (1, 2, 3)"; check_one( @@ -126,6 +126,16 @@ fn parse_insert_values() { "public.customer", &["id".to_string(), "name".to_string(), "active".to_string()], &rows1, + false, + ); + + let sql = r"INSERT INTO t (id, name, active) VALUE (1, 2, 3)"; + check_one( + sql, + "t", + &["id".to_string(), "name".to_string(), "active".to_string()], + &rows1, + true, ); fn check_one( @@ -133,6 +143,7 @@ fn parse_insert_values() { expected_table_name: &str, expected_columns: &[String], expected_rows: &[Vec], + expected_value_keyword: bool, ) { match verified_stmt(sql) { Statement::Insert(Insert { @@ -147,8 +158,13 @@ fn parse_insert_values() { assert_eq!(column, &Ident::new(expected_columns[index].clone())); } match *source.body { - SetExpr::Values(Values { rows, .. }) => { - assert_eq!(rows.as_slice(), expected_rows) + SetExpr::Values(Values { + rows, + value_keyword, + .. + }) => { + assert_eq!(rows.as_slice(), expected_rows); + assert!(value_keyword == expected_value_keyword); } _ => unreachable!(), } @@ -9908,6 +9924,7 @@ fn parse_merge() { action: MergeAction::Insert(MergeInsertExpr { columns: vec![Ident::new("A"), Ident::new("B"), Ident::new("C")], kind: MergeInsertKind::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::CompoundIdentifier(vec![ diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 92b63533..065e8f9e 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -157,6 +157,7 @@ fn test_databricks_lambdas() { #[test] fn test_values_clause() { let values = Values { + value_keyword: false, explicit_row: false, rows: vec![ vec![ diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 86c1013c..b31a5b7c 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1885,6 +1885,7 @@ fn parse_simple_insert() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![ vec![ @@ -1950,6 +1951,7 @@ fn parse_ignore_insert() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::Value( @@ -1999,6 +2001,7 @@ fn parse_priority_insert() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::Value( @@ -2045,6 +2048,7 @@ fn parse_priority_insert() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::Value( @@ -2097,6 +2101,7 @@ fn parse_insert_as() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![Expr::Value( (Value::SingleQuotedString("2024-01-01".to_string())).with_empty_span() @@ -2156,6 +2161,7 @@ fn parse_insert_as() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::value(number("1")), @@ -2206,6 +2212,7 @@ fn parse_replace_insert() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::Value( @@ -2253,6 +2260,7 @@ fn parse_empty_row_insert() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![], vec![]] })), @@ -2303,6 +2311,7 @@ fn parse_insert_with_on_duplicate_update() { Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::Value( diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 9ba0fb97..87cb43ed 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5169,6 +5169,7 @@ fn test_simple_postgres_insert_with_alias() { source: Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::Identifier(Ident::new("DEFAULT")), @@ -5238,6 +5239,7 @@ fn test_simple_postgres_insert_with_alias() { source: Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::Identifier(Ident::new("DEFAULT")), @@ -5309,6 +5311,7 @@ fn test_simple_insert_with_quoted_alias() { source: Some(Box::new(Query { with: None, body: Box::new(SetExpr::Values(Values { + value_keyword: false, explicit_row: false, rows: vec![vec![ Expr::Identifier(Ident::new("DEFAULT")), From a00d5cd9673cc393e17135b5f710b5694005e050 Mon Sep 17 00:00:00 2001 From: Christopher Watford Date: Wed, 12 Nov 2025 04:52:15 -0500 Subject: [PATCH 6/8] feat: Add support for SET SESSION AUTHORIZATION #2086 (#2087) --- src/ast/mod.rs | 46 +++++++++++++++++++++++++++++++++++++++ src/parser/mod.rs | 12 ++++++++++ tests/sqlparser_common.rs | 25 +++++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b32697f6..9a62b71d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2919,6 +2919,15 @@ pub enum Set { /// MySQL-style /// SET a = 1, b = 2, ..; MultipleAssignments { assignments: Vec }, + /// Session authorization for Postgres/Redshift + /// + /// ```sql + /// SET SESSION AUTHORIZATION { user_name | DEFAULT } + /// ``` + /// + /// See + /// See + SetSessionAuthorization(SetSessionAuthorizationParam), /// MS-SQL session /// /// See @@ -2993,6 +3002,7 @@ impl Display for Set { modifier = context_modifier.map(|m| format!("{m}")).unwrap_or_default() ) } + Self::SetSessionAuthorization(kind) => write!(f, "SET SESSION AUTHORIZATION {kind}"), Self::SetSessionParam(kind) => write!(f, "SET {kind}"), Self::SetTransaction { modes, @@ -9822,6 +9832,42 @@ impl fmt::Display for TableObject { } } +/// Represents a SET SESSION AUTHORIZATION statement +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct SetSessionAuthorizationParam { + pub scope: ContextModifier, + pub kind: SetSessionAuthorizationParamKind, +} + +impl fmt::Display for SetSessionAuthorizationParam { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.kind) + } +} + +/// Represents the parameter kind for SET SESSION AUTHORIZATION +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SetSessionAuthorizationParamKind { + /// Default authorization + Default, + + /// User name + User(Ident), +} + +impl fmt::Display for SetSessionAuthorizationParamKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SetSessionAuthorizationParamKind::Default => write!(f, "DEFAULT"), + SetSessionAuthorizationParamKind::User(name) => write!(f, "{}", name), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9615343c..e4a5af72 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13112,6 +13112,18 @@ impl<'a> Parser<'a> { session: false, } .into()); + } else if self.parse_keyword(Keyword::AUTHORIZATION) { + let auth_value = if self.parse_keyword(Keyword::DEFAULT) { + SetSessionAuthorizationParamKind::Default + } else { + let value = self.parse_identifier()?; + SetSessionAuthorizationParamKind::User(value) + }; + return Ok(Set::SetSessionAuthorization(SetSessionAuthorizationParam { + scope: scope.expect("SET ... AUTHORIZATION must have a scope"), + kind: auth_value, + }) + .into()); } if self.dialect.supports_comma_separated_set_assignments() { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a235c392..e85a8ec6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -17672,3 +17672,28 @@ fn parse_reset_statement() { _ => unreachable!(), } } + +#[test] +fn test_parse_set_session_authorization() { + let stmt = verified_stmt("SET SESSION AUTHORIZATION DEFAULT"); + assert_eq!( + stmt, + Statement::Set(Set::SetSessionAuthorization(SetSessionAuthorizationParam { + scope: ContextModifier::Session, + kind: SetSessionAuthorizationParamKind::Default, + })) + ); + + let stmt = verified_stmt("SET SESSION AUTHORIZATION 'username'"); + assert_eq!( + stmt, + Statement::Set(Set::SetSessionAuthorization(SetSessionAuthorizationParam { + scope: ContextModifier::Session, + kind: SetSessionAuthorizationParamKind::User(Ident { + value: "username".to_string(), + quote_style: Some('\''), + span: Span::empty(), + }), + })) + ); +} From 78be8b178b53e5999ebcb27713fb9f05e8ece156 Mon Sep 17 00:00:00 2001 From: Luca Cappelletti Date: Thu, 13 Nov 2025 10:25:27 +0100 Subject: [PATCH 7/8] Complete PostgreSQL `CREATE TYPE` Support (#2094) Co-authored-by: Ifeanyi Ubah --- src/ast/ddl.rs | 313 +++++++++++++++++++++++++++++++++++- src/ast/mod.rs | 38 +++-- src/keywords.rs | 19 +++ src/parser/mod.rs | 313 ++++++++++++++++++++++++++++++++++-- tests/sqlparser_common.rs | 259 ++++++++++++++++++++++++++--- tests/sqlparser_postgres.rs | 2 +- 6 files changed, 889 insertions(+), 55 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1ae24a7f..286b16a4 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -2023,21 +2023,44 @@ impl fmt::Display for DropBehavior { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum UserDefinedTypeRepresentation { + /// Composite type: `CREATE TYPE name AS (attributes)` Composite { attributes: Vec, }, + /// Enum type: `CREATE TYPE name AS ENUM (labels)` + /// /// Note: this is PostgreSQL-specific. See Enum { labels: Vec }, + /// Range type: `CREATE TYPE name AS RANGE (options)` + /// + /// Note: this is PostgreSQL-specific. See + Range { + options: Vec, + }, + /// Base type (SQL definition): `CREATE TYPE name (options)` + /// + /// Note the lack of `AS` keyword + /// + /// Note: this is PostgreSQL-specific. See + SqlDefinition { + options: Vec, + }, } impl fmt::Display for UserDefinedTypeRepresentation { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - UserDefinedTypeRepresentation::Composite { attributes } => { - write!(f, "({})", display_comma_separated(attributes)) + Self::Composite { attributes } => { + write!(f, "AS ({})", display_comma_separated(attributes)) } - UserDefinedTypeRepresentation::Enum { labels } => { - write!(f, "ENUM ({})", display_comma_separated(labels)) + Self::Enum { labels } => { + write!(f, "AS ENUM ({})", display_comma_separated(labels)) + } + Self::Range { options } => { + write!(f, "AS RANGE ({})", display_comma_separated(options)) + } + Self::SqlDefinition { options } => { + write!(f, "({})", display_comma_separated(options)) } } } @@ -2063,6 +2086,288 @@ impl fmt::Display for UserDefinedTypeCompositeAttributeDef { } } +/// Internal length specification for PostgreSQL user-defined base types. +/// +/// Specifies the internal length in bytes of the new type's internal representation. +/// The default assumption is that it is variable-length. +/// +/// # PostgreSQL Documentation +/// See: +/// +/// # Examples +/// ```sql +/// CREATE TYPE mytype ( +/// INPUT = in_func, +/// OUTPUT = out_func, +/// INTERNALLENGTH = 16 -- Fixed 16-byte length +/// ); +/// +/// CREATE TYPE mytype2 ( +/// INPUT = in_func, +/// OUTPUT = out_func, +/// INTERNALLENGTH = VARIABLE -- Variable length +/// ); +/// ``` +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum UserDefinedTypeInternalLength { + /// Fixed internal length: `INTERNALLENGTH = ` + Fixed(u64), + /// Variable internal length: `INTERNALLENGTH = VARIABLE` + Variable, +} + +impl fmt::Display for UserDefinedTypeInternalLength { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + UserDefinedTypeInternalLength::Fixed(n) => write!(f, "{}", n), + UserDefinedTypeInternalLength::Variable => write!(f, "VARIABLE"), + } + } +} + +/// Alignment specification for PostgreSQL user-defined base types. +/// +/// Specifies the storage alignment requirement for values of the data type. +/// The allowed values equate to alignment on 1, 2, 4, or 8 byte boundaries. +/// Note that variable-length types must have an alignment of at least 4, since +/// they necessarily contain an int4 as their first component. +/// +/// # PostgreSQL Documentation +/// See: +/// +/// # Examples +/// ```sql +/// CREATE TYPE mytype ( +/// INPUT = in_func, +/// OUTPUT = out_func, +/// ALIGNMENT = int4 -- 4-byte alignment +/// ); +/// ``` +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum Alignment { + /// Single-byte alignment: `ALIGNMENT = char` + Char, + /// 2-byte alignment: `ALIGNMENT = int2` + Int2, + /// 4-byte alignment: `ALIGNMENT = int4` + Int4, + /// 8-byte alignment: `ALIGNMENT = double` + Double, +} + +impl fmt::Display for Alignment { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Alignment::Char => write!(f, "char"), + Alignment::Int2 => write!(f, "int2"), + Alignment::Int4 => write!(f, "int4"), + Alignment::Double => write!(f, "double"), + } + } +} + +/// Storage specification for PostgreSQL user-defined base types. +/// +/// Specifies the storage strategy for values of the data type: +/// - `plain`: Prevents compression and out-of-line storage (for fixed-length types) +/// - `external`: Allows out-of-line storage but not compression +/// - `extended`: Allows both compression and out-of-line storage (default for most types) +/// - `main`: Allows compression but discourages out-of-line storage +/// +/// # PostgreSQL Documentation +/// See: +/// +/// # Examples +/// ```sql +/// CREATE TYPE mytype ( +/// INPUT = in_func, +/// OUTPUT = out_func, +/// STORAGE = plain +/// ); +/// ``` +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum UserDefinedTypeStorage { + /// No compression or out-of-line storage: `STORAGE = plain` + Plain, + /// Out-of-line storage allowed, no compression: `STORAGE = external` + External, + /// Both compression and out-of-line storage allowed: `STORAGE = extended` + Extended, + /// Compression allowed, out-of-line discouraged: `STORAGE = main` + Main, +} + +impl fmt::Display for UserDefinedTypeStorage { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + UserDefinedTypeStorage::Plain => write!(f, "plain"), + UserDefinedTypeStorage::External => write!(f, "external"), + UserDefinedTypeStorage::Extended => write!(f, "extended"), + UserDefinedTypeStorage::Main => write!(f, "main"), + } + } +} + +/// Options for PostgreSQL `CREATE TYPE ... AS RANGE` statement. +/// +/// Range types are data types representing a range of values of some element type +/// (called the range's subtype). These options configure the behavior of the range type. +/// +/// # PostgreSQL Documentation +/// See: +/// +/// # Examples +/// ```sql +/// CREATE TYPE int4range AS RANGE ( +/// SUBTYPE = int4, +/// SUBTYPE_OPCLASS = int4_ops, +/// CANONICAL = int4range_canonical, +/// SUBTYPE_DIFF = int4range_subdiff +/// ); +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum UserDefinedTypeRangeOption { + /// The element type that the range type will represent: `SUBTYPE = subtype` + Subtype(DataType), + /// The operator class for the subtype: `SUBTYPE_OPCLASS = subtype_operator_class` + SubtypeOpClass(ObjectName), + /// Collation to use for ordering the subtype: `COLLATION = collation` + Collation(ObjectName), + /// Function to convert range values to canonical form: `CANONICAL = canonical_function` + Canonical(ObjectName), + /// Function to compute the difference between two subtype values: `SUBTYPE_DIFF = subtype_diff_function` + SubtypeDiff(ObjectName), + /// Name of the corresponding multirange type: `MULTIRANGE_TYPE_NAME = multirange_type_name` + MultirangeTypeName(ObjectName), +} + +impl fmt::Display for UserDefinedTypeRangeOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + UserDefinedTypeRangeOption::Subtype(dt) => write!(f, "SUBTYPE = {}", dt), + UserDefinedTypeRangeOption::SubtypeOpClass(name) => { + write!(f, "SUBTYPE_OPCLASS = {}", name) + } + UserDefinedTypeRangeOption::Collation(name) => write!(f, "COLLATION = {}", name), + UserDefinedTypeRangeOption::Canonical(name) => write!(f, "CANONICAL = {}", name), + UserDefinedTypeRangeOption::SubtypeDiff(name) => write!(f, "SUBTYPE_DIFF = {}", name), + UserDefinedTypeRangeOption::MultirangeTypeName(name) => { + write!(f, "MULTIRANGE_TYPE_NAME = {}", name) + } + } + } +} + +/// Options for PostgreSQL `CREATE TYPE ... ()` statement (base type definition). +/// +/// Base types are the lowest-level data types in PostgreSQL. To define a new base type, +/// you must specify functions that convert it to and from text representation, and optionally +/// binary representation and other properties. +/// +/// Note: This syntax uses parentheses directly after the type name, without the `AS` keyword. +/// +/// # PostgreSQL Documentation +/// See: +/// +/// # Examples +/// ```sql +/// CREATE TYPE complex ( +/// INPUT = complex_in, +/// OUTPUT = complex_out, +/// INTERNALLENGTH = 16, +/// ALIGNMENT = double +/// ); +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum UserDefinedTypeSqlDefinitionOption { + /// Function to convert from external text representation to internal: `INPUT = input_function` + Input(ObjectName), + /// Function to convert from internal to external text representation: `OUTPUT = output_function` + Output(ObjectName), + /// Function to convert from external binary representation to internal: `RECEIVE = receive_function` + Receive(ObjectName), + /// Function to convert from internal to external binary representation: `SEND = send_function` + Send(ObjectName), + /// Function to convert type modifiers from text array to internal form: `TYPMOD_IN = type_modifier_input_function` + TypmodIn(ObjectName), + /// Function to convert type modifiers from internal to text form: `TYPMOD_OUT = type_modifier_output_function` + TypmodOut(ObjectName), + /// Function to compute statistics for the data type: `ANALYZE = analyze_function` + Analyze(ObjectName), + /// Function to handle subscripting operations: `SUBSCRIPT = subscript_function` + Subscript(ObjectName), + /// Internal storage size in bytes, or VARIABLE for variable-length: `INTERNALLENGTH = { internallength | VARIABLE }` + InternalLength(UserDefinedTypeInternalLength), + /// Indicates values are passed by value rather than by reference: `PASSEDBYVALUE` + PassedByValue, + /// Storage alignment requirement (1, 2, 4, or 8 bytes): `ALIGNMENT = alignment` + Alignment(Alignment), + /// Storage strategy for varlena types: `STORAGE = storage` + Storage(UserDefinedTypeStorage), + /// Copy properties from an existing type: `LIKE = like_type` + Like(ObjectName), + /// Type category for implicit casting rules (single char): `CATEGORY = category` + Category(char), + /// Whether this type is preferred within its category: `PREFERRED = preferred` + Preferred(bool), + /// Default value for the type: `DEFAULT = default` + Default(Expr), + /// Element type for array types: `ELEMENT = element` + Element(DataType), + /// Delimiter character for array value display: `DELIMITER = delimiter` + Delimiter(String), + /// Whether the type supports collation: `COLLATABLE = collatable` + Collatable(bool), +} + +impl fmt::Display for UserDefinedTypeSqlDefinitionOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + UserDefinedTypeSqlDefinitionOption::Input(name) => write!(f, "INPUT = {}", name), + UserDefinedTypeSqlDefinitionOption::Output(name) => write!(f, "OUTPUT = {}", name), + UserDefinedTypeSqlDefinitionOption::Receive(name) => write!(f, "RECEIVE = {}", name), + UserDefinedTypeSqlDefinitionOption::Send(name) => write!(f, "SEND = {}", name), + UserDefinedTypeSqlDefinitionOption::TypmodIn(name) => write!(f, "TYPMOD_IN = {}", name), + UserDefinedTypeSqlDefinitionOption::TypmodOut(name) => { + write!(f, "TYPMOD_OUT = {}", name) + } + UserDefinedTypeSqlDefinitionOption::Analyze(name) => write!(f, "ANALYZE = {}", name), + UserDefinedTypeSqlDefinitionOption::Subscript(name) => { + write!(f, "SUBSCRIPT = {}", name) + } + UserDefinedTypeSqlDefinitionOption::InternalLength(len) => { + write!(f, "INTERNALLENGTH = {}", len) + } + UserDefinedTypeSqlDefinitionOption::PassedByValue => write!(f, "PASSEDBYVALUE"), + UserDefinedTypeSqlDefinitionOption::Alignment(align) => { + write!(f, "ALIGNMENT = {}", align) + } + UserDefinedTypeSqlDefinitionOption::Storage(storage) => { + write!(f, "STORAGE = {}", storage) + } + UserDefinedTypeSqlDefinitionOption::Like(name) => write!(f, "LIKE = {}", name), + UserDefinedTypeSqlDefinitionOption::Category(c) => write!(f, "CATEGORY = '{}'", c), + UserDefinedTypeSqlDefinitionOption::Preferred(b) => write!(f, "PREFERRED = {}", b), + UserDefinedTypeSqlDefinitionOption::Default(expr) => write!(f, "DEFAULT = {}", expr), + UserDefinedTypeSqlDefinitionOption::Element(dt) => write!(f, "ELEMENT = {}", dt), + UserDefinedTypeSqlDefinitionOption::Delimiter(s) => { + write!(f, "DELIMITER = '{}'", escape_single_quote_string(s)) + } + UserDefinedTypeSqlDefinitionOption::Collatable(b) => write!(f, "COLLATABLE = {}", b), + } + } +} + /// PARTITION statement used in ALTER TABLE et al. such as in Hive and ClickHouse SQL. /// For example, ClickHouse's OPTIMIZE TABLE supports syntax like PARTITION ID 'partition_id' and PARTITION expr. /// [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/optimize) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9a62b71d..aa3fb082 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -59,19 +59,21 @@ pub use self::dcl::{ AlterRoleOperation, CreateRole, ResetConfig, RoleOption, SecondaryRoles, SetConfigValue, Use, }; pub use self::ddl::{ - AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, AlterPolicyOperation, - AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, AlterTableLock, - AlterTableOperation, AlterTableType, AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, - AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, ClusteredBy, ColumnDef, - ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, - ConstraintCharacteristics, CreateConnector, CreateDomain, CreateExtension, CreateFunction, - CreateIndex, CreateTable, CreateTrigger, CreateView, Deduplicate, DeferrableInitial, - DropBehavior, DropExtension, DropFunction, DropTrigger, GeneratedAs, GeneratedExpressionMode, - IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, - IdentityPropertyOrder, IndexColumn, IndexOption, IndexType, KeyOrIndexDisplay, Msck, - NullsDistinctOption, Owner, Partition, ProcedureParam, ReferentialAction, RenameTableNameKind, - ReplicaIdentity, TagsColumnOption, TriggerObjectKind, Truncate, - UserDefinedTypeCompositeAttributeDef, UserDefinedTypeRepresentation, ViewColumnDef, + Alignment, AlterColumnOperation, AlterConnectorOwner, AlterIndexOperation, + AlterPolicyOperation, AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, + AlterTableLock, AlterTableOperation, AlterTableType, AlterType, AlterTypeAddValue, + AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, + ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, ColumnOptions, ColumnPolicy, + ColumnPolicyProperty, ConstraintCharacteristics, CreateConnector, CreateDomain, + CreateExtension, CreateFunction, CreateIndex, CreateTable, CreateTrigger, CreateView, + Deduplicate, DeferrableInitial, DropBehavior, DropExtension, DropFunction, DropTrigger, + GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, + IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, + IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, Owner, Partition, + ProcedureParam, ReferentialAction, RenameTableNameKind, ReplicaIdentity, TagsColumnOption, + TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, + UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, + UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, }; pub use self::dml::{Delete, Insert, Update}; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -2787,7 +2789,7 @@ impl fmt::Display for Declare { } /// Sql options of a `CREATE TABLE` statement. -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Default)] +#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum CreateTableOptions { @@ -4107,7 +4109,7 @@ pub enum Statement { /// ``` CreateType { name: ObjectName, - representation: UserDefinedTypeRepresentation, + representation: Option, }, /// ```sql /// PRAGMA . = @@ -5657,7 +5659,11 @@ impl fmt::Display for Statement { name, representation, } => { - write!(f, "CREATE TYPE {name} AS {representation}") + write!(f, "CREATE TYPE {name}")?; + if let Some(repr) = representation { + write!(f, " {repr}")?; + } + Ok(()) } Statement::Pragma { name, value, is_eq } => { write!(f, "PRAGMA {name}")?; diff --git a/src/keywords.rs b/src/keywords.rs index dc4ecd2f..7ff42b41 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -91,6 +91,7 @@ define_keywords!( ALERT, ALGORITHM, ALIAS, + ALIGNMENT, ALL, ALLOCATE, ALLOWOVERWRITE, @@ -166,6 +167,7 @@ define_keywords!( CACHE, CALL, CALLED, + CANONICAL, CARDINALITY, CASCADE, CASCADED, @@ -177,6 +179,7 @@ define_keywords!( CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER, CATALOG_SYNC_NAMESPACE_MODE, CATCH, + CATEGORY, CEIL, CEILING, CENTURY, @@ -203,6 +206,7 @@ define_keywords!( CLUSTERED, CLUSTERING, COALESCE, + COLLATABLE, COLLATE, COLLATION, COLLECT, @@ -501,6 +505,7 @@ define_keywords!( INT8, INTEGER, INTEGRATION, + INTERNALLENGTH, INTERPOLATE, INTERSECT, INTERSECTION, @@ -563,6 +568,7 @@ define_keywords!( LS, LSEG, MACRO, + MAIN, MANAGE, MANAGED, MANAGEDLOCATION, @@ -615,6 +621,7 @@ define_keywords!( MONTH, MONTHS, MSCK, + MULTIRANGE_TYPE_NAME, MULTISET, MUTATION, NAME, @@ -718,6 +725,7 @@ define_keywords!( PARTITION, PARTITIONED, PARTITIONS, + PASSEDBYVALUE, PASSING, PASSKEY, PASSWORD, @@ -734,6 +742,7 @@ define_keywords!( PERSISTENT, PIVOT, PLACING, + PLAIN, PLAN, PLANS, POINT, @@ -748,6 +757,7 @@ define_keywords!( PRECEDES, PRECEDING, PRECISION, + PREFERRED, PREPARE, PRESERVE, PRESET, @@ -778,6 +788,7 @@ define_keywords!( READS, READ_ONLY, REAL, + RECEIVE, RECLUSTER, RECURSIVE, REF, @@ -868,6 +879,7 @@ define_keywords!( SELECT, SEMANTIC_VIEW, SEMI, + SEND, SENSITIVE, SEPARATOR, SEQUENCE, @@ -936,9 +948,13 @@ define_keywords!( STRING, STRUCT, SUBMULTISET, + SUBSCRIPT, SUBSTR, SUBSTRING, SUBSTRING_REGEX, + SUBTYPE, + SUBTYPE_DIFF, + SUBTYPE_OPCLASS, SUCCEEDS, SUM, SUPER, @@ -1008,6 +1024,8 @@ define_keywords!( TSVECTOR, TUPLE, TYPE, + TYPMOD_IN, + TYPMOD_OUT, UBIGINT, UESCAPE, UHUGEINT, @@ -1057,6 +1075,7 @@ define_keywords!( VARBINARY, VARBIT, VARCHAR, + VARIABLE, VARIABLES, VARYING, VAR_POP, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e4a5af72..2744a967 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17359,20 +17359,59 @@ impl<'a> Parser<'a> { pub fn parse_create_type(&mut self) -> Result { let name = self.parse_object_name(false)?; - self.expect_keyword_is(Keyword::AS)?; - if self.parse_keyword(Keyword::ENUM) { - return self.parse_create_type_enum(name); - } + // Check if we have AS keyword + let has_as = self.parse_keyword(Keyword::AS); - let mut attributes = vec![]; - if !self.consume_token(&Token::LParen) || self.consume_token(&Token::RParen) { + if !has_as { + // Two cases: CREATE TYPE name; or CREATE TYPE name (options); + if self.consume_token(&Token::LParen) { + // CREATE TYPE name (options) - SQL definition without AS + let options = self.parse_create_type_sql_definition_options()?; + self.expect_token(&Token::RParen)?; + return Ok(Statement::CreateType { + name, + representation: Some(UserDefinedTypeRepresentation::SqlDefinition { options }), + }); + } + + // CREATE TYPE name; - no representation return Ok(Statement::CreateType { name, - representation: UserDefinedTypeRepresentation::Composite { attributes }, + representation: None, }); } + // We have AS keyword + if self.parse_keyword(Keyword::ENUM) { + // CREATE TYPE name AS ENUM (labels) + self.parse_create_type_enum(name) + } else if self.parse_keyword(Keyword::RANGE) { + // CREATE TYPE name AS RANGE (options) + self.parse_create_type_range(name) + } else if self.consume_token(&Token::LParen) { + // CREATE TYPE name AS (attributes) - Composite + self.parse_create_type_composite(name) + } else { + self.expected("ENUM, RANGE, or '(' after AS", self.peek_token()) + } + } + + /// Parse remainder of `CREATE TYPE AS (attributes)` statement (composite type) + /// + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtype.html) + fn parse_create_type_composite(&mut self, name: ObjectName) -> Result { + if self.consume_token(&Token::RParen) { + // Empty composite type + return Ok(Statement::CreateType { + name, + representation: Some(UserDefinedTypeRepresentation::Composite { + attributes: vec![], + }), + }); + } + + let mut attributes = vec![]; loop { let attr_name = self.parse_identifier()?; let attr_data_type = self.parse_data_type()?; @@ -17386,18 +17425,16 @@ impl<'a> Parser<'a> { data_type: attr_data_type, collation: attr_collation, }); - let comma = self.consume_token(&Token::Comma); - if self.consume_token(&Token::RParen) { - // allow a trailing comma + + if !self.consume_token(&Token::Comma) { break; - } else if !comma { - return self.expected("',' or ')' after attribute definition", self.peek_token()); } } + self.expect_token(&Token::RParen)?; Ok(Statement::CreateType { name, - representation: UserDefinedTypeRepresentation::Composite { attributes }, + representation: Some(UserDefinedTypeRepresentation::Composite { attributes }), }) } @@ -17411,10 +17448,258 @@ impl<'a> Parser<'a> { Ok(Statement::CreateType { name, - representation: UserDefinedTypeRepresentation::Enum { labels }, + representation: Some(UserDefinedTypeRepresentation::Enum { labels }), }) } + /// Parse remainder of `CREATE TYPE AS RANGE` statement + /// + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createtype.html) + fn parse_create_type_range(&mut self, name: ObjectName) -> Result { + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated0(|p| p.parse_range_option(), Token::RParen)?; + self.expect_token(&Token::RParen)?; + + Ok(Statement::CreateType { + name, + representation: Some(UserDefinedTypeRepresentation::Range { options }), + }) + } + + /// Parse a single range option for a `CREATE TYPE AS RANGE` statement + fn parse_range_option(&mut self) -> Result { + let keyword = self.parse_one_of_keywords(&[ + Keyword::SUBTYPE, + Keyword::SUBTYPE_OPCLASS, + Keyword::COLLATION, + Keyword::CANONICAL, + Keyword::SUBTYPE_DIFF, + Keyword::MULTIRANGE_TYPE_NAME, + ]); + + match keyword { + Some(Keyword::SUBTYPE) => { + self.expect_token(&Token::Eq)?; + let data_type = self.parse_data_type()?; + Ok(UserDefinedTypeRangeOption::Subtype(data_type)) + } + Some(Keyword::SUBTYPE_OPCLASS) => { + self.expect_token(&Token::Eq)?; + let name = self.parse_object_name(false)?; + Ok(UserDefinedTypeRangeOption::SubtypeOpClass(name)) + } + Some(Keyword::COLLATION) => { + self.expect_token(&Token::Eq)?; + let name = self.parse_object_name(false)?; + Ok(UserDefinedTypeRangeOption::Collation(name)) + } + Some(Keyword::CANONICAL) => { + self.expect_token(&Token::Eq)?; + let name = self.parse_object_name(false)?; + Ok(UserDefinedTypeRangeOption::Canonical(name)) + } + Some(Keyword::SUBTYPE_DIFF) => { + self.expect_token(&Token::Eq)?; + let name = self.parse_object_name(false)?; + Ok(UserDefinedTypeRangeOption::SubtypeDiff(name)) + } + Some(Keyword::MULTIRANGE_TYPE_NAME) => { + self.expect_token(&Token::Eq)?; + let name = self.parse_object_name(false)?; + Ok(UserDefinedTypeRangeOption::MultirangeTypeName(name)) + } + _ => self.expected("range option keyword", self.peek_token()), + } + } + + /// Parse SQL definition options for CREATE TYPE (options) + fn parse_create_type_sql_definition_options( + &mut self, + ) -> Result, ParserError> { + self.parse_comma_separated0(|p| p.parse_sql_definition_option(), Token::RParen) + } + + /// Parse a single SQL definition option for CREATE TYPE (options) + fn parse_sql_definition_option( + &mut self, + ) -> Result { + let keyword = self.parse_one_of_keywords(&[ + Keyword::INPUT, + Keyword::OUTPUT, + Keyword::RECEIVE, + Keyword::SEND, + Keyword::TYPMOD_IN, + Keyword::TYPMOD_OUT, + Keyword::ANALYZE, + Keyword::SUBSCRIPT, + Keyword::INTERNALLENGTH, + Keyword::PASSEDBYVALUE, + Keyword::ALIGNMENT, + Keyword::STORAGE, + Keyword::LIKE, + Keyword::CATEGORY, + Keyword::PREFERRED, + Keyword::DEFAULT, + Keyword::ELEMENT, + Keyword::DELIMITER, + Keyword::COLLATABLE, + ]); + + match keyword { + Some(Keyword::INPUT) => { + self.expect_token(&Token::Eq)?; + let name = self.parse_object_name(false)?; + Ok(UserDefinedTypeSqlDefinitionOption::Input(name)) + } + Some(Keyword::OUTPUT) => { + self.expect_token(&Token::Eq)?; + let name = self.parse_object_name(false)?; + Ok(UserDefinedTypeSqlDefinitionOption::Output(name)) + } + Some(Keyword::RECEIVE) => { + self.expect_token(&Token::Eq)?; + let name = self.parse_object_name(false)?; + Ok(UserDefinedTypeSqlDefinitionOption::Receive(name)) + } + Some(Keyword::SEND) => { + self.expect_token(&Token::Eq)?; + let name = self.parse_object_name(false)?; + Ok(UserDefinedTypeSqlDefinitionOption::Send(name)) + } + Some(Keyword::TYPMOD_IN) => { + self.expect_token(&Token::Eq)?; + let name = self.parse_object_name(false)?; + Ok(UserDefinedTypeSqlDefinitionOption::TypmodIn(name)) + } + Some(Keyword::TYPMOD_OUT) => { + self.expect_token(&Token::Eq)?; + let name = self.parse_object_name(false)?; + Ok(UserDefinedTypeSqlDefinitionOption::TypmodOut(name)) + } + Some(Keyword::ANALYZE) => { + self.expect_token(&Token::Eq)?; + let name = self.parse_object_name(false)?; + Ok(UserDefinedTypeSqlDefinitionOption::Analyze(name)) + } + Some(Keyword::SUBSCRIPT) => { + self.expect_token(&Token::Eq)?; + let name = self.parse_object_name(false)?; + Ok(UserDefinedTypeSqlDefinitionOption::Subscript(name)) + } + Some(Keyword::INTERNALLENGTH) => { + self.expect_token(&Token::Eq)?; + if self.parse_keyword(Keyword::VARIABLE) { + Ok(UserDefinedTypeSqlDefinitionOption::InternalLength( + UserDefinedTypeInternalLength::Variable, + )) + } else { + let value = self.parse_literal_uint()?; + Ok(UserDefinedTypeSqlDefinitionOption::InternalLength( + UserDefinedTypeInternalLength::Fixed(value), + )) + } + } + Some(Keyword::PASSEDBYVALUE) => Ok(UserDefinedTypeSqlDefinitionOption::PassedByValue), + Some(Keyword::ALIGNMENT) => { + self.expect_token(&Token::Eq)?; + let align_keyword = self.parse_one_of_keywords(&[ + Keyword::CHAR, + Keyword::INT2, + Keyword::INT4, + Keyword::DOUBLE, + ]); + match align_keyword { + Some(Keyword::CHAR) => Ok(UserDefinedTypeSqlDefinitionOption::Alignment( + Alignment::Char, + )), + Some(Keyword::INT2) => Ok(UserDefinedTypeSqlDefinitionOption::Alignment( + Alignment::Int2, + )), + Some(Keyword::INT4) => Ok(UserDefinedTypeSqlDefinitionOption::Alignment( + Alignment::Int4, + )), + Some(Keyword::DOUBLE) => Ok(UserDefinedTypeSqlDefinitionOption::Alignment( + Alignment::Double, + )), + _ => self.expected( + "alignment value (char, int2, int4, or double)", + self.peek_token(), + ), + } + } + Some(Keyword::STORAGE) => { + self.expect_token(&Token::Eq)?; + let storage_keyword = self.parse_one_of_keywords(&[ + Keyword::PLAIN, + Keyword::EXTERNAL, + Keyword::EXTENDED, + Keyword::MAIN, + ]); + match storage_keyword { + Some(Keyword::PLAIN) => Ok(UserDefinedTypeSqlDefinitionOption::Storage( + UserDefinedTypeStorage::Plain, + )), + Some(Keyword::EXTERNAL) => Ok(UserDefinedTypeSqlDefinitionOption::Storage( + UserDefinedTypeStorage::External, + )), + Some(Keyword::EXTENDED) => Ok(UserDefinedTypeSqlDefinitionOption::Storage( + UserDefinedTypeStorage::Extended, + )), + Some(Keyword::MAIN) => Ok(UserDefinedTypeSqlDefinitionOption::Storage( + UserDefinedTypeStorage::Main, + )), + _ => self.expected( + "storage value (plain, external, extended, or main)", + self.peek_token(), + ), + } + } + Some(Keyword::LIKE) => { + self.expect_token(&Token::Eq)?; + let name = self.parse_object_name(false)?; + Ok(UserDefinedTypeSqlDefinitionOption::Like(name)) + } + Some(Keyword::CATEGORY) => { + self.expect_token(&Token::Eq)?; + let category_str = self.parse_literal_string()?; + let category_char = category_str.chars().next().ok_or_else(|| { + ParserError::ParserError( + "CATEGORY value must be a single character".to_string(), + ) + })?; + Ok(UserDefinedTypeSqlDefinitionOption::Category(category_char)) + } + Some(Keyword::PREFERRED) => { + self.expect_token(&Token::Eq)?; + let value = + self.parse_keyword(Keyword::TRUE) || !self.parse_keyword(Keyword::FALSE); + Ok(UserDefinedTypeSqlDefinitionOption::Preferred(value)) + } + Some(Keyword::DEFAULT) => { + self.expect_token(&Token::Eq)?; + let expr = self.parse_expr()?; + Ok(UserDefinedTypeSqlDefinitionOption::Default(expr)) + } + Some(Keyword::ELEMENT) => { + self.expect_token(&Token::Eq)?; + let data_type = self.parse_data_type()?; + Ok(UserDefinedTypeSqlDefinitionOption::Element(data_type)) + } + Some(Keyword::DELIMITER) => { + self.expect_token(&Token::Eq)?; + let delimiter = self.parse_literal_string()?; + Ok(UserDefinedTypeSqlDefinitionOption::Delimiter(delimiter)) + } + Some(Keyword::COLLATABLE) => { + self.expect_token(&Token::Eq)?; + let value = + self.parse_keyword(Keyword::TRUE) || !self.parse_keyword(Keyword::FALSE); + Ok(UserDefinedTypeSqlDefinitionOption::Collatable(value)) + } + _ => self.expected("SQL definition option keyword", self.peek_token()), + } + } + fn parse_parenthesized_identifiers(&mut self) -> Result, ParserError> { self.expect_token(&Token::LParen)?; let idents = self.parse_comma_separated0(|p| p.parse_identifier(), Token::RParen)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e85a8ec6..b360f751 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11710,28 +11710,247 @@ fn parse_projection_trailing_comma() { #[test] fn parse_create_type() { - let create_type = - verified_stmt("CREATE TYPE db.type_name AS (foo INT, bar TEXT COLLATE \"de_DE\")"); - assert_eq!( + match verified_stmt("CREATE TYPE mytype") { Statement::CreateType { - name: ObjectName::from(vec![Ident::new("db"), Ident::new("type_name")]), - representation: UserDefinedTypeRepresentation::Composite { - attributes: vec![ - UserDefinedTypeCompositeAttributeDef { - name: Ident::new("foo"), - data_type: DataType::Int(None), - collation: None, - }, - UserDefinedTypeCompositeAttributeDef { - name: Ident::new("bar"), - data_type: DataType::Text, - collation: Some(ObjectName::from(vec![Ident::with_quote('\"', "de_DE")])), - } - ] + name, + representation, + } => { + assert_eq!(name.to_string(), "mytype"); + assert!(representation.is_none()); + } + _ => unreachable!(), + } + + match verified_stmt("CREATE TYPE address AS (street VARCHAR(100), city TEXT COLLATE \"en_US\")") + { + Statement::CreateType { + name, + representation, + } => { + assert_eq!(name.to_string(), "address"); + match representation { + Some(UserDefinedTypeRepresentation::Composite { attributes }) => { + assert_eq!(attributes.len(), 2); + assert_eq!(attributes[0].name, Ident::new("street")); + assert_eq!( + attributes[0].data_type, + DataType::Varchar(Some(CharacterLength::IntegerLength { + length: 100, + unit: None + })) + ); + assert_eq!(attributes[0].collation, None); + + assert_eq!(attributes[1].name, Ident::new("city")); + assert_eq!(attributes[1].data_type, DataType::Text); + assert_eq!( + attributes[1].collation.as_ref().map(|n| n.to_string()), + Some("\"en_US\"".to_string()) + ); + } + _ => unreachable!(), } - }, - create_type - ); + } + _ => unreachable!(), + } + + verified_stmt("CREATE TYPE empty AS ()"); + + match verified_stmt("CREATE TYPE mood AS ENUM ('happy', 'sad')") { + Statement::CreateType { + name, + representation, + } => { + assert_eq!(name.to_string(), "mood"); + match representation { + Some(UserDefinedTypeRepresentation::Enum { labels }) => { + assert_eq!(labels.len(), 2); + assert_eq!(labels[0], Ident::with_quote('\'', "happy")); + assert_eq!(labels[1], Ident::with_quote('\'', "sad")); + } + _ => unreachable!(), + } + } + _ => unreachable!(), + } + + match verified_stmt("CREATE TYPE int4range AS RANGE (SUBTYPE = INTEGER, CANONICAL = fn1)") { + Statement::CreateType { + name, + representation, + } => { + assert_eq!(name.to_string(), "int4range"); + match representation { + Some(UserDefinedTypeRepresentation::Range { options }) => { + assert_eq!(options.len(), 2); + assert!(matches!( + options[0], + UserDefinedTypeRangeOption::Subtype(DataType::Integer(_)) + )); + assert!(matches!( + options[1], + UserDefinedTypeRangeOption::Canonical(_) + )); + } + _ => unreachable!(), + } + } + _ => unreachable!(), + } + + verified_stmt("CREATE TYPE textrange AS RANGE (SUBTYPE = TEXT, COLLATION = \"en_US\", MULTIRANGE_TYPE_NAME = textmultirange)"); + + match verified_stmt( + "CREATE TYPE int4range AS RANGE (SUBTYPE = INTEGER, SUBTYPE_OPCLASS = int4_ops)", + ) { + Statement::CreateType { + name, + representation, + } => { + assert_eq!(name.to_string(), "int4range"); + match representation { + Some(UserDefinedTypeRepresentation::Range { options }) => { + assert_eq!(options.len(), 2); + assert!(matches!( + options[0], + UserDefinedTypeRangeOption::Subtype(DataType::Integer(_)) + )); + match &options[1] { + UserDefinedTypeRangeOption::SubtypeOpClass(name) => { + assert_eq!(name.to_string(), "int4_ops"); + } + _ => unreachable!("Expected SubtypeOpClass"), + } + } + _ => unreachable!(), + } + } + _ => unreachable!(), + } + + match verified_stmt( + "CREATE TYPE int4range AS RANGE (SUBTYPE = INTEGER, SUBTYPE_DIFF = int4range_subdiff)", + ) { + Statement::CreateType { + name, + representation, + } => { + assert_eq!(name.to_string(), "int4range"); + match representation { + Some(UserDefinedTypeRepresentation::Range { options }) => { + assert_eq!(options.len(), 2); + assert!(matches!( + options[0], + UserDefinedTypeRangeOption::Subtype(DataType::Integer(_)) + )); + match &options[1] { + UserDefinedTypeRangeOption::SubtypeDiff(name) => { + assert_eq!(name.to_string(), "int4range_subdiff"); + } + _ => unreachable!("Expected SubtypeDiff"), + } + } + _ => unreachable!(), + } + } + _ => unreachable!(), + } + + match verified_stmt( + "CREATE TYPE int4range AS RANGE (SUBTYPE = INTEGER, SUBTYPE_OPCLASS = int4_ops, CANONICAL = int4range_canonical, SUBTYPE_DIFF = int4range_subdiff, MULTIRANGE_TYPE_NAME = int4multirange)", + ) { + Statement::CreateType { + name, + representation, + } => { + assert_eq!(name.to_string(), "int4range"); + match representation { + Some(UserDefinedTypeRepresentation::Range { options }) => { + assert_eq!(options.len(), 5); + assert!(matches!( + options[0], + UserDefinedTypeRangeOption::Subtype(DataType::Integer(_)) + )); + assert!(matches!( + options[1], + UserDefinedTypeRangeOption::SubtypeOpClass(_) + )); + assert!(matches!( + options[2], + UserDefinedTypeRangeOption::Canonical(_) + )); + assert!(matches!( + options[3], + UserDefinedTypeRangeOption::SubtypeDiff(_) + )); + assert!(matches!( + options[4], + UserDefinedTypeRangeOption::MultirangeTypeName(_) + )); + } + _ => unreachable!(), + } + } + _ => unreachable!(), + } + + match verified_stmt( + "CREATE TYPE mytype (INPUT = in_fn, OUTPUT = out_fn, INTERNALLENGTH = 16, PASSEDBYVALUE)", + ) { + Statement::CreateType { + name, + representation, + } => { + assert_eq!(name.to_string(), "mytype"); + match representation { + Some(UserDefinedTypeRepresentation::SqlDefinition { options }) => { + assert_eq!(options.len(), 4); + assert!(matches!( + options[0], + UserDefinedTypeSqlDefinitionOption::Input(_) + )); + assert!(matches!( + options[1], + UserDefinedTypeSqlDefinitionOption::Output(_) + )); + assert!(matches!( + options[2], + UserDefinedTypeSqlDefinitionOption::InternalLength( + UserDefinedTypeInternalLength::Fixed(16) + ) + )); + assert!(matches!( + options[3], + UserDefinedTypeSqlDefinitionOption::PassedByValue + )); + } + _ => unreachable!(), + } + } + _ => unreachable!(), + } + + verified_stmt("CREATE TYPE mytype (INPUT = in_fn, OUTPUT = out_fn, INTERNALLENGTH = VARIABLE, STORAGE = extended)"); + + // Test all storage variants + for storage in ["plain", "external", "extended", "main"] { + verified_stmt(&format!( + "CREATE TYPE t (INPUT = f_in, OUTPUT = f_out, STORAGE = {storage})" + )); + } + + // Test all alignment variants + for align in ["char", "int2", "int4", "double"] { + verified_stmt(&format!( + "CREATE TYPE t (INPUT = f_in, OUTPUT = f_out, ALIGNMENT = {align})" + )); + } + + // Test additional function options (PostgreSQL-specific due to ANALYZE keyword) + pg_and_generic().verified_stmt("CREATE TYPE t (INPUT = f_in, OUTPUT = f_out, RECEIVE = f_recv, SEND = f_send, TYPMOD_IN = f_tmin, TYPMOD_OUT = f_tmout, ANALYZE = f_analyze, SUBSCRIPT = f_sub)"); + + // Test advanced options + verified_stmt("CREATE TYPE t (INPUT = f_in, OUTPUT = f_out, LIKE = INT, CATEGORY = 'N', PREFERRED = true, DEFAULT = 0, ELEMENT = INTEGER, DELIMITER = ',', COLLATABLE = false)"); } #[test] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 87cb43ed..4edab706 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6223,7 +6223,7 @@ fn parse_create_type_as_enum() { match statement { Statement::CreateType { name, - representation: UserDefinedTypeRepresentation::Enum { labels }, + representation: Some(UserDefinedTypeRepresentation::Enum { labels }), } => { assert_eq!("public.my_type", name.to_string()); assert_eq!( From 4f79997c8efa1c6089ef5795ac8317152205d8c0 Mon Sep 17 00:00:00 2001 From: xitep Date: Thu, 13 Nov 2025 10:27:58 +0100 Subject: [PATCH 8/8] Include DML keyword in statement span (#2090) --- src/ast/dml.rs | 13 ++++- src/ast/spans.rs | 111 +++++++++++++++++++++++++++++++----- src/dialect/sqlite.rs | 2 +- src/parser/mod.rs | 51 +++++++++++------ tests/sqlparser_common.rs | 2 + tests/sqlparser_mysql.rs | 1 + tests/sqlparser_postgres.rs | 3 + tests/sqlparser_sqlite.rs | 4 +- 8 files changed, 150 insertions(+), 37 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index c0bfcb19..d6009ce8 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -27,9 +27,10 @@ use sqlparser_derive::{Visit, VisitMut}; use crate::display_utils::{indented_list, Indent, SpaceOrNewline}; use super::{ - display_comma_separated, query::InputFormatClause, Assignment, Expr, FromTable, Ident, - InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, OrderByExpr, Query, SelectItem, - Setting, SqliteOnConflict, TableObject, TableWithJoins, UpdateTableFromKind, + display_comma_separated, helpers::attached_token::AttachedToken, query::InputFormatClause, + Assignment, Expr, FromTable, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, + OrderByExpr, Query, SelectItem, Setting, SqliteOnConflict, TableObject, TableWithJoins, + UpdateTableFromKind, }; /// INSERT statement. @@ -37,6 +38,8 @@ use super::{ #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Insert { + /// Token for the `INSERT` keyword (or its substitutes) + pub insert_token: AttachedToken, /// Only for Sqlite pub or: Option, /// Only for mysql @@ -179,6 +182,8 @@ impl Display for Insert { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Delete { + /// Token for the `DELETE` keyword + pub delete_token: AttachedToken, /// Multi tables delete are supported in mysql pub tables: Vec, /// FROM @@ -246,6 +251,8 @@ impl Display for Delete { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Update { + /// Token for the `UPDATE` keyword + pub update_token: AttachedToken, /// TABLE pub table: TableWithJoins, /// Column assignments diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 719e261c..3a4f1d02 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -839,6 +839,7 @@ impl Spanned for CopySource { impl Spanned for Delete { fn span(&self) -> Span { let Delete { + delete_token, tables, from, using, @@ -849,19 +850,21 @@ impl Spanned for Delete { } = self; union_spans( - tables - .iter() - .map(|i| i.span()) - .chain(core::iter::once(from.span())) - .chain( - using - .iter() - .map(|u| union_spans(u.iter().map(|i| i.span()))), - ) - .chain(selection.iter().map(|i| i.span())) - .chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))) - .chain(order_by.iter().map(|i| i.span())) - .chain(limit.iter().map(|i| i.span())), + core::iter::once(delete_token.0.span).chain( + tables + .iter() + .map(|i| i.span()) + .chain(core::iter::once(from.span())) + .chain( + using + .iter() + .map(|u| union_spans(u.iter().map(|i| i.span()))), + ) + .chain(selection.iter().map(|i| i.span())) + .chain(returning.iter().flat_map(|i| i.iter().map(|k| k.span()))) + .chain(order_by.iter().map(|i| i.span())) + .chain(limit.iter().map(|i| i.span())), + ), ) } } @@ -869,6 +872,7 @@ impl Spanned for Delete { impl Spanned for Update { fn span(&self) -> Span { let Update { + update_token, table, assignments, from, @@ -880,6 +884,7 @@ impl Spanned for Update { union_spans( core::iter::once(table.span()) + .chain(core::iter::once(update_token.0.span)) .chain(assignments.iter().map(|i| i.span())) .chain(from.iter().map(|i| i.span())) .chain(selection.iter().map(|i| i.span())) @@ -1217,6 +1222,7 @@ impl Spanned for AlterIndexOperation { impl Spanned for Insert { fn span(&self) -> Span { let Insert { + insert_token, or: _, // enum, sqlite specific ignore: _, // bool into: _, // bool @@ -1239,7 +1245,8 @@ impl Spanned for Insert { } = self; union_spans( - core::iter::once(table.span()) + core::iter::once(insert_token.0.span) + .chain(core::iter::once(table.span())) .chain(table_alias.as_ref().map(|i| i.span)) .chain(columns.iter().map(|i| i.span)) .chain(source.as_ref().map(|q| q.span())) @@ -2540,4 +2547,80 @@ ALTER TABLE users assert_eq!(stmt_span.start, (2, 13).into()); assert_eq!(stmt_span.end, (4, 11).into()); } + + #[test] + fn test_update_statement_span() { + let sql = r#"-- foo + UPDATE foo + /* bar */ + SET bar = 3 + WHERE quux > 42 ; +"#; + + let r = Parser::parse_sql(&crate::dialect::GenericDialect, sql).unwrap(); + assert_eq!(1, r.len()); + + let stmt_span = r[0].span(); + + assert_eq!(stmt_span.start, (2, 7).into()); + assert_eq!(stmt_span.end, (5, 17).into()); + } + + #[test] + fn test_insert_statement_span() { + let sql = r#" +/* foo */ INSERT INTO FOO (X, Y, Z) + SELECT 1, 2, 3 + FROM DUAL +;"#; + + let r = Parser::parse_sql(&crate::dialect::GenericDialect, sql).unwrap(); + assert_eq!(1, r.len()); + + let stmt_span = r[0].span(); + + assert_eq!(stmt_span.start, (2, 11).into()); + assert_eq!(stmt_span.end, (4, 12).into()); + } + + #[test] + fn test_replace_statement_span() { + let sql = r#" +/* foo */ REPLACE INTO + cities(name,population) +SELECT + name, + population +FROM + cities +WHERE id = 1 +;"#; + + let r = Parser::parse_sql(&crate::dialect::GenericDialect, sql).unwrap(); + assert_eq!(1, r.len()); + + dbg!(&r[0]); + + let stmt_span = r[0].span(); + + assert_eq!(stmt_span.start, (2, 11).into()); + assert_eq!(stmt_span.end, (9, 13).into()); + } + + #[test] + fn test_delete_statement_span() { + let sql = r#"-- foo + DELETE /* quux */ + FROM foo + WHERE foo.x = 42 +;"#; + + let r = Parser::parse_sql(&crate::dialect::GenericDialect, sql).unwrap(); + assert_eq!(1, r.len()); + + let stmt_span = r[0].span(); + + assert_eq!(stmt_span.start, (2, 7).into()); + assert_eq!(stmt_span.end, (4, 24).into()); + } } diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 64a8d532..ba4cb617 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -68,7 +68,7 @@ impl Dialect for SQLiteDialect { fn parse_statement(&self, parser: &mut Parser) -> Option> { if parser.parse_keyword(Keyword::REPLACE) { parser.prev_token(); - Some(parser.parse_insert()) + Some(parser.parse_insert(parser.get_current_token().clone())) } else { None } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2744a967..0b2158e6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -586,11 +586,11 @@ impl<'a> Parser<'a> { Keyword::DISCARD => self.parse_discard(), Keyword::DECLARE => self.parse_declare(), Keyword::FETCH => self.parse_fetch_statement(), - Keyword::DELETE => self.parse_delete(), - Keyword::INSERT => self.parse_insert(), - Keyword::REPLACE => self.parse_replace(), + Keyword::DELETE => self.parse_delete(next_token), + Keyword::INSERT => self.parse_insert(next_token), + Keyword::REPLACE => self.parse_replace(next_token), Keyword::UNCACHE => self.parse_uncache_table(), - Keyword::UPDATE => self.parse_update(), + Keyword::UPDATE => self.parse_update(next_token), Keyword::ALTER => self.parse_alter(), Keyword::CALL => self.parse_call(), Keyword::COPY => self.parse_copy(), @@ -11817,8 +11817,11 @@ impl<'a> Parser<'a> { /// Parse a DELETE statement, returning a `Box`ed SetExpr /// /// This is used to reduce the size of the stack frames in debug builds - fn parse_delete_setexpr_boxed(&mut self) -> Result, ParserError> { - Ok(Box::new(SetExpr::Delete(self.parse_delete()?))) + fn parse_delete_setexpr_boxed( + &mut self, + delete_token: TokenWithSpan, + ) -> Result, ParserError> { + Ok(Box::new(SetExpr::Delete(self.parse_delete(delete_token)?))) } /// Parse a MERGE statement, returning a `Box`ed SetExpr @@ -11828,7 +11831,7 @@ impl<'a> Parser<'a> { Ok(Box::new(SetExpr::Merge(self.parse_merge()?))) } - pub fn parse_delete(&mut self) -> Result { + pub fn parse_delete(&mut self, delete_token: TokenWithSpan) -> Result { let (tables, with_from_keyword) = if !self.parse_keyword(Keyword::FROM) { // `FROM` keyword is optional in BigQuery SQL. // https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#delete_statement @@ -11871,6 +11874,7 @@ impl<'a> Parser<'a> { }; Ok(Statement::Delete(Delete { + delete_token: delete_token.into(), tables, from: if with_from_keyword { FromTable::WithFromKeyword(from) @@ -12000,7 +12004,7 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::INSERT) { Ok(Query { with, - body: self.parse_insert_setexpr_boxed()?, + body: self.parse_insert_setexpr_boxed(self.get_current_token().clone())?, order_by: None, limit_clause: None, fetch: None, @@ -12014,7 +12018,7 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::UPDATE) { Ok(Query { with, - body: self.parse_update_setexpr_boxed()?, + body: self.parse_update_setexpr_boxed(self.get_current_token().clone())?, order_by: None, limit_clause: None, fetch: None, @@ -12028,7 +12032,7 @@ impl<'a> Parser<'a> { } else if self.parse_keyword(Keyword::DELETE) { Ok(Query { with, - body: self.parse_delete_setexpr_boxed()?, + body: self.parse_delete_setexpr_boxed(self.get_current_token().clone())?, limit_clause: None, order_by: None, fetch: None, @@ -15470,7 +15474,10 @@ impl<'a> Parser<'a> { } /// Parse an REPLACE statement - pub fn parse_replace(&mut self) -> Result { + pub fn parse_replace( + &mut self, + replace_token: TokenWithSpan, + ) -> Result { if !dialect_of!(self is MySqlDialect | GenericDialect) { return parser_err!( "Unsupported statement REPLACE", @@ -15478,7 +15485,7 @@ impl<'a> Parser<'a> { ); } - let mut insert = self.parse_insert()?; + let mut insert = self.parse_insert(replace_token)?; if let Statement::Insert(Insert { replace_into, .. }) = &mut insert { *replace_into = true; } @@ -15489,12 +15496,15 @@ impl<'a> Parser<'a> { /// Parse an INSERT statement, returning a `Box`ed SetExpr /// /// This is used to reduce the size of the stack frames in debug builds - fn parse_insert_setexpr_boxed(&mut self) -> Result, ParserError> { - Ok(Box::new(SetExpr::Insert(self.parse_insert()?))) + fn parse_insert_setexpr_boxed( + &mut self, + insert_token: TokenWithSpan, + ) -> Result, ParserError> { + Ok(Box::new(SetExpr::Insert(self.parse_insert(insert_token)?))) } /// Parse an INSERT statement - pub fn parse_insert(&mut self) -> Result { + pub fn parse_insert(&mut self, insert_token: TokenWithSpan) -> Result { let or = self.parse_conflict_clause(); let priority = if !dialect_of!(self is MySqlDialect | GenericDialect) { None @@ -15663,6 +15673,7 @@ impl<'a> Parser<'a> { }; Ok(Statement::Insert(Insert { + insert_token: insert_token.into(), or, table: table_object, table_alias, @@ -15754,11 +15765,14 @@ impl<'a> Parser<'a> { /// Parse an UPDATE statement, returning a `Box`ed SetExpr /// /// This is used to reduce the size of the stack frames in debug builds - fn parse_update_setexpr_boxed(&mut self) -> Result, ParserError> { - Ok(Box::new(SetExpr::Update(self.parse_update()?))) + fn parse_update_setexpr_boxed( + &mut self, + update_token: TokenWithSpan, + ) -> Result, ParserError> { + Ok(Box::new(SetExpr::Update(self.parse_update(update_token)?))) } - pub fn parse_update(&mut self) -> Result { + pub fn parse_update(&mut self, update_token: TokenWithSpan) -> Result { let or = self.parse_conflict_clause(); let table = self.parse_table_and_joins()?; let from_before_set = if self.parse_keyword(Keyword::FROM) { @@ -15793,6 +15807,7 @@ impl<'a> Parser<'a> { None }; Ok(Update { + update_token: update_token.into(), table, assignments, from, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b360f751..b06f1141 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -456,6 +456,7 @@ fn parse_update_set_from() { assert_eq!( stmt, Statement::Update(Update { + update_token: AttachedToken::empty(), table: TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("t1")])), joins: vec![], @@ -551,6 +552,7 @@ fn parse_update_with_table_alias() { returning, or: None, limit: None, + update_token: _, }) => { assert_eq!( TableWithJoins { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index b31a5b7c..bc5d48ba 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2632,6 +2632,7 @@ fn parse_update_with_joins() { returning, or: None, limit: None, + update_token: _, }) => { assert_eq!( TableWithJoins { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 4edab706..75d567c1 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5140,6 +5140,7 @@ fn test_simple_postgres_insert_with_alias() { assert_eq!( statement, Statement::Insert(Insert { + insert_token: AttachedToken::empty(), or: None, ignore: false, into: true, @@ -5210,6 +5211,7 @@ fn test_simple_postgres_insert_with_alias() { assert_eq!( statement, Statement::Insert(Insert { + insert_token: AttachedToken::empty(), or: None, ignore: false, into: true, @@ -5282,6 +5284,7 @@ fn test_simple_insert_with_quoted_alias() { assert_eq!( statement, Statement::Insert(Insert { + insert_token: AttachedToken::empty(), or: None, ignore: false, into: true, diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index f1f6cf49..321cfef0 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -22,6 +22,7 @@ #[macro_use] mod test_utils; +use sqlparser::ast::helpers::attached_token::AttachedToken; use sqlparser::keywords::Keyword; use test_utils::*; @@ -494,7 +495,8 @@ fn parse_update_tuple_row_values() { }, from: None, returning: None, - limit: None + limit: None, + update_token: AttachedToken::empty() }) ); }