From 1ec41593b7361506750b01a43d1d365256ca7f4b Mon Sep 17 00:00:00 2001 From: kould Date: Sat, 24 Feb 2024 02:32:29 +0800 Subject: [PATCH] feat: support `Create Table ... Like` --- src/frontend/src/instance.rs | 6 +- src/operator/src/expr_factory.rs | 2 +- src/operator/src/statement.rs | 6 +- src/operator/src/statement/ddl.rs | 47 +++++++++++++- src/operator/src/statement/show.rs | 2 +- src/query/src/sql.rs | 11 +--- src/session/src/context.rs | 11 ++++ src/sql/src/parsers/create_parser.rs | 63 +++++++++++++------ src/sql/src/statements/create.rs | 7 +++ src/sql/src/statements/statement.rs | 6 +- .../standalone/common/create/create.result | 38 +++++++++++ .../cases/standalone/common/create/create.sql | 14 +++++ 12 files changed, 176 insertions(+), 37 deletions(-) diff --git a/src/frontend/src/instance.rs b/src/frontend/src/instance.rs index 8f63adee0198..7b32605639db 100644 --- a/src/frontend/src/instance.rs +++ b/src/frontend/src/instance.rs @@ -460,8 +460,10 @@ pub fn check_permission( // database ops won't be checked Statement::CreateDatabase(_) | Statement::ShowDatabases(_) => {} // show create table and alter are not supported yet - Statement::ShowCreateTable(_) | Statement::CreateExternalTable(_) | Statement::Alter(_) => { - } + Statement::ShowCreateTable(_) + | Statement::CreateExternalTable(_) + | Statement::CreateTableLike(_) + | Statement::Alter(_) => {} // set/show variable now only alter/show variable in session Statement::SetVariables(_) | Statement::ShowVariables(_) => {} diff --git a/src/operator/src/expr_factory.rs b/src/operator/src/expr_factory.rs index 55072ddd3c51..2f0ba948f5c8 100644 --- a/src/operator/src/expr_factory.rs +++ b/src/operator/src/expr_factory.rs @@ -181,7 +181,7 @@ pub(crate) async fn create_external_expr( Ok(expr) } -/// Convert `CreateTable` statement to `CreateExpr` gRPC request. +/// Convert `CreateTable` statement to `CreateTableExpr` gRPC request. pub fn create_to_expr(create: &CreateTable, query_ctx: QueryContextRef) -> Result { let (catalog_name, schema_name, table_name) = table_idents_to_full_name(&create.name, &query_ctx) diff --git a/src/operator/src/statement.rs b/src/operator/src/statement.rs index b0cd2c773b08..9eb92f832617 100644 --- a/src/operator/src/statement.rs +++ b/src/operator/src/statement.rs @@ -153,6 +153,10 @@ impl StatementExecutor { let _ = self.create_table(stmt, query_ctx).await?; Ok(Output::AffectedRows(0)) } + Statement::CreateTableLike(stmt) => { + let _ = self.create_table_like(stmt, query_ctx).await?; + Ok(Output::AffectedRows(0)) + } Statement::CreateExternalTable(stmt) => { let _ = self.create_external_table(stmt, query_ctx).await?; Ok(Output::AffectedRows(0)) @@ -174,7 +178,6 @@ impl StatementExecutor { let table_name = TableName::new(catalog, schema, table); self.truncate_table(table_name).await } - Statement::CreateDatabase(stmt) => { self.create_database( query_ctx.current_catalog(), @@ -183,7 +186,6 @@ impl StatementExecutor { ) .await } - Statement::ShowCreateTable(show) => { let (catalog, schema, table) = table_idents_to_full_name(&show.table_name, &query_ctx) diff --git a/src/operator/src/statement/ddl.rs b/src/operator/src/statement/ddl.rs index 9231ba8d5c0e..4a68e081ace3 100644 --- a/src/operator/src/statement/ddl.rs +++ b/src/operator/src/statement/ddl.rs @@ -21,6 +21,7 @@ use catalog::CatalogManagerRef; use chrono::Utc; use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME}; use common_catalog::format_full_table_name; +use common_error::ext::BoxedError; use common_meta::cache_invalidator::Context; use common_meta::ddl::ExecutorContext; use common_meta::key::schema_name::{SchemaNameKey, SchemaNameValue}; @@ -34,12 +35,13 @@ use datatypes::prelude::ConcreteDataType; use datatypes::schema::RawSchema; use lazy_static::lazy_static; use partition::partition::{PartitionBound, PartitionDef}; +use query::sql::show_create_table; use regex::Regex; use session::context::QueryContextRef; use snafu::{ensure, IntoError, OptionExt, ResultExt}; use sql::ast::Value as SqlValue; use sql::statements::alter::AlterTable; -use sql::statements::create::{CreateExternalTable, CreateTable, Partitions}; +use sql::statements::create::{CreateExternalTable, CreateTable, CreateTableLike, Partitions}; use sql::statements::sql_value_to_value; use sql::MAXVALUE; use table::dist_table::DistTable; @@ -57,6 +59,8 @@ use crate::error::{ UnrecognizedTableOptionSnafu, }; use crate::expr_factory; +use crate::statement::show::create_partitions_stmt; +use crate::table::table_idents_to_full_name; lazy_static! { static ref NAME_PATTERN_REG: Regex = Regex::new(&format!("^{NAME_PATTERN}$")).unwrap(); @@ -74,6 +78,47 @@ impl StatementExecutor { .await } + #[tracing::instrument(skip_all)] + pub async fn create_table_like( + &self, + stmt: CreateTableLike, + ctx: QueryContextRef, + ) -> Result { + let (catalog, schema, table) = table_idents_to_full_name(&stmt.target, &ctx) + .map_err(BoxedError::new) + .context(error::ExternalSnafu)?; + let table_ref = self + .catalog_manager + .table(&catalog, &schema, &table) + .await + .context(error::CatalogSnafu)? + .context(error::TableNotFoundSnafu { table_name: &table })?; + let partitions = self + .partition_manager + .find_table_partitions(table_ref.table_info().table_id()) + .await + .context(error::FindTablePartitionRuleSnafu { table_name: table })?; + + let quote_style = ctx.quote_style(); + let mut create_stmt = + show_create_table::create_table_stmt(&table_ref.table_info(), quote_style) + .context(error::ParseQuerySnafu)?; + create_stmt.name = stmt.name; + create_stmt.if_not_exists = false; + + let partitions = create_partitions_stmt(partitions)?.and_then(|mut partitions| { + if !partitions.column_list.is_empty() { + partitions.set_quote(quote_style); + Some(partitions) + } else { + None + } + }); + + let create_expr = &mut expr_factory::create_to_expr(&create_stmt, ctx.clone())?; + self.create_table_inner(create_expr, partitions, &ctx).await + } + #[tracing::instrument(skip_all)] pub async fn create_external_table( &self, diff --git a/src/operator/src/statement/show.rs b/src/operator/src/statement/show.rs index ee3c2e10710d..d43a3e007c71 100644 --- a/src/operator/src/statement/show.rs +++ b/src/operator/src/statement/show.rs @@ -78,7 +78,7 @@ impl StatementExecutor { } } -fn create_partitions_stmt(partitions: Vec) -> Result> { +pub(crate) fn create_partitions_stmt(partitions: Vec) -> Result> { if partitions.is_empty() { return Ok(None); } diff --git a/src/query/src/sql.rs b/src/query/src/sql.rs index 34ff7fbe8a82..e170f1208473 100644 --- a/src/query/src/sql.rs +++ b/src/query/src/sql.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod show_create_table; +pub mod show_create_table; use std::collections::HashMap; use std::sync::Arc; @@ -258,14 +258,7 @@ pub fn show_create_table( let table_info = table.table_info(); let table_name = &table_info.name; - // Default to double quote and fallback to back quote - let quote_style = if query_ctx.sql_dialect().is_delimited_identifier_start('"') { - '"' - } else if query_ctx.sql_dialect().is_delimited_identifier_start('\'') { - '\'' - } else { - '`' - }; + let quote_style = query_ctx.quote_style(); let mut stmt = show_create_table::create_table_stmt(&table_info, quote_style)?; stmt.partitions = partitions.map(|mut p| { diff --git a/src/session/src/context.rs b/src/session/src/context.rs index 53cc2db7441a..cc41af37445b 100644 --- a/src/session/src/context.rs +++ b/src/session/src/context.rs @@ -172,6 +172,17 @@ impl QueryContext { session.set_timezone(tz.as_ref().clone()) } } + + /// Default to double quote and fallback to back quote + pub fn quote_style(&self) -> char { + if self.sql_dialect().is_delimited_identifier_start('"') { + '"' + } else if self.sql_dialect().is_delimited_identifier_start('\'') { + '\'' + } else { + '`' + } + } } impl QueryContextBuilder { diff --git a/src/sql/src/parsers/create_parser.rs b/src/sql/src/parsers/create_parser.rs index cbdb05a985bd..81717766742b 100644 --- a/src/sql/src/parsers/create_parser.rs +++ b/src/sql/src/parsers/create_parser.rs @@ -20,7 +20,7 @@ use datatypes::prelude::ConcreteDataType; use itertools::Itertools; use once_cell::sync::Lazy; use snafu::{ensure, OptionExt, ResultExt}; -use sqlparser::ast::{ColumnOption, ColumnOptionDef, DataType, Value}; +use sqlparser::ast::{ColumnOption, ColumnOptionDef, DataType, ObjectName, Value}; use sqlparser::dialect::keywords::Keyword; use sqlparser::keywords::ALL_KEYWORDS; use sqlparser::parser::IsOptional::Mandatory; @@ -35,7 +35,8 @@ use crate::error::{ }; use crate::parser::ParserContext; use crate::statements::create::{ - CreateDatabase, CreateExternalTable, CreateTable, PartitionEntry, Partitions, TIME_INDEX, + CreateDatabase, CreateExternalTable, CreateTable, CreateTableLike, PartitionEntry, Partitions, + TIME_INDEX, }; use crate::statements::statement::Statement; use crate::statements::{ @@ -74,15 +75,7 @@ impl<'a> ParserContext<'a> { let if_not_exists = self.parser .parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - let raw_table_name = self - .parser - .parse_object_name() - .context(error::UnexpectedSnafu { - sql: self.sql, - expected: "a table name", - actual: self.peek_token_as_string(), - })?; - let table_name = Self::canonicalize_object_name(raw_table_name); + let table_name = self.parse_table_name()?; let (columns, constraints) = self.parse_columns()?; let engine = self.parse_table_engine(common_catalog::consts::FILE_ENGINE)?; let options = self @@ -144,15 +137,16 @@ impl<'a> ParserContext<'a> { self.parser .parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); - let raw_table_name = self - .parser - .parse_object_name() - .context(error::UnexpectedSnafu { - sql: self.sql, - expected: "a table name", - actual: self.peek_token_as_string(), - })?; - let table_name = Self::canonicalize_object_name(raw_table_name); + let table_name = self.parse_table_name()?; + + if self.parser.parse_keyword(Keyword::LIKE) { + let target_table_name = self.parse_table_name()?; + + return Ok(Statement::CreateTableLike(CreateTableLike { + name: table_name, + target: target_table_name, + })); + } let (columns, constraints) = self.parse_columns()?; @@ -188,6 +182,18 @@ impl<'a> ParserContext<'a> { Ok(Statement::CreateTable(create_table)) } + fn parse_table_name(&mut self) -> Result { + let raw_table_name = self + .parser + .parse_object_name() + .context(error::UnexpectedSnafu { + sql: self.sql, + expected: "a table name", + actual: self.peek_token_as_string(), + })?; + Ok(Self::canonicalize_object_name(raw_table_name)) + } + // "PARTITION BY ..." syntax: // https://dev.mysql.com/doc/refman/8.0/en/partitioning-columns-range.html fn parse_partitions(&mut self) -> Result> { @@ -884,6 +890,23 @@ mod tests { use crate::dialect::GreptimeDbDialect; use crate::parser::ParseOptions; + #[test] + fn test_parse_create_table_like() { + let sql = "CREATE TABLE t1 LIKE t2"; + let stmts = + ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()) + .unwrap(); + + assert_eq!(1, stmts.len()); + match &stmts[0] { + Statement::CreateTableLike(c) => { + assert_eq!(c.name.to_string(), "t1"); + assert_eq!(c.target.to_string(), "t2"); + } + _ => unreachable!(), + } + } + #[test] fn test_validate_external_table_options() { let sql = "CREATE EXTERNAL TABLE city ( diff --git a/src/sql/src/statements/create.rs b/src/sql/src/statements/create.rs index ebcb844078c3..b6d8e8fe2dee 100644 --- a/src/sql/src/statements/create.rs +++ b/src/sql/src/statements/create.rs @@ -218,6 +218,13 @@ pub struct CreateExternalTable { pub engine: String, } +#[derive(Debug, PartialEq, Eq, Clone, Visit, VisitMut)] +pub struct CreateTableLike { + /// Table name + pub name: ObjectName, + pub target: ObjectName, +} + #[cfg(test)] mod tests { use crate::dialect::GreptimeDbDialect; diff --git a/src/sql/src/statements/statement.rs b/src/sql/src/statements/statement.rs index 028f437c3daf..b8af789616d1 100644 --- a/src/sql/src/statements/statement.rs +++ b/src/sql/src/statements/statement.rs @@ -19,7 +19,9 @@ use sqlparser_derive::{Visit, VisitMut}; use super::show::ShowVariables; use crate::error::{ConvertToDfStatementSnafu, Error}; use crate::statements::alter::AlterTable; -use crate::statements::create::{CreateDatabase, CreateExternalTable, CreateTable}; +use crate::statements::create::{ + CreateDatabase, CreateExternalTable, CreateTable, CreateTableLike, +}; use crate::statements::delete::Delete; use crate::statements::describe::DescribeTable; use crate::statements::drop::DropTable; @@ -45,6 +47,8 @@ pub enum Statement { CreateTable(CreateTable), // CREATE EXTERNAL TABLE CreateExternalTable(CreateExternalTable), + // CREATE TABLE ... LIKE + CreateTableLike(CreateTableLike), // DROP TABLE DropTable(DropTable), // CREATE DATABASE diff --git a/tests/cases/standalone/common/create/create.result b/tests/cases/standalone/common/create/create.result index 53cb8bbba0f9..641514a526a4 100644 --- a/tests/cases/standalone/common/create/create.result +++ b/tests/cases/standalone/common/create/create.result @@ -143,3 +143,41 @@ DROP TABLE neg_default_value; Affected Rows: 0 +CREATE TABLE test_like_1 (i INTEGER, j TIMESTAMP TIME INDEX); + +Affected Rows: 0 + +CREATE TABLE test_like_2 LIKE test_like_1; + +Affected Rows: 0 + +CREATE TABLE test_like_2 LIKE test_like_1; + +Error: 4000(TableAlreadyExists), Table already exists: `greptime.public.test_like_2` + +DESC TABLE test_like_1; + ++--------+----------------------+-----+------+---------+---------------+ +| Column | Type | Key | Null | Default | Semantic Type | ++--------+----------------------+-----+------+---------+---------------+ +| i | Int32 | | YES | | FIELD | +| j | TimestampMillisecond | PRI | NO | | TIMESTAMP | ++--------+----------------------+-----+------+---------+---------------+ + +DESC TABLE test_like_2; + ++--------+----------------------+-----+------+---------+---------------+ +| Column | Type | Key | Null | Default | Semantic Type | ++--------+----------------------+-----+------+---------+---------------+ +| i | Int32 | | YES | | FIELD | +| j | TimestampMillisecond | PRI | NO | | TIMESTAMP | ++--------+----------------------+-----+------+---------+---------------+ + +DROP TABLE test_like_1; + +Affected Rows: 0 + +DROP TABLE test_like_2; + +Affected Rows: 0 + diff --git a/tests/cases/standalone/common/create/create.sql b/tests/cases/standalone/common/create/create.sql index 47d95c46d73c..a2e3c630d9d7 100644 --- a/tests/cases/standalone/common/create/create.sql +++ b/tests/cases/standalone/common/create/create.sql @@ -57,3 +57,17 @@ CREATE TABLE neg_default_value(i INT DEFAULT -1024, ts TIMESTAMP TIME INDEX); desc TABLE neg_default_value; DROP TABLE neg_default_value; + +CREATE TABLE test_like_1 (i INTEGER, j TIMESTAMP TIME INDEX); + +CREATE TABLE test_like_2 LIKE test_like_1; + +CREATE TABLE test_like_2 LIKE test_like_1; + +DESC TABLE test_like_1; + +DESC TABLE test_like_2; + +DROP TABLE test_like_1; + +DROP TABLE test_like_2;