diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e494553ce..d19624093 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -92,24 +92,24 @@ pub use self::dml::{ pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ AfterMatchSkip, ConnectByKind, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, - ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, ExprWithAliasAndOrderBy, Fetch, ForClause, - ForJson, ForXml, FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, - IlikeSelectItem, InputFormatClause, Interpolate, InterpolateExpr, Join, JoinConstraint, - JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, JsonTableNamedColumn, - JsonTableNestedColumn, LateralView, LimitClause, LockClause, LockType, MatchRecognizePattern, - MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, - OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, - OrderBySort, PipeOperator, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, - RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, - SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SelectModifiers, - SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, - TableAliasColumnDef, TableFactor, TableFunctionArgs, TableIndexHintForClause, - TableIndexHintType, TableIndexHints, TableIndexType, TableSample, TableSampleBucket, - TableSampleKind, TableSampleMethod, TableSampleModifier, TableSampleQuantity, TableSampleSeed, - TableSampleSeedModifier, TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity, - UpdateTableFromKind, ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, - XmlNamespaceDefinition, XmlPassingArgument, XmlPassingClause, XmlTableColumn, - XmlTableColumnOption, + ExceptSelectItem, ExcludeSelectItem, ExplicitTable, ExprWithAlias, ExprWithAliasAndOrderBy, + Fetch, ForClause, ForJson, ForXml, FormatClause, GroupByExpr, GroupByWithModifier, + IdentWithAlias, IlikeSelectItem, InheritanceModifier, InputFormatClause, Interpolate, + InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonTableColumn, + JsonTableColumnErrorHandling, JsonTableNamedColumn, JsonTableNestedColumn, LateralView, + LimitClause, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, + NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OpenJsonTableColumn, + OrderBy, OrderByExpr, OrderByKind, OrderByOptions, OrderBySort, PipeOperator, PivotValueSource, + ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, + ReplaceSelectItem, RowsPerMatch, Select, SelectFlavor, SelectInto, SelectItem, + SelectItemQualifiedWildcardKind, SelectModifiers, SetExpr, SetOperator, SetQuantifier, Setting, + SymbolDefinition, TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs, + TableIndexHintForClause, TableIndexHintType, TableIndexHints, TableIndexType, TableSample, + TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier, + TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion, + TableWithJoins, Top, TopQuantity, UpdateTableFromKind, ValueTableMode, Values, + WildcardAdditionalOptions, With, WithFill, XmlNamespaceDefinition, XmlPassingArgument, + XmlPassingClause, XmlTableColumn, XmlTableColumnOption, }; pub use self::trigger::{ diff --git a/src/ast/query.rs b/src/ast/query.rs index eb209228e..359805b4d 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -176,7 +176,7 @@ pub enum SetExpr { /// `MERGE` statement Merge(Statement), /// `TABLE` command - Table(Box), + Table(Box), } impl SetExpr { @@ -293,28 +293,49 @@ impl fmt::Display for SetQuantifier { } } +/// SQL:2016 ``: `TABLE `, shorthand for `SELECT * FROM `. +/// Postgres extends with `ONLY` and trailing `*`; see [`InheritanceModifier`]. +/// +/// +/// #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -/// A [`TABLE` command]( https://www.postgresql.org/docs/current/sql-select.html#SQL-TABLE) #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -/// A (possibly schema-qualified) table reference used in `FROM` clauses. -pub struct Table { - /// Optional table name (absent for e.g. `TABLE` command without argument). - pub table_name: Option, - /// Optional schema/catalog name qualifying the table. - pub schema_name: Option, +pub struct ExplicitTable { + /// The (possibly schema-qualified) table name. + pub name: ObjectName, + /// Postgres inheritance modifier (`ONLY` or trailing `*`), if present. + pub inheritance: InheritanceModifier, +} + +/// Postgres inheritance-hierarchy modifier for table references. +/// +/// Controls whether a query against a table includes rows from descendant +/// tables (inheritance children or partitions). See +/// . +#[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 InheritanceModifier { + /// No modifier — default behavior (descendants included). + None, + /// `ONLY` prefix — exclude rows from descendant tables. + Only, + /// Trailing `*` — explicitly include descendant rows. Same effect as + /// `None`, preserved as a distinct variant so round-tripping echoes + /// what the user wrote. + IncludeDescendants, } -impl fmt::Display for Table { +impl fmt::Display for ExplicitTable { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(ref table_name) = self.table_name { - if let Some(ref schema_name) = self.schema_name { - write!(f, "TABLE {}.{}", schema_name, table_name,)?; - } else { - write!(f, "TABLE {}", table_name)?; - } - } else { - write!(f, "TABLE")?; + f.write_str("TABLE ")?; + if self.inheritance == InheritanceModifier::Only { + f.write_str("ONLY ")?; + } + write!(f, "{}", self.name)?; + if self.inheritance == InheritanceModifier::IncludeDescendants { + f.write_str(" *")?; } Ok(()) } diff --git a/src/dialect/ansi.rs b/src/dialect/ansi.rs index 89c8a9ea2..d2f708e3c 100644 --- a/src/dialect/ansi.rs +++ b/src/dialect/ansi.rs @@ -39,4 +39,9 @@ impl Dialect for AnsiDialect { fn supports_nested_comments(&self) -> bool { true } + + /// SQL:2016 ``. + fn supports_table_command(&self) -> bool { + true + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 25f57e3d1..14a2efadd 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -312,4 +312,12 @@ impl Dialect for GenericDialect { fn supports_xml_expressions(&self) -> bool { true } + + fn supports_table_command(&self) -> bool { + true + } + + fn supports_explicit_table_inheritance_modifiers(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 9b2ede40d..20265f718 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1398,6 +1398,20 @@ pub trait Dialect: Debug + Any { fn supports_array_typedef_with_brackets(&self) -> bool { false } + + /// Returns true if the dialect supports the `TABLE` command + /// (SQL:2016 ``). See [`ExplicitTable`]. + fn supports_table_command(&self) -> bool { + false + } + + /// Returns true if the dialect supports Postgres inheritance modifiers + /// (`ONLY` prefix and trailing `*`) on the `TABLE` command. + /// See [`InheritanceModifier`]. + fn supports_explicit_table_inheritance_modifiers(&self) -> bool { + false + } + /// Returns true if the dialect supports geometric types. /// /// Postgres: diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index c40d6d674..033437024 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -330,4 +330,12 @@ impl Dialect for PostgreSqlDialect { fn supports_comment_optimizer_hint(&self) -> bool { true } + + fn supports_table_command(&self) -> bool { + true + } + + fn supports_explicit_table_inheritance_modifiers(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e0c2dc269..5ab906fd8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -528,7 +528,7 @@ impl<'a> Parser<'a> { } /// Convenience method to parse a string with one or more SQL - /// statements into produce an Abstract Syntax Tree (AST). + /// statements to produce an Abstract Syntax Tree (AST). /// /// Example /// ``` @@ -623,6 +623,10 @@ impl<'a> Parser<'a> { self.prev_token(); self.parse_query().map(Into::into) } + Keyword::TABLE if self.dialect.supports_table_command() => { + self.prev_token(); + self.parse_query().map(Into::into) + } Keyword::TRUNCATE => self.parse_truncate().map(Into::into), Keyword::ATTACH => { if dialect_of!(self is DuckDbDialect) { @@ -14672,8 +14676,8 @@ impl<'a> Parser<'a> { } 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 if self.dialect.supports_table_command() && self.parse_keyword(Keyword::TABLE) { + SetExpr::Table(Box::new(self.parse_explicit_table()?)) } else { return self.expected_ref( "SELECT, VALUES, or a subquery in the query body", @@ -15177,49 +15181,31 @@ impl<'a> Parser<'a> { Ok(clauses) } - /// Parse `CREATE TABLE x AS TABLE y` - pub fn parse_as_table(&mut self) -> Result { - let token1 = self.next_token(); - let token2 = self.next_token(); - let token3 = self.next_token(); + /// Parse the body of a TABLE query expression. + /// Called after the `TABLE` keyword has been consumed. + pub fn parse_explicit_table(&mut self) -> Result { + let allow_inheritance = self.dialect.supports_explicit_table_inheritance_modifiers(); - let table_name; - let schema_name; - if token2 == Token::Period { - match token1.token { - Token::Word(w) => { - schema_name = w.value; - } - _ => { - return self.expected("Schema name", token1); - } - } - match token3.token { - Token::Word(w) => { - table_name = w.value; - } - _ => { - return self.expected("Table name", token3); - } - } - Ok(Table { - table_name: Some(table_name), - schema_name: Some(schema_name), - }) - } else { - match token1.token { - Token::Word(w) => { - table_name = w.value; - } - _ => { - return self.expected("Table name", token1); - } - } - Ok(Table { - table_name: Some(table_name), - schema_name: None, - }) + let has_only = allow_inheritance && self.parse_keyword(Keyword::ONLY); + let parenthesized = has_only && self.consume_token(&Token::LParen); + + let name = self.parse_object_name(true)?; + + if parenthesized { + self.expect_token(&Token::RParen)?; } + + let has_star = allow_inheritance && !has_only && self.consume_token(&Token::Mul); + + let inheritance = if has_only { + InheritanceModifier::Only + } else if has_star { + InheritanceModifier::IncludeDescendants + } else { + InheritanceModifier::None + }; + + Ok(ExplicitTable { name, inheritance }) } /// Parse a `SET ROLE` statement. Expects SET to be consumed already. diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d12b6a444..e236fe5b5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4590,13 +4590,15 @@ fn parse_create_table_as() { #[test] fn parse_create_table_as_table() { + let dialects = all_dialects_where(|d| d.supports_table_command()); + let sql1 = "CREATE TABLE new_table AS TABLE old_table"; let expected_query1 = Box::new(Query { with: None, - body: Box::new(SetExpr::Table(Box::new(Table { - table_name: Some("old_table".to_string()), - schema_name: None, + body: Box::new(SetExpr::Table(Box::new(ExplicitTable { + name: ObjectName::from(vec![Ident::new("old_table")]), + inheritance: InheritanceModifier::None, }))), order_by: None, limit_clause: None, @@ -4608,7 +4610,7 @@ fn parse_create_table_as_table() { pipe_operators: vec![], }); - match verified_stmt(sql1) { + match dialects.verified_stmt(sql1) { Statement::CreateTable(CreateTable { query, name, .. }) => { assert_eq!(name, ObjectName::from(vec![Ident::new("new_table")])); assert_eq!(query.unwrap(), expected_query1); @@ -4620,9 +4622,9 @@ fn parse_create_table_as_table() { let expected_query2 = Box::new(Query { with: None, - body: Box::new(SetExpr::Table(Box::new(Table { - table_name: Some("old_table".to_string()), - schema_name: Some("schema_name".to_string()), + body: Box::new(SetExpr::Table(Box::new(ExplicitTable { + name: ObjectName::from(vec![Ident::new("schema_name"), Ident::new("old_table")]), + inheritance: InheritanceModifier::None, }))), order_by: None, limit_clause: None, @@ -4634,7 +4636,7 @@ fn parse_create_table_as_table() { pipe_operators: vec![], }); - match verified_stmt(sql2) { + match dialects.verified_stmt(sql2) { Statement::CreateTable(CreateTable { query, name, .. }) => { assert_eq!(name, ObjectName::from(vec![Ident::new("new_table")])); assert_eq!(query.unwrap(), expected_query2); @@ -4643,6 +4645,42 @@ fn parse_create_table_as_table() { } } +#[test] +fn parse_table_command() { + let dialects = all_dialects_where(|d| d.supports_table_command()); + + // Top-level. + dialects.verified_stmt("TABLE films"); + // Schema-qualified. + dialects.verified_stmt("TABLE myschema.films"); + // Database-qualified (three-part name). + dialects.verified_stmt("TABLE mydb.myschema.films"); + // Quoted identifiers — round-trip preserves quoting and case. + dialects.verified_stmt("TABLE \"MyTable\""); + dialects.verified_stmt("TABLE \"My Schema\".\"My Table\""); + // ORDER BY + LIMIT + OFFSET. + dialects.verified_stmt("TABLE films ORDER BY did LIMIT 10 OFFSET 2"); + // FETCH. + dialects.verified_stmt("TABLE films FETCH FIRST 3 ROWS ONLY"); + // UNION of TABLE commands. + dialects.verified_stmt("TABLE a UNION TABLE b"); + // INTERSECT, EXCEPT. + dialects.verified_stmt("TABLE a INTERSECT TABLE b"); + dialects.verified_stmt("TABLE a EXCEPT TABLE b"); + // Mixed with SELECT. + dialects.verified_stmt("TABLE a UNION ALL SELECT * FROM b"); + // CTE body is TABLE. + dialects.verified_stmt("WITH x AS (TABLE films) SELECT * FROM x"); + // WITH prefix + TABLE at top level. + dialects.verified_stmt("WITH x AS (SELECT 1) TABLE films"); + // Derived table. + dialects.verified_stmt("SELECT * FROM (TABLE films) AS x"); + // FOR locking clauses. + dialects.verified_stmt("TABLE films FOR UPDATE"); + dialects.verified_stmt("TABLE films FOR SHARE"); + dialects.verified_stmt("TABLE films FOR UPDATE OF films"); +} + #[test] fn parse_create_table_on_cluster() { let generic = TestedDialects::new(vec![Box::new(GenericDialect {})]); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 274988be0..047ea0679 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9243,3 +9243,43 @@ fn parse_lock_table() { } } } + +#[test] +fn parse_table_command_inheritance_modifiers() { + // ONLY (exclude descendants). + pg().verified_stmt("TABLE ONLY films"); + pg().verified_stmt("TABLE ONLY myschema.films"); + // Trailing * (explicitly include descendants). + pg().verified_stmt("TABLE films *"); + pg().verified_stmt("TABLE myschema.films *"); + // Parenthesized ONLY — SQL-standard form, canonicalized to parens-less. + pg().one_statement_parses_to("TABLE ONLY (films)", "TABLE ONLY films"); + // Inheritance modifiers are usable in all contexts the TABLE command is. + pg().verified_stmt("CREATE TABLE new_t AS TABLE ONLY old_t"); + pg().verified_stmt("CREATE TABLE new_t AS TABLE old_t *"); + pg().verified_stmt("CREATE VIEW v AS TABLE ONLY films"); + pg().verified_stmt("SELECT * FROM (TABLE ONLY films) AS x"); +} + +#[test] +fn parse_table_command_rejects_invalid() { + // Inheritance modifiers are mutually exclusive. + assert!( + pg().parse_sql_statements("TABLE ONLY films *").is_err(), + "expected PG parser to reject: TABLE ONLY films *" + ); + + for sql in [ + "TABLE films WHERE x > 0", + "TABLE films GROUP BY x", + "TABLE films HAVING count(*) > 1", + "TABLE films WINDOW w AS ()", + "TABLE films (a, b)", + "TABLE films AS f", + ] { + assert!( + pg().parse_sql_statements(sql).is_err(), + "expected PG parser to reject: {sql}" + ); + } +}