From 6f427aa4994f2722bd05e9af316ef1f1cae9f8ae Mon Sep 17 00:00:00 2001 From: Ben Herzberg Date: Tue, 15 Sep 2026 14:34:46 +0300 Subject: [PATCH 1/2] Support TABLE query command --- src/ast/query.rs | 15 +++++++++++++-- src/parser/mod.rs | 21 +++++++++++++++++++-- tests/sqlparser_common.rs | 4 ++++ tests/sqlparser_postgres.rs | 7 +++++++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 98a9c84ae..2dadb3727 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -298,19 +298,30 @@ impl fmt::Display for SetQuantifier { /// A [`TABLE` command](https://www.postgresql.org/docs/current/sql-select.html#SQL-TABLE) #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Table { + /// `ONLY` modifier before the table name. + pub only: bool, /// 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, + /// Trailing `*` modifier after the table name. + pub with_asterisk: bool, } impl fmt::Display for Table { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(ref table_name) = self.table_name { + write!(f, "TABLE ")?; + if self.only { + write!(f, "ONLY ")?; + } if let Some(ref schema_name) = self.schema_name { - write!(f, "TABLE {}.{}", schema_name, table_name,)?; + write!(f, "{}.{}", schema_name, table_name,)?; } else { - write!(f, "TABLE {}", table_name)?; + write!(f, "{}", table_name)?; + } + if self.with_asterisk { + write!(f, " *")?; } } else { write!(f, "TABLE")?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index eb435f153..104ddc58c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -641,7 +641,11 @@ impl<'a> Parser<'a> { self.prev_token(); self.parse_raise_stmt().map(Into::into) } - Keyword::SELECT | Keyword::WITH | Keyword::VALUES | Keyword::FROM => { + Keyword::SELECT + | Keyword::TABLE + | Keyword::WITH + | Keyword::VALUES + | Keyword::FROM => { self.prev_token(); self.parse_query().map(Into::into) } @@ -14496,7 +14500,15 @@ impl<'a> Parser<'a> { } } - match self.maybe_parse(|parser| parser.parse_statement())? { + let statement = if self.dialect.describe_requires_table_keyword() + && self.peek_keyword(Keyword::TABLE) + { + None + } else { + self.maybe_parse(|parser| parser.parse_statement())? + }; + + match statement { Some(Statement::Explain { .. }) | Some(Statement::ExplainTable { .. }) => Err( ParserError::ParserError("Explain must be root of the plan".to_string()), ), @@ -15625,17 +15637,22 @@ impl<'a> Parser<'a> { /// Parse `CREATE TABLE x AS TABLE y` pub fn parse_as_table(&mut self) -> Result { + let only = self.parse_keyword(Keyword::ONLY); let first_name = self.parse_identifier()?; if self.consume_token(&Token::Period) { let second_name = self.parse_identifier()?; Ok(Table { + only, table_name: Some(second_name), schema_name: Some(first_name), + with_asterisk: self.consume_token(&Token::Mul), }) } else { Ok(Table { + only, table_name: Some(first_name), schema_name: None, + with_asterisk: self.consume_token(&Token::Mul), }) } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9fb208bd5..b4e55d964 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4848,8 +4848,10 @@ fn parse_create_table_as_table() { let expected_query1 = Box::new(Query { with: None, body: Box::new(SetExpr::Table(Box::new(Table { + only: false, table_name: Some(Ident::new("old_table")), schema_name: None, + with_asterisk: false, }))), order_by: None, limit_clause: None, @@ -4874,8 +4876,10 @@ fn parse_create_table_as_table() { let expected_query2 = Box::new(Query { with: None, body: Box::new(SetExpr::Table(Box::new(Table { + only: false, table_name: Some(Ident::new("old_table")), schema_name: Some(Ident::new("schema_name")), + with_asterisk: false, }))), order_by: None, limit_clause: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1f4377219..4533c06d6 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10037,3 +10037,10 @@ fn parse_bitstring_literal_escaping() { pg_and_generic().verified_stmt("SELECT B''''"); pg_and_generic().verified_stmt("SELECT B'it''s'"); } + +#[test] +fn parse_table_command() { + pg().verified_stmt("TABLE customers ORDER BY contact_name LIMIT 1"); + pg().verified_stmt("TABLE ONLY customers"); + pg().verified_stmt("TABLE customers *"); +} From 5b3d24349f9c2ca49179d87dbe9a03987617e72c Mon Sep 17 00:00:00 2001 From: Ben Herzberg Date: Wed, 16 Sep 2026 11:02:15 +0300 Subject: [PATCH 2/2] Address review comments: broaden TABLE command test to pg_and_generic, add ONLY/* coverage for AS TABLE --- tests/sqlparser_common.rs | 56 +++++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 6 ++-- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b4e55d964..2c38e4a4d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -4898,6 +4898,62 @@ fn parse_create_table_as_table() { } _ => unreachable!(), } + + let sql3 = "CREATE TABLE new_table AS TABLE ONLY old_table"; + + let expected_query3 = Box::new(Query { + with: None, + body: Box::new(SetExpr::Table(Box::new(Table { + only: true, + table_name: Some(Ident::new("old_table")), + schema_name: None, + with_asterisk: false, + }))), + order_by: None, + limit_clause: None, + fetch: None, + locks: vec![], + for_clause: None, + settings: None, + format_clause: None, + pipe_operators: vec![], + }); + + match verified_stmt(sql3) { + Statement::CreateTable(CreateTable { query, name, .. }) => { + assert_eq!(name, ObjectName::from(vec![Ident::new("new_table")])); + assert_eq!(query.unwrap(), expected_query3); + } + _ => unreachable!(), + } + + let sql4 = "CREATE TABLE new_table AS TABLE old_table *"; + + let expected_query4 = Box::new(Query { + with: None, + body: Box::new(SetExpr::Table(Box::new(Table { + only: false, + table_name: Some(Ident::new("old_table")), + schema_name: None, + with_asterisk: true, + }))), + order_by: None, + limit_clause: None, + fetch: None, + locks: vec![], + for_clause: None, + settings: None, + format_clause: None, + pipe_operators: vec![], + }); + + match verified_stmt(sql4) { + Statement::CreateTable(CreateTable { query, name, .. }) => { + assert_eq!(name, ObjectName::from(vec![Ident::new("new_table")])); + assert_eq!(query.unwrap(), expected_query4); + } + _ => unreachable!(), + } } #[test] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 4533c06d6..9463ddad3 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10040,7 +10040,7 @@ fn parse_bitstring_literal_escaping() { #[test] fn parse_table_command() { - pg().verified_stmt("TABLE customers ORDER BY contact_name LIMIT 1"); - pg().verified_stmt("TABLE ONLY customers"); - pg().verified_stmt("TABLE customers *"); + pg_and_generic().verified_stmt("TABLE customers ORDER BY contact_name LIMIT 1"); + pg_and_generic().verified_stmt("TABLE ONLY customers"); + pg_and_generic().verified_stmt("TABLE customers *"); }