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}"
+ );
+ }
+}