From b5b8594bc0a962414b50ad37e4acba5c5c595a8d Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Sun, 8 Feb 2026 10:03:52 +0100 Subject: [PATCH 01/10] [PIVOT] Optional AS keyword for value aliases --- src/ast/query.rs | 7 +++++-- src/parser/mod.rs | 13 ++++++++++++- tests/sqlparser_common.rs | 6 ++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index b8f605be5..6d95216df 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1589,6 +1589,7 @@ pub enum TableFactor { /// /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#pivot_operator) /// [Snowflake](https://docs.snowflake.com/en/sql-reference/constructs/pivot) + /// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/SELECT.html#GUID-CFA006CA-6FF1-4972-821E-6996142A51C6__GUID-68257B27-1C4C-4C47-8140-5C60E0E65D35) Pivot { /// The input table to pivot. table: Box, @@ -1610,8 +1611,10 @@ pub enum TableFactor { /// table UNPIVOT [ { INCLUDE | EXCLUDE } NULLS ] (value FOR name IN (column1, [ column2, ... ])) [ alias ] /// ``` /// - /// See . - /// See . + /// [Snowflake](https://docs.snowflake.com/en/sql-reference/constructs/unpivot) + /// [Databricks](https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-syntax-qry-select-unpivot) + /// [BigQuery](https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#unpivot_operator) + /// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/SELECT.html#GUID-CFA006CA-6FF1-4972-821E-6996142A51C6__GUID-9B4E0389-413C-4014-94A1-0A0571BDF7E1) Unpivot { /// The input table to unpivot. table: Box, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7a2bda8ac..46d7d5d63 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -16200,6 +16200,15 @@ impl<'a> Parser<'a> { Ok(ExprWithAlias { expr, alias }) } + /// Parse an expression followed by an optional alias; Unlike + /// [Self::parse_expr_with_alias] the "AS" keyword between the expression + /// and the alias is optional. + fn parse_expr_with_alias_optional_as_keyword(&mut self) -> Result { + let expr = self.parse_expr()?; + let alias = self.parse_identifier_optional_alias()?; + Ok(ExprWithAlias { expr, alias }) + } + /// Parse a PIVOT table factor (ClickHouse/Oracle style pivot), returning a TableFactor. pub fn parse_pivot_table_factor( &mut self, @@ -16228,7 +16237,9 @@ impl<'a> Parser<'a> { } else if self.peek_sub_query() { PivotValueSource::Subquery(self.parse_query()?) } else { - PivotValueSource::List(self.parse_comma_separated(Self::parse_expr_with_alias)?) + PivotValueSource::List( + self.parse_comma_separated(Self::parse_expr_with_alias_optional_as_keyword)?, + ) }; self.expect_token(&Token::RParen)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 899dba8dd..5b5c4f153 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11357,6 +11357,12 @@ fn parse_pivot_table() { verified_stmt(multiple_value_columns_sql).to_string(), multiple_value_columns_sql ); + + // assert optional "AS" keyword for aliases for pivot values + one_statement_parses_to( + "SELECT * FROM t PIVOT(SUM(1) FOR a.abc IN (1 x, 'two' y, three z))", + "SELECT * FROM t PIVOT(SUM(1) FOR a.abc IN (1 AS x, 'two' AS y, three AS z))", + ); } #[test] From c4b7e7be892890c58fb9ea16972252d6e7ccac42 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Sun, 8 Feb 2026 17:57:03 +0100 Subject: [PATCH 02/10] [PIVOT] Optional AS keyword for aggregation function aliases --- src/parser/mod.rs | 36 ++++++++++++++++++++---------------- tests/sqlparser_common.rs | 6 ++++++ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 46d7d5d63..f67d56745 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13562,7 +13562,7 @@ impl<'a> Parser<'a> { Keyword::PIVOT => { self.expect_token(&Token::LParen)?; let aggregate_functions = - self.parse_comma_separated(Self::parse_aliased_function_call)?; + self.parse_comma_separated(Self::parse_pivot_aggregate_function)?; self.expect_keyword_is(Keyword::FOR)?; let value_column = self.parse_period_separated(|p| p.parse_identifier())?; self.expect_keyword_is(Keyword::IN)?; @@ -16153,20 +16153,6 @@ impl<'a> Parser<'a> { }) } - fn parse_aliased_function_call(&mut self) -> Result { - let function_name = match self.next_token().token { - Token::Word(w) => Ok(w.value), - _ => self.expected("a function identifier", self.peek_token()), - }?; - let expr = self.parse_function(ObjectName::from(vec![Ident::new(function_name)]))?; - let alias = if self.parse_keyword(Keyword::AS) { - Some(self.parse_identifier()?) - } else { - None - }; - - Ok(ExprWithAlias { expr, alias }) - } /// Parses an expression with an optional alias /// /// Examples: @@ -16209,13 +16195,31 @@ impl<'a> Parser<'a> { Ok(ExprWithAlias { expr, alias }) } + /// Parses a plain function call with an optional alias for the `PIVOT` clause + fn parse_pivot_aggregate_function(&mut self) -> Result { + let function_name = match self.next_token().token { + Token::Word(w) => Ok(w.value), + _ => self.expected("a function identifier", self.peek_token()), + }?; + let expr = self.parse_function(ObjectName::from(vec![Ident::new(function_name)]))?; + let alias = { + fn validator(explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { + // ~ for a PIVOT aggregate function the alias must not be a "FOR"; in any dialect + kw != &Keyword::FOR && parser.dialect.is_select_item_alias(explicit, kw, parser) + } + self.parse_optional_alias_inner(None, validator)? + }; + Ok(ExprWithAlias { expr, alias }) + } + /// Parse a PIVOT table factor (ClickHouse/Oracle style pivot), returning a TableFactor. pub fn parse_pivot_table_factor( &mut self, table: TableFactor, ) -> Result { self.expect_token(&Token::LParen)?; - let aggregate_functions = self.parse_comma_separated(Self::parse_aliased_function_call)?; + let aggregate_functions = + self.parse_comma_separated(Self::parse_pivot_aggregate_function)?; self.expect_keyword_is(Keyword::FOR)?; let value_column = if self.peek_token_ref().token == Token::LParen { self.parse_parenthesized_column_list_inner(Mandatory, false, |p| { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5b5c4f153..1e58f4030 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11363,6 +11363,12 @@ fn parse_pivot_table() { "SELECT * FROM t PIVOT(SUM(1) FOR a.abc IN (1 x, 'two' y, three z))", "SELECT * FROM t PIVOT(SUM(1) FOR a.abc IN (1 AS x, 'two' AS y, three AS z))", ); + + // assert optional "AS" keyword for aliases for pivot aggregate function + one_statement_parses_to( + "SELECT * FROM t PIVOT(SUM(1) x, COUNT(42) y FOR a.abc IN (1))", + "SELECT * FROM t PIVOT(SUM(1) AS x, COUNT(42) AS y FOR a.abc IN (1))", + ); } #[test] From 0f50545d6f32bac55b2e12db74e0cf9c60151af1 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Wed, 11 Feb 2026 12:20:32 +0100 Subject: [PATCH 03/10] [Oracle] Table alias for INSERTed table --- src/ast/dml.rs | 20 ++++++++----- src/ast/mod.rs | 11 +++++++ src/ast/spans.rs | 2 +- src/parser/mod.rs | 39 ++++++++++++++++--------- tests/sqlparser_oracle.rs | 57 ++++++++++++++++++++++++++++++++++++- tests/sqlparser_postgres.rs | 33 +++++++++++++-------- 6 files changed, 128 insertions(+), 34 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index f9c8823a2..06e939e1e 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -31,9 +31,9 @@ use crate::{ use super::{ display_comma_separated, helpers::attached_token::AttachedToken, query::InputFormatClause, - Assignment, Expr, FromTable, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, - OptimizerHint, OrderByExpr, Query, SelectInto, SelectItem, Setting, SqliteOnConflict, - TableFactor, TableObject, TableWithJoins, UpdateTableFromKind, Values, + Assignment, Expr, FromTable, Ident, InsertAliases, InsertTableAlias, MysqlInsertPriority, + ObjectName, OnInsert, OptimizerHint, OrderByExpr, Query, SelectInto, SelectItem, Setting, + SqliteOnConflict, TableFactor, TableObject, TableWithJoins, UpdateTableFromKind, Values, }; /// INSERT statement. @@ -56,8 +56,9 @@ pub struct Insert { pub into: bool, /// TABLE pub table: TableObject, - /// table_name as foo (for PostgreSQL) - pub table_alias: Option, + /// `table_name as foo` (for PostgreSQL) + /// `table_name foo` (for Oracle) + pub table_alias: Option, /// COLUMNS pub columns: Vec, /// Overwrite (Hive) @@ -125,8 +126,13 @@ pub struct Insert { impl Display for Insert { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // SQLite OR conflict has a special format: INSERT OR ... INTO table_name - let table_name = if let Some(alias) = &self.table_alias { - format!("{0} AS {alias}", self.table) + let table_name = if let Some(table_alias) = &self.table_alias { + format!( + "{table} {as_keyword}{alias}", + table = self.table, + as_keyword = if table_alias.explicit { "AS " } else { "" }, + alias = table_alias.alias + ) } else { self.table.to_string() }; diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d534b300b..bb11305d6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6456,6 +6456,17 @@ pub struct InsertAliases { pub col_aliases: Option>, } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +/// Optional alias for an `INSERT` table; i.e. the table to be inserted into +pub struct InsertTableAlias { + /// `true` if the aliases was explicitly introduced with the "AS" keyword + pub explicit: bool, + /// the alias name itself + pub alias: Ident, +} + #[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/ast/spans.rs b/src/ast/spans.rs index 74f19e831..9c6dad7e9 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1327,7 +1327,7 @@ impl Spanned for Insert { union_spans( core::iter::once(insert_token.0.span) .chain(core::iter::once(table.span())) - .chain(table_alias.as_ref().map(|i| i.span)) + .chain(table_alias.iter().map(|k| k.alias.span)) .chain(columns.iter().map(|i| i.span)) .chain(source.as_ref().map(|q| q.span())) .chain(assignments.iter().map(|i| i.span())) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6c9314d95..73e549313 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4545,7 +4545,13 @@ impl<'a> Parser<'a> { /// /// Returns true if the current token matches the expected keyword. pub fn peek_keyword(&self, expected: Keyword) -> bool { - matches!(&self.peek_token_ref().token, Token::Word(w) if expected == w.keyword) + self.peek_keyword_one_of(&[expected]) + } + + #[must_use] + /// Checks whether the current token is one of the expected keywords without consuming it. + fn peek_keyword_one_of(&self, expected: &[Keyword]) -> bool { + matches!(&self.peek_token_ref().token, Token::Word(w) if expected.contains(&w.keyword)) } /// If the current token is the `expected` keyword followed by @@ -17166,12 +17172,26 @@ impl<'a> Parser<'a> { let table = self.parse_keyword(Keyword::TABLE); let table_object = self.parse_table_object()?; - let table_alias = - if dialect_of!(self is PostgreSqlDialect) && self.parse_keyword(Keyword::AS) { - Some(self.parse_identifier()?) + let table_alias = if dialect_of!(self is OracleDialect) { + if !self.peek_sub_query() + && !self.peek_keyword_one_of(&[Keyword::DEFAULT, Keyword::VALUES]) + { + self.maybe_parse(|parser| parser.parse_identifier())? + .map(|alias| InsertTableAlias { + explicit: false, + alias, + }) } else { None - }; + } + } else if dialect_of!(self is PostgreSqlDialect) && self.parse_keyword(Keyword::AS) { + Some(InsertTableAlias { + explicit: true, + alias: self.parse_identifier()?, + }) + } else { + None + }; let is_mysql = dialect_of!(self is MySqlDialect); @@ -19412,14 +19432,7 @@ impl<'a> Parser<'a> { /// Returns true if the next keyword indicates a sub query, i.e. SELECT or WITH fn peek_sub_query(&mut self) -> bool { - if self - .parse_one_of_keywords(&[Keyword::SELECT, Keyword::WITH]) - .is_some() - { - self.prev_token(); - return true; - } - false + self.peek_keyword_one_of(&[Keyword::SELECT, Keyword::WITH]) } pub(crate) fn parse_show_stmt_options(&mut self) -> Result { diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs index 0dbccdb5e..34149dc57 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -21,7 +21,10 @@ use pretty_assertions::assert_eq; use sqlparser::{ - ast::{BinaryOperator, Expr, Ident, QuoteDelimitedString, Value, ValueWithSpan}, + ast::{ + BinaryOperator, Expr, Ident, Insert, InsertTableAlias, ObjectName, QuoteDelimitedString, + Statement, TableObject, Value, ValueWithSpan, + }, dialect::OracleDialect, parser::ParserError, tokenizer::Span, @@ -414,3 +417,55 @@ fn test_connect_by() { ORDER BY \"Employee\", \"Manager\", \"Pathlen\", \"Path\"", ); } + +#[test] +fn test_insert_with_table_alias() { + let oracle_dialect = oracle(); + + fn verify_table_name_with_alias(stmt: &Statement, exp_table_name: &str, exp_table_alias: &str) { + assert!(matches!(stmt, + Statement::Insert(Insert { + table: TableObject::TableName(table_name), + table_alias: Some(InsertTableAlias { + explicit: false, + alias: Ident { + value: table_alias, + quote_style: None, + span: _ + } + }), + .. + }) + if table_alias == exp_table_alias + && table_name == &ObjectName::from(vec![Ident { + value: exp_table_name.into(), + quote_style: None, + span: Span::empty(), + }]) + )); + } + + let stmt = oracle_dialect.verified_stmt( + "INSERT INTO foo_t t \ + SELECT 1, 2, 3 FROM dual", + ); + verify_table_name_with_alias(&stmt, "foo_t", "t"); + + let stmt = oracle_dialect.verified_stmt( + "INSERT INTO foo_t asdf (a, b, c) \ + SELECT 1, 2, 3 FROM dual", + ); + verify_table_name_with_alias(&stmt, "foo_t", "asdf"); + + let stmt = oracle_dialect.verified_stmt( + "INSERT INTO foo_t t (a, b, c) \ + VALUES (1, 2, 3)", + ); + verify_table_name_with_alias(&stmt, "foo_t", "t"); + + let stmt = oracle_dialect.verified_stmt( + "INSERT INTO foo_t t \ + VALUES (1, 2, 3)", + ); + verify_table_name_with_alias(&stmt, "foo_t", "t"); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d79e2b833..ed1cd74e9 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5445,10 +5445,13 @@ fn test_simple_postgres_insert_with_alias() { quote_style: None, span: Span::empty(), }])), - table_alias: Some(Ident { - value: "test_table".to_string(), - quote_style: None, - span: Span::empty(), + table_alias: Some(InsertTableAlias { + explicit: true, + alias: Ident { + value: "test_table".to_string(), + quote_style: None, + span: Span::empty(), + } }), columns: vec![ Ident { @@ -5521,10 +5524,13 @@ fn test_simple_postgres_insert_with_alias() { quote_style: None, span: Span::empty(), }])), - table_alias: Some(Ident { - value: "test_table".to_string(), - quote_style: None, - span: Span::empty(), + table_alias: Some(InsertTableAlias { + explicit: true, + alias: Ident { + value: "test_table".to_string(), + quote_style: None, + span: Span::empty(), + } }), columns: vec![ Ident { @@ -5599,10 +5605,13 @@ fn test_simple_insert_with_quoted_alias() { quote_style: None, span: Span::empty(), }])), - table_alias: Some(Ident { - value: "Test_Table".to_string(), - quote_style: Some('"'), - span: Span::empty(), + table_alias: Some(InsertTableAlias { + explicit: true, + alias: Ident { + value: "Test_Table".to_string(), + quote_style: Some('"'), + span: Span::empty(), + } }), columns: vec![ Ident { From e9b8fb665d907aae6f8d46fbd2b1c47b01da0cef Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Thu, 19 Feb 2026 14:23:57 +0100 Subject: [PATCH 04/10] Avoid introducing new method --- src/parser/mod.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 73e549313..909b4932e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4545,13 +4545,7 @@ impl<'a> Parser<'a> { /// /// Returns true if the current token matches the expected keyword. pub fn peek_keyword(&self, expected: Keyword) -> bool { - self.peek_keyword_one_of(&[expected]) - } - - #[must_use] - /// Checks whether the current token is one of the expected keywords without consuming it. - fn peek_keyword_one_of(&self, expected: &[Keyword]) -> bool { - matches!(&self.peek_token_ref().token, Token::Word(w) if expected.contains(&w.keyword)) + matches!(&self.peek_token_ref().token, Token::Word(w) if expected == w.keyword) } /// If the current token is the `expected` keyword followed by @@ -17174,7 +17168,7 @@ impl<'a> Parser<'a> { let table_alias = if dialect_of!(self is OracleDialect) { if !self.peek_sub_query() - && !self.peek_keyword_one_of(&[Keyword::DEFAULT, Keyword::VALUES]) + && self.peek_one_of_keywords(&[Keyword::DEFAULT, Keyword::VALUES]).is_none() { self.maybe_parse(|parser| parser.parse_identifier())? .map(|alias| InsertTableAlias { @@ -19432,7 +19426,7 @@ impl<'a> Parser<'a> { /// Returns true if the next keyword indicates a sub query, i.e. SELECT or WITH fn peek_sub_query(&mut self) -> bool { - self.peek_keyword_one_of(&[Keyword::SELECT, Keyword::WITH]) + self.peek_one_of_keywords(&[Keyword::SELECT, Keyword::WITH]).is_some() } pub(crate) fn parse_show_stmt_options(&mut self) -> Result { From b35094a9eaf9f397dcffd4d4a2f93a16dc3c868b Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Thu, 19 Feb 2026 14:43:11 +0100 Subject: [PATCH 05/10] Raneme InsertTableAlias to TableAliasWithoutColumns --- src/ast/dml.rs | 7 ++----- src/ast/mod.rs | 2 +- src/parser/mod.rs | 4 ++-- tests/sqlparser_oracle.rs | 4 ++-- tests/sqlparser_postgres.rs | 4 ++-- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 06e939e1e..d90ee9d81 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -30,10 +30,7 @@ use crate::{ }; use super::{ - display_comma_separated, helpers::attached_token::AttachedToken, query::InputFormatClause, - Assignment, Expr, FromTable, Ident, InsertAliases, InsertTableAlias, MysqlInsertPriority, - ObjectName, OnInsert, OptimizerHint, OrderByExpr, Query, SelectInto, SelectItem, Setting, - SqliteOnConflict, TableFactor, TableObject, TableWithJoins, UpdateTableFromKind, Values, + Assignment, Expr, FromTable, Ident, InsertAliases, TableAliasWithoutColumns, MysqlInsertPriority, ObjectName, OnInsert, OptimizerHint, OrderByExpr, Query, SelectInto, SelectItem, Setting, SqliteOnConflict, TableFactor, TableObject, TableWithJoins, UpdateTableFromKind, Values, display_comma_separated, helpers::attached_token::AttachedToken, query::InputFormatClause }; /// INSERT statement. @@ -58,7 +55,7 @@ pub struct Insert { pub table: TableObject, /// `table_name as foo` (for PostgreSQL) /// `table_name foo` (for Oracle) - pub table_alias: Option, + pub table_alias: Option, /// COLUMNS pub columns: Vec, /// Overwrite (Hive) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index bb11305d6..1f2f39b46 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -6460,7 +6460,7 @@ pub struct InsertAliases { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] /// Optional alias for an `INSERT` table; i.e. the table to be inserted into -pub struct InsertTableAlias { +pub struct TableAliasWithoutColumns { /// `true` if the aliases was explicitly introduced with the "AS" keyword pub explicit: bool, /// the alias name itself diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 909b4932e..5f61cd1ea 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17171,7 +17171,7 @@ impl<'a> Parser<'a> { && self.peek_one_of_keywords(&[Keyword::DEFAULT, Keyword::VALUES]).is_none() { self.maybe_parse(|parser| parser.parse_identifier())? - .map(|alias| InsertTableAlias { + .map(|alias| TableAliasWithoutColumns { explicit: false, alias, }) @@ -17179,7 +17179,7 @@ impl<'a> Parser<'a> { None } } else if dialect_of!(self is PostgreSqlDialect) && self.parse_keyword(Keyword::AS) { - Some(InsertTableAlias { + Some(TableAliasWithoutColumns { explicit: true, alias: self.parse_identifier()?, }) diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs index 34149dc57..c318d83cc 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -22,7 +22,7 @@ use pretty_assertions::assert_eq; use sqlparser::{ ast::{ - BinaryOperator, Expr, Ident, Insert, InsertTableAlias, ObjectName, QuoteDelimitedString, + BinaryOperator, Expr, Ident, Insert, TableAliasWithoutColumns, ObjectName, QuoteDelimitedString, Statement, TableObject, Value, ValueWithSpan, }, dialect::OracleDialect, @@ -426,7 +426,7 @@ fn test_insert_with_table_alias() { assert!(matches!(stmt, Statement::Insert(Insert { table: TableObject::TableName(table_name), - table_alias: Some(InsertTableAlias { + table_alias: Some(TableAliasWithoutColumns { explicit: false, alias: Ident { value: table_alias, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index ed1cd74e9..13f9087c6 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5445,7 +5445,7 @@ fn test_simple_postgres_insert_with_alias() { quote_style: None, span: Span::empty(), }])), - table_alias: Some(InsertTableAlias { + table_alias: Some(TableAliasWithoutColumns { explicit: true, alias: Ident { value: "test_table".to_string(), @@ -5605,7 +5605,7 @@ fn test_simple_insert_with_quoted_alias() { quote_style: None, span: Span::empty(), }])), - table_alias: Some(InsertTableAlias { + table_alias: Some(TableAliasWithoutColumns { explicit: true, alias: Ident { value: "Test_Table".to_string(), From 83aa69a604ae6fc784206c1e026b1489b06989eb Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Thu, 19 Feb 2026 16:25:21 +0100 Subject: [PATCH 06/10] Test coverage --- tests/sqlparser_oracle.rs | 56 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs index c318d83cc..1445c4af4 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -22,8 +22,8 @@ use pretty_assertions::assert_eq; use sqlparser::{ ast::{ - BinaryOperator, Expr, Ident, Insert, TableAliasWithoutColumns, ObjectName, QuoteDelimitedString, - Statement, TableObject, Value, ValueWithSpan, + BinaryOperator, Expr, Ident, Insert, ObjectName, Query, QuoteDelimitedString, SetExpr, + Statement, TableAliasWithoutColumns, TableObject, Value, ValueWithSpan, }, dialect::OracleDialect, parser::ParserError, @@ -469,3 +469,55 @@ fn test_insert_with_table_alias() { ); verify_table_name_with_alias(&stmt, "foo_t", "t"); } + +#[test] +fn test_insert_without_alias() { + let oracle_dialect = oracle(); + + // check DEFAULT + let sql = "INSERT INTO t default SELECT 'a' FROM dual"; + assert_eq!( + oracle_dialect.parse_sql_statements(sql), + Err(ParserError::ParserError( + "Expected: SELECT, VALUES, or a subquery in the query body, found: default".into() + )) + ); + + // check SELECT + let sql = "INSERT INTO t SELECT 'a' FROM dual"; + let stmt = oracle_dialect.verified_stmt(sql); + assert!(matches!( + &stmt, + Statement::Insert(Insert { + table_alias: None, + source: Some(source), + .. + }) + if matches!(&**source, Query { body, .. } if matches!(&**body, SetExpr::Select(_))))); + + // check WITH + let sql = "INSERT INTO dual WITH w AS (SELECT 1 AS y FROM dual) SELECT y FROM w"; + let stmt = oracle_dialect.verified_stmt(sql); + assert!(matches!( + &stmt, + Statement::Insert(Insert { + table_alias: None, + source: Some(source), + .. + }) + if matches!(&**source, Query { body, .. } if matches!(&**body, SetExpr::Select(_))))); + + // check VALUES + let sql = "INSERT INTO t VALUES (1)"; + let stmt = oracle_dialect.verified_stmt(sql); + dbg!(&stmt); + assert!(matches!( + stmt, + Statement::Insert(Insert { + table_alias: None, + source: Some(source), + .. + }) + if matches!(&*source, Query { body, .. } if matches!(&**body, SetExpr::Values(_))) + )); +} From 99762f839cf214a9641574036034ed55d7343e58 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Thu, 19 Feb 2026 17:08:29 +0100 Subject: [PATCH 07/10] Guard insert table aliases with an explicit dialect flag --- src/dialect/mod.rs | 11 +++++++++++ src/dialect/oracle.rs | 7 +++++++ src/dialect/postgresql.rs | 6 ++++++ src/parser/mod.rs | 12 ++++-------- tests/sqlparser_oracle.rs | 9 +++++++++ 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 6e374d3d8..cc83d6762 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1238,6 +1238,17 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if this dialect supports `INSERT INTO t AS alias ...`. + fn supports_insert_table_explicit_alias(&self) -> bool { + false + } + + /// Returns true if this dialect supports `INSERT INTO t alias ...` with + /// `alias` _not_ preceded by the "AS" keyword. + fn supports_insert_table_implicit_alias(&self) -> bool { + false + } + /// Returns true if this dialect supports `SET` statements without an explicit /// assignment operator such as `=`. For example: `SET SHOWPLAN_XML ON`. fn supports_set_stmt_without_operator(&self) -> bool { diff --git a/src/dialect/oracle.rs b/src/dialect/oracle.rs index deb7beacb..4c26600a4 100644 --- a/src/dialect/oracle.rs +++ b/src/dialect/oracle.rs @@ -110,4 +110,11 @@ impl Dialect for OracleDialect { fn supports_comment_optimizer_hint(&self) -> bool { true } + + /// Supports insert table aliases (but with no preceding "AS" keyword) + /// + /// See + fn supports_insert_table_implicit_alias(&self) -> bool { + true + } } diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 8e4d78a44..b363f4f88 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -288,4 +288,10 @@ impl Dialect for PostgreSqlDialect { fn supports_interval_options(&self) -> bool { true } + + /// [Postgres] support insert table aliases with an explicit "AS" keyword. + /// See: + fn supports_insert_table_explicit_alias(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5f61cd1ea..9cb272b77 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17166,19 +17166,15 @@ impl<'a> Parser<'a> { let table = self.parse_keyword(Keyword::TABLE); let table_object = self.parse_table_object()?; - let table_alias = if dialect_of!(self is OracleDialect) { - if !self.peek_sub_query() - && self.peek_one_of_keywords(&[Keyword::DEFAULT, Keyword::VALUES]).is_none() - { + let table_alias = if self.dialect.supports_insert_table_implicit_alias() + && !self.peek_sub_query() + && self.peek_one_of_keywords(&[Keyword::AS, Keyword::DEFAULT, Keyword::VALUES]).is_none() { self.maybe_parse(|parser| parser.parse_identifier())? .map(|alias| TableAliasWithoutColumns { explicit: false, alias, }) - } else { - None - } - } else if dialect_of!(self is PostgreSqlDialect) && self.parse_keyword(Keyword::AS) { + } else if self.dialect.supports_insert_table_explicit_alias() && self.parse_keyword(Keyword::AS) { Some(TableAliasWithoutColumns { explicit: true, alias: self.parse_identifier()?, diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs index 1445c4af4..a416ed748 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -483,6 +483,15 @@ fn test_insert_without_alias() { )) ); + // check AS + let sql = "INSERT INTO AS t default SELECT 'a' FROM dual"; + assert_eq!( + oracle_dialect.parse_sql_statements(sql), + Err(ParserError::ParserError( + "Expected: SELECT, VALUES, or a subquery in the query body, found: default".into() + )) + ); + // check SELECT let sql = "INSERT INTO t SELECT 'a' FROM dual"; let stmt = oracle_dialect.verified_stmt(sql); From 0f0334be082cb9c4ed89f44c0366db2d14617fe4 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Thu, 19 Feb 2026 17:09:14 +0100 Subject: [PATCH 08/10] Cargo fmt --- src/ast/dml.rs | 6 +++++- src/parser/mod.rs | 22 ++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index d90ee9d81..ad300c61c 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -30,7 +30,11 @@ use crate::{ }; use super::{ - Assignment, Expr, FromTable, Ident, InsertAliases, TableAliasWithoutColumns, MysqlInsertPriority, ObjectName, OnInsert, OptimizerHint, OrderByExpr, Query, SelectInto, SelectItem, Setting, SqliteOnConflict, TableFactor, TableObject, TableWithJoins, UpdateTableFromKind, Values, display_comma_separated, helpers::attached_token::AttachedToken, query::InputFormatClause + display_comma_separated, helpers::attached_token::AttachedToken, query::InputFormatClause, + Assignment, Expr, FromTable, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, + OptimizerHint, OrderByExpr, Query, SelectInto, SelectItem, Setting, SqliteOnConflict, + TableAliasWithoutColumns, TableFactor, TableObject, TableWithJoins, UpdateTableFromKind, + Values, }; /// INSERT statement. diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9cb272b77..c3ddc4ddf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17168,13 +17168,18 @@ impl<'a> Parser<'a> { let table_alias = if self.dialect.supports_insert_table_implicit_alias() && !self.peek_sub_query() - && self.peek_one_of_keywords(&[Keyword::AS, Keyword::DEFAULT, Keyword::VALUES]).is_none() { - self.maybe_parse(|parser| parser.parse_identifier())? - .map(|alias| TableAliasWithoutColumns { - explicit: false, - alias, - }) - } else if self.dialect.supports_insert_table_explicit_alias() && self.parse_keyword(Keyword::AS) { + && self + .peek_one_of_keywords(&[Keyword::AS, Keyword::DEFAULT, Keyword::VALUES]) + .is_none() + { + self.maybe_parse(|parser| parser.parse_identifier())? + .map(|alias| TableAliasWithoutColumns { + explicit: false, + alias, + }) + } else if self.dialect.supports_insert_table_explicit_alias() + && self.parse_keyword(Keyword::AS) + { Some(TableAliasWithoutColumns { explicit: true, alias: self.parse_identifier()?, @@ -19422,7 +19427,8 @@ impl<'a> Parser<'a> { /// Returns true if the next keyword indicates a sub query, i.e. SELECT or WITH fn peek_sub_query(&mut self) -> bool { - self.peek_one_of_keywords(&[Keyword::SELECT, Keyword::WITH]).is_some() + self.peek_one_of_keywords(&[Keyword::SELECT, Keyword::WITH]) + .is_some() } pub(crate) fn parse_show_stmt_options(&mut self) -> Result { From f67c5c0f7ac9becca98844b8856ec6a92ec06621 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Thu, 19 Feb 2026 17:16:54 +0100 Subject: [PATCH 09/10] Fix compilation error --- tests/sqlparser_postgres.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 13f9087c6..559adf5dd 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5524,7 +5524,7 @@ fn test_simple_postgres_insert_with_alias() { quote_style: None, span: Span::empty(), }])), - table_alias: Some(InsertTableAlias { + table_alias: Some(TableAliasWithoutColumns { explicit: true, alias: Ident { value: "test_table".to_string(), From db57637490e4f80072f6bc46e383e4a6d51b7b49 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Thu, 19 Feb 2026 17:30:15 +0100 Subject: [PATCH 10/10] Fix rustdoc --- src/dialect/postgresql.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index b363f4f88..a97b013e7 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -289,8 +289,9 @@ impl Dialect for PostgreSqlDialect { true } - /// [Postgres] support insert table aliases with an explicit "AS" keyword. - /// See: + /// [Postgres] supports insert table aliases with an explicit "AS" keyword. + /// + /// [Postgres]: https://www.postgresql.org/docs/17/sql-insert.html fn supports_insert_table_explicit_alias(&self) -> bool { true }