diff --git a/src/ast/dml.rs b/src/ast/dml.rs index f9c8823a2..ad300c61c 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -33,7 +33,8 @@ 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, + TableAliasWithoutColumns, TableFactor, TableObject, TableWithJoins, UpdateTableFromKind, + Values, }; /// INSERT statement. @@ -56,8 +57,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 +127,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..1f2f39b46 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 TableAliasWithoutColumns { + /// `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/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..a97b013e7 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -288,4 +288,11 @@ impl Dialect for PostgreSqlDialect { fn supports_interval_options(&self) -> bool { true } + + /// [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 + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6c9314d95..c3ddc4ddf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17166,12 +17166,27 @@ 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()?) - } else { - 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 if self.dialect.supports_insert_table_explicit_alias() + && self.parse_keyword(Keyword::AS) + { + Some(TableAliasWithoutColumns { + explicit: true, + alias: self.parse_identifier()?, + }) + } else { + None + }; let is_mysql = dialect_of!(self is MySqlDialect); @@ -19412,14 +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 { - if self - .parse_one_of_keywords(&[Keyword::SELECT, Keyword::WITH]) + self.peek_one_of_keywords(&[Keyword::SELECT, Keyword::WITH]) .is_some() - { - self.prev_token(); - return true; - } - false } pub(crate) fn parse_show_stmt_options(&mut self) -> Result { diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs index 0dbccdb5e..a416ed748 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, ObjectName, Query, QuoteDelimitedString, SetExpr, + Statement, TableAliasWithoutColumns, TableObject, Value, ValueWithSpan, + }, dialect::OracleDialect, parser::ParserError, tokenizer::Span, @@ -414,3 +417,116 @@ 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(TableAliasWithoutColumns { + 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"); +} + +#[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 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); + 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(_))) + )); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d79e2b833..559adf5dd 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(TableAliasWithoutColumns { + 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(TableAliasWithoutColumns { + 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(TableAliasWithoutColumns { + explicit: true, + alias: Ident { + value: "Test_Table".to_string(), + quote_style: Some('"'), + span: Span::empty(), + } }), columns: vec![ Ident {