From 808f0ce0e20314a84497c99dc47d4dd38c19bbc7 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Thu, 29 Oct 2026 15:58:38 +0100 Subject: [PATCH 1/2] PostgreSQL: Support ONLY and * table inheritance markers --- .../postgres_table_inheritance_markers.sql | 3 + src/ast/mod.rs | 4 +- src/ast/query.rs | 30 ++++- src/ast/spans.rs | 1 + src/dialect/generic.rs | 4 + src/dialect/mod.rs | 14 +++ src/dialect/postgresql.rs | 5 + src/parser/mod.rs | 19 +++ src/test_utils.rs | 3 + tests/sqlparser_bigquery.rs | 3 + tests/sqlparser_common.rs | 23 ++++ tests/sqlparser_mssql.rs | 14 ++- tests/sqlparser_mysql.rs | 2 + tests/sqlparser_postgres.rs | 119 ++++++++++++++++++ 14 files changed, 237 insertions(+), 7 deletions(-) create mode 100644 fuzz/fuzz_seeds/postgres_table_inheritance_markers.sql diff --git a/fuzz/fuzz_seeds/postgres_table_inheritance_markers.sql b/fuzz/fuzz_seeds/postgres_table_inheritance_markers.sql new file mode 100644 index 0000000000..d3c5bf91f8 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_table_inheritance_markers.sql @@ -0,0 +1,3 @@ +SELECT * FROM ONLY (a) JOIN b * AS x ON a.id = x.id; +UPDATE ONLY t SET id = 1; +DELETE FROM t *; diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7a338a4859..653a3581e5 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -112,8 +112,8 @@ pub use self::query::{ SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SelectModifiers, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs, TableIndexDirective, - TableIndexHintForClause, TableIndexHintType, TableIndexHints, TableIndexType, TableSample, - TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier, + TableIndexHintForClause, TableIndexHintType, TableIndexHints, TableIndexType, TableInheritance, + TableSample, TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier, TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity, UpdateTableFromKind, ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, XmlNamespaceDefinition, XmlPassingArgument, diff --git a/src/ast/query.rs b/src/ast/query.rs index b2b3f56b5c..e2364dbe9a 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1477,6 +1477,22 @@ impl fmt::Display for TableIndexDirective { } } +/// PostgreSQL table inheritance marker, see [`TableFactor::Table::inheritance`]. +/// +/// +#[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 TableInheritance { + /// `ONLY name`, or `ONLY (name)` when `parenthesized`, excluding descendant tables. + Only { + /// Whether the name was written in parentheses. + parenthesized: bool, + }, + /// `name *`, explicitly including descendant tables. + IncludeDescendants, +} + /// A table name or a parenthesized subquery with an optional alias #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1516,6 +1532,8 @@ pub enum TableFactor { sample: Option, /// Optional table-level index-selection hints or requirements. index_directives: Vec, + /// PostgreSQL `ONLY` or `*` inheritance marker. + inheritance: Option, }, /// A derived table (a parenthesized subquery), optionally `LATERAL`. Derived { @@ -2250,8 +2268,18 @@ impl fmt::Display for TableFactor { json_path, sample, index_directives, + inheritance, } => { - name.fmt(f)?; + match inheritance { + Some(TableInheritance::Only { + parenthesized: true, + }) => write!(f, "ONLY ({name})")?, + Some(TableInheritance::Only { + parenthesized: false, + }) => write!(f, "ONLY {name}")?, + Some(TableInheritance::IncludeDescendants) => write!(f, "{name} *")?, + None => name.fmt(f)?, + } if let Some(json_path) = json_path { json_path.fmt(f)?; } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index ee6c6993ad..e5eca80edb 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2059,6 +2059,7 @@ impl Spanned for TableFactor { json_path: _, sample: _, index_directives: _, + inheritance: _, } => union_spans( name.0 .iter() diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index cafa085275..4f1875b874 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -333,6 +333,10 @@ impl Dialect for GenericDialect { true } + fn supports_table_inheritance_markers(&self) -> bool { + true + } + fn supports_select_format(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index ae79a3970f..fdf193902a 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -2174,6 +2174,20 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if this dialect supports the `ONLY` and `*` table + /// inheritance markers on a table factor. + /// + /// Example: + /// ```sql + /// SELECT * FROM ONLY parent; + /// SELECT * FROM parent *; + /// ``` + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/sql-select.html#SQL-FROM) + fn supports_table_inheritance_markers(&self) -> bool { + false + } + /// Returns true if this dialect supports the `FORMAT` clause in `SELECT` statements. /// /// Example: diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 235184c802..e5a1014bfa 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -613,4 +613,9 @@ impl Dialect for PostgreSqlDialect { fn supports_attach_partition_of(&self) -> bool { true } + + /// See + fn supports_table_inheritance_markers(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 55fbdd525d..01bcfbd9a9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17435,6 +17435,10 @@ impl<'a> Parser<'a> { // Stage reference: @mystage or @namespace.stage (e.g. Snowflake) self.parse_snowflake_stage_table_factor() } else { + let inheritance_markers = self.dialect.supports_table_inheritance_markers(); + let only = inheritance_markers && self.parse_keyword(Keyword::ONLY); + let only_parenthesized = only && self.consume_token(&Token::LParen); + // Unquoted reserved keywords, e.g. `ON`, are not table names. The // keyword is consumed first so that `is_table_factor` peeks past it, // as it does at the trailing comma check. @@ -17451,6 +17455,19 @@ impl<'a> Parser<'a> { let name = self.parse_object_name(true)?; + let inheritance = if only { + if only_parenthesized { + self.expect_token(&Token::RParen)?; + } + Some(TableInheritance::Only { + parenthesized: only_parenthesized, + }) + } else if inheritance_markers && self.consume_token(&Token::Mul) { + Some(TableInheritance::IncludeDescendants) + } else { + None + }; + let json_path = match &self.peek_token_ref().token { Token::LBracket if self.dialect.supports_partiql() => Some(self.parse_json_path()?), _ => None, @@ -17528,6 +17545,7 @@ impl<'a> Parser<'a> { json_path, sample, index_directives, + inheritance, }; while let Some(kw) = self.parse_one_of_keywords(&[Keyword::PIVOT, Keyword::UNPIVOT]) { @@ -17578,6 +17596,7 @@ impl<'a> Parser<'a> { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }) } diff --git a/src/test_utils.rs b/src/test_utils.rs index 4c9bbb2bc2..aba2836b25 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -391,6 +391,7 @@ pub fn table(name: impl Into) -> TableFactor { json_path: None, sample: None, index_directives: vec![], + inheritance: None, } } @@ -406,6 +407,7 @@ pub fn table_from_name(name: ObjectName) -> TableFactor { json_path: None, sample: None, index_directives: vec![], + inheritance: None, } } @@ -425,6 +427,7 @@ pub fn table_with_alias( json_path: None, sample: None, index_directives: vec![], + inheritance: None, } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index b110423370..9b36709fdd 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1768,6 +1768,7 @@ fn parse_table_time_travel() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, joins: vec![] },] @@ -1883,6 +1884,7 @@ fn parse_merge() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, table ); @@ -1898,6 +1900,7 @@ fn parse_merge() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, source ); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 61fb856a74..db7786d681 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -643,6 +643,7 @@ fn parse_update_with_table_alias() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, joins: vec![], }, @@ -745,6 +746,7 @@ fn parse_select_with_table_alias() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, joins: vec![], }] @@ -951,6 +953,7 @@ fn parse_where_delete_with_alias_statement() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, from[0].relation, ); @@ -967,6 +970,7 @@ fn parse_where_delete_with_alias_statement() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, joins: vec![], }]), @@ -8227,6 +8231,7 @@ fn parse_joins_on() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, global, join_operator: f(JoinConstraint::On(Expr::BinaryOp { @@ -8369,6 +8374,7 @@ fn parse_joins_using() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, global: false, join_operator: f(JoinConstraint::Using(vec![ObjectName::from(vec![ @@ -8464,6 +8470,7 @@ fn parse_natural_join() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, global: false, join_operator: f(JoinConstraint::Natural), @@ -10980,6 +10987,7 @@ fn parse_merge() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, } ); assert_eq!(table, table_no_into); @@ -12457,6 +12465,7 @@ fn parse_pivot_table() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }), aggregate_functions: vec![ expected_function("a", None), @@ -12537,6 +12546,7 @@ fn parse_pivot_table() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }), aggregate_functions: vec![ ExprWithAlias { @@ -12623,6 +12633,7 @@ fn parse_unpivot_table() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }), null_inclusion: None, value: Expr::Identifier(Ident::new("quantity")), @@ -12882,6 +12893,7 @@ fn parse_select_table_with_index_hints() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, joins: vec![], }] @@ -12911,6 +12923,7 @@ fn parse_pivot_unpivot_table() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }), null_inclusion: None, value: Expr::Identifier(Ident::new("population")), @@ -19000,6 +19013,7 @@ fn test_nested_join_without_parentheses() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -19013,6 +19027,7 @@ fn test_nested_join_without_parentheses() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, global: false, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { @@ -19067,6 +19082,7 @@ fn test_nested_join_without_parentheses() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -19080,6 +19096,7 @@ fn test_nested_join_without_parentheses() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, global: false, join_operator: JoinOperator::Join(JoinConstraint::On(Expr::BinaryOp { @@ -19134,6 +19151,7 @@ fn test_nested_join_without_parentheses() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -19147,6 +19165,7 @@ fn test_nested_join_without_parentheses() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, global: false, join_operator: JoinOperator::Left(JoinConstraint::On(Expr::BinaryOp { @@ -19201,6 +19220,7 @@ fn test_nested_join_without_parentheses() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -19214,6 +19234,7 @@ fn test_nested_join_without_parentheses() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, global: false, join_operator: JoinOperator::Right(JoinConstraint::On(Expr::BinaryOp { @@ -19268,6 +19289,7 @@ fn test_nested_join_without_parentheses() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -19281,6 +19303,7 @@ fn test_nested_join_without_parentheses() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, global: false, join_operator: JoinOperator::FullOuter(JoinConstraint::On( diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index fe8531cace..71e3b6cf7b 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -75,7 +75,8 @@ fn parse_table_time_travel() { with_ordinality: false, json_path: None, sample: None, - index_directives: vec![] + index_directives: vec![], + inheritance: None, }, joins: vec![] },] @@ -495,7 +496,8 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, - index_directives: vec![] + index_directives: vec![], + inheritance: None, }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -549,7 +551,8 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, - index_directives: vec![] + index_directives: vec![], + inheritance: None, }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -603,7 +606,8 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, - index_directives: vec![] + index_directives: vec![], + inheritance: None, }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -658,6 +662,7 @@ fn parse_mssql_openjson() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -692,6 +697,7 @@ fn parse_mssql_openjson() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, joins: vec![Join { relation: TableFactor::OpenJsonTable { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index a7aaef6b48..0e21857976 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2805,6 +2805,7 @@ fn parse_update_with_joins() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -2818,6 +2819,7 @@ fn parse_update_with_joins() { json_path: None, sample: None, index_directives: vec![], + inheritance: None, }, global: false, join_operator: JoinOperator::Join(JoinConstraint::On(Expr::BinaryOp { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index aa9cc38412..7fc3b8cdc3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11769,3 +11769,122 @@ fn parse_storage_parameters_without_value() { ParserError::ParserError("Expected: =, found: 70".to_string()) ); } + +#[test] +fn parse_table_inheritance_markers() { + fn assert_relation( + relation: &TableFactor, + expected_name: &str, + expected_alias: Option<&str>, + expected_inheritance: Option, + ) { + match relation { + TableFactor::Table { + name, + alias, + inheritance, + .. + } => { + assert_eq!(name.to_string(), expected_name); + assert_eq!( + alias.as_ref().map(|a| a.name.value.as_str()), + expected_alias + ); + assert_eq!(*inheritance, expected_inheritance); + } + other => panic!("expected a table, got {other:?}"), + } + } + + let only = Some(TableInheritance::Only { + parenthesized: false, + }); + let only_parenthesized = Some(TableInheritance::Only { + parenthesized: true, + }); + let descendants = Some(TableInheritance::IncludeDescendants); + + for (sql, name, alias, inheritance) in [ + ("SELECT * FROM ONLY t", "t", None, only), + ("SELECT * FROM ONLY t AS x", "t", Some("x"), only), + ("SELECT * FROM ONLY (t)", "t", None, only_parenthesized), + ( + "SELECT * FROM ONLY (app.t) x", + "app.t", + Some("x"), + only_parenthesized, + ), + ("SELECT * FROM t *", "t", None, descendants), + ( + "SELECT * FROM app.t * AS x", + "app.t", + Some("x"), + descendants, + ), + ("SELECT * FROM \"only\"", "\"only\"", None, None), + ] { + let select = pg_and_generic().verified_only_select(sql); + assert_relation(&select.from[0].relation, name, alias, inheritance); + } + + let select = + pg_and_generic().verified_only_select("SELECT * FROM ONLY a JOIN b * ON a.id = b.id"); + assert_relation(&select.from[0].relation, "a", None, only); + assert_relation(&select.from[0].joins[0].relation, "b", None, descendants); + + for (sql, inheritance) in [ + ("UPDATE ONLY t SET id = 1", only), + ("UPDATE ONLY (t) SET id = 1", only_parenthesized), + ("UPDATE t * SET id = 1", descendants), + ] { + match pg_and_generic().verified_stmt(sql) { + Statement::Update(Update { table, .. }) => { + assert_relation(&table.relation, "t", None, inheritance) + } + other => panic!("expected an update, got {other:?}"), + } + } + + for (sql, inheritance) in [ + ("DELETE FROM ONLY t", only), + ("DELETE FROM ONLY (t)", only_parenthesized), + ("DELETE FROM t *", descendants), + ] { + match pg_and_generic().verified_stmt(sql) { + Statement::Delete(Delete { + from: FromTable::WithFromKeyword(from), + .. + }) => assert_relation(&from[0].relation, "t", None, inheritance), + other => panic!("expected a delete, got {other:?}"), + } + } + + pg_and_generic().one_statement_parses_to("SELECT * FROM t*", "SELECT * FROM t *"); + pg_and_generic().one_statement_parses_to("SELECT * FROM ONLY(t)", "SELECT * FROM ONLY (t)"); + pg_and_generic().one_statement_parses_to("UPDATE t* SET id = 1", "UPDATE t * SET id = 1"); + pg_and_generic().one_statement_parses_to("DELETE FROM t*", "DELETE FROM t *"); + + for sql in [ + "SELECT * FROM ONLY", + "SELECT * FROM ONLY t *", + "SELECT * FROM ONLY (t *)", + "SELECT * FROM ONLY (t", + "SELECT * FROM ONLY ()", + ] { + assert!(pg_and_generic().parse_sql_statements(sql).is_err(), "{sql}"); + } + + let other_dialects = all_dialects_where(|d| !d.supports_table_inheritance_markers()); + for sql in [ + "SELECT * FROM t *", + "UPDATE t * SET id = 1", + "DELETE FROM t *", + ] { + for dialect in &other_dialects.dialects { + assert!( + Parser::parse_sql(dialect.as_ref(), sql).is_err(), + "{sql} with {dialect:?}" + ); + } + } +} From df088c4c720d83993e95b4b8794488d94384c4db Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 30 Oct 2026 12:02:15 +0100 Subject: [PATCH 2/2] Test table inheritance markers and formatter errors --- tests/sqlparser_postgres.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7fc3b8cdc3..42846c9e51 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11888,3 +11888,31 @@ fn parse_table_inheritance_markers() { } } } + +#[test] +fn display_table_inheritance_propagates_write_errors() { + use core::fmt::{self, Write}; + + struct RejectingWriter(&'static str); + + impl Write for RejectingWriter { + fn write_str(&mut self, value: &str) -> fmt::Result { + if value == self.0 { + Err(fmt::Error) + } else { + Ok(()) + } + } + } + + for (sql, rejected) in [ + ("SELECT * FROM ONLY (t)", "ONLY ("), + ("SELECT * FROM ONLY t", "ONLY "), + ("SELECT * FROM t *", " *"), + ("SELECT * FROM t", "t"), + ] { + let stmt = pg_and_generic().verified_stmt(sql); + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{stmt}"), Err(fmt::Error), "{sql}"); + } +}