diff --git a/docs/generated/sql/bnf/create_table_as_stmt.bnf b/docs/generated/sql/bnf/create_table_as_stmt.bnf index 93cc9f8294b1..4764d85b1c1a 100644 --- a/docs/generated/sql/bnf/create_table_as_stmt.bnf +++ b/docs/generated/sql/bnf/create_table_as_stmt.bnf @@ -1,5 +1,5 @@ create_table_as_stmt ::= - 'CREATE' opt_persistence_temp_table 'TABLE' table_name '(' column_name create_as_col_qual_list ( ( ',' column_name create_as_col_qual_list | ',' family_def | ',' create_as_constraint_def ) )* ')' opt_with_storage_parameter_list 'AS' select_stmt 'ON' 'COMMIT' 'PRESERVE' 'ROWS' - | 'CREATE' opt_persistence_temp_table 'TABLE' table_name opt_with_storage_parameter_list 'AS' select_stmt 'ON' 'COMMIT' 'PRESERVE' 'ROWS' - | 'CREATE' opt_persistence_temp_table 'TABLE' 'IF' 'NOT' 'EXISTS' table_name '(' column_name create_as_col_qual_list ( ( ',' column_name create_as_col_qual_list | ',' family_def | ',' create_as_constraint_def ) )* ')' opt_with_storage_parameter_list 'AS' select_stmt 'ON' 'COMMIT' 'PRESERVE' 'ROWS' - | 'CREATE' opt_persistence_temp_table 'TABLE' 'IF' 'NOT' 'EXISTS' table_name opt_with_storage_parameter_list 'AS' select_stmt 'ON' 'COMMIT' 'PRESERVE' 'ROWS' + 'CREATE' opt_persistence_temp_table 'TABLE' table_name '(' column_name create_as_col_qual_list ( ( ',' column_name create_as_col_qual_list | ',' family_def | ',' create_as_constraint_def ) )* ')' opt_with_storage_parameter_list 'AS' select_stmt opt_create_as_data 'ON' 'COMMIT' 'PRESERVE' 'ROWS' + | 'CREATE' opt_persistence_temp_table 'TABLE' table_name opt_with_storage_parameter_list 'AS' select_stmt opt_create_as_data 'ON' 'COMMIT' 'PRESERVE' 'ROWS' + | 'CREATE' opt_persistence_temp_table 'TABLE' 'IF' 'NOT' 'EXISTS' table_name '(' column_name create_as_col_qual_list ( ( ',' column_name create_as_col_qual_list | ',' family_def | ',' create_as_constraint_def ) )* ')' opt_with_storage_parameter_list 'AS' select_stmt opt_create_as_data 'ON' 'COMMIT' 'PRESERVE' 'ROWS' + | 'CREATE' opt_persistence_temp_table 'TABLE' 'IF' 'NOT' 'EXISTS' table_name opt_with_storage_parameter_list 'AS' select_stmt opt_create_as_data 'ON' 'COMMIT' 'PRESERVE' 'ROWS' diff --git a/docs/generated/sql/bnf/stmt_block.bnf b/docs/generated/sql/bnf/stmt_block.bnf index 938a374ec113..ef6f5a719654 100644 --- a/docs/generated/sql/bnf/stmt_block.bnf +++ b/docs/generated/sql/bnf/stmt_block.bnf @@ -2028,8 +2028,8 @@ create_table_stmt ::= | 'CREATE' opt_persistence_temp_table 'TABLE' 'IF' 'NOT' 'EXISTS' table_name '(' opt_table_elem_list ')' opt_partition_by_table opt_table_with opt_create_table_on_commit opt_locality create_table_as_stmt ::= - 'CREATE' opt_persistence_temp_table 'TABLE' table_name create_as_opt_col_list opt_table_with 'AS' select_stmt opt_create_table_on_commit - | 'CREATE' opt_persistence_temp_table 'TABLE' 'IF' 'NOT' 'EXISTS' table_name create_as_opt_col_list opt_table_with 'AS' select_stmt opt_create_table_on_commit + 'CREATE' opt_persistence_temp_table 'TABLE' table_name create_as_opt_col_list opt_table_with 'AS' select_stmt opt_create_as_data opt_create_table_on_commit + | 'CREATE' opt_persistence_temp_table 'TABLE' 'IF' 'NOT' 'EXISTS' table_name create_as_opt_col_list opt_table_with 'AS' select_stmt opt_create_as_data opt_create_table_on_commit create_type_stmt ::= 'CREATE' 'TYPE' type_name 'AS' 'ENUM' '(' opt_enum_val_list ')' @@ -2949,6 +2949,9 @@ create_as_opt_col_list ::= '(' create_as_table_defs ')' | +opt_create_as_data ::= + 'WITH' 'NO' 'DATA' + opt_enum_val_list ::= enum_val_list | diff --git a/pkg/sql/create_table.go b/pkg/sql/create_table.go index 37607688d5ef..9470ccb9624e 100644 --- a/pkg/sql/create_table.go +++ b/pkg/sql/create_table.go @@ -407,8 +407,9 @@ func (n *createTableNode) startExec(params runParams) error { } // If we have a single statement txn we want to run CTAS async, and - // consequently ensure it gets queued as a SchemaChange. - if params.extendedEvalCtx.TxnIsSingleStmt { + // consequently ensure it gets queued as a SchemaChange. WITH NO DATA + // has no data to copy, so the descriptor can go PUBLIC immediately. + if params.extendedEvalCtx.TxnIsSingleStmt && !n.n.WithNoData { desc.State = descpb.DescriptorState_ADD } } else { @@ -538,8 +539,8 @@ func (n *createTableNode) startExec(params runParams) error { } // If we are in a multi-statement txn or the source has placeholders, we - // execute the CTAS query synchronously. - if n.n.As() && !params.extendedEvalCtx.TxnIsSingleStmt { + // execute the CTAS query synchronously. WITH NO DATA skips the row fill. + if n.n.As() && !params.extendedEvalCtx.TxnIsSingleStmt && !n.n.WithNoData { err = func() error { // The data fill portion of CREATE AS must operate on a read snapshot, // so that it doesn't end up observing its own writes. diff --git a/pkg/sql/logictest/testdata/logic_test/create_as b/pkg/sql/logictest/testdata/logic_test/create_as index cd024cc6e47e..fb3899b85125 100644 --- a/pkg/sql/logictest/testdata/logic_test/create_as +++ b/pkg/sql/logictest/testdata/logic_test/create_as @@ -688,3 +688,64 @@ forks 30 1 spoons 10 1 subtest end + +# Test CREATE TABLE ... AS ... WITH NO DATA (#171333). The destination table +# inherits the SELECT's column types but is left empty. +subtest with_no_data + +statement ok +CREATE TABLE wnd_src (a INT, b STRING); INSERT INTO wnd_src VALUES (1, 'x'), (2, 'y') + +statement ok +CREATE TABLE wnd_empty AS SELECT * FROM wnd_src WITH NO DATA + +query IT rowsort +SELECT a, b FROM wnd_empty +---- + +query TT +SELECT column_name, data_type FROM [SHOW COLUMNS FROM wnd_empty] WHERE column_name IN ('a', 'b') ORDER BY column_name +---- +a INT8 +b STRING + +# Explicit WITH DATA still copies rows. +statement ok +CREATE TABLE wnd_full AS SELECT * FROM wnd_src WITH DATA + +query IT rowsort +SELECT a, b FROM wnd_full +---- +1 x +2 y + +# Default (no clause) still copies rows. +statement ok +CREATE TABLE wnd_default AS SELECT * FROM wnd_src + +query IT rowsort +SELECT a, b FROM wnd_default +---- +1 x +2 y + +# IF NOT EXISTS path accepts WITH NO DATA. +statement ok +CREATE TABLE IF NOT EXISTS wnd_ifne AS SELECT * FROM wnd_src WITH NO DATA + +query IT rowsort +SELECT a, b FROM wnd_ifne +---- + +# Temporary table path accepts WITH NO DATA. +statement ok +SET experimental_enable_temp_tables = 'on' + +statement ok +CREATE TEMP TABLE wnd_temp AS SELECT * FROM wnd_src WITH NO DATA + +query IT rowsort +SELECT a, b FROM wnd_temp +---- + +subtest end diff --git a/pkg/sql/parser/parse_test.go b/pkg/sql/parser/parse_test.go index 25b2cc792a39..71806acae20e 100644 --- a/pkg/sql/parser/parse_test.go +++ b/pkg/sql/parser/parse_test.go @@ -432,8 +432,6 @@ func TestUnimplementedSyntax(t *testing.T) { {`CREATE TABLE a(b INT8) WITH OIDS`, 0, `create table with oids`, ``}, - {`CREATE TABLE a AS SELECT b WITH NO DATA`, 0, `create table as with no data`, ``}, - {`CREATE TABLE a(b INT8 REFERENCES c(x) MATCH PARTIAL`, 20305, `match partial`, ``}, {`CREATE TABLE a(b INT8, FOREIGN KEY (b) REFERENCES c(x) MATCH PARTIAL)`, 20305, `match partial`, ``}, diff --git a/pkg/sql/parser/sql.y b/pkg/sql/parser/sql.y index f744e49cd600..4983016a409e 100644 --- a/pkg/sql/parser/sql.y +++ b/pkg/sql/parser/sql.y @@ -1621,7 +1621,7 @@ func (u *sqlSymUnion) filterType() tree.FilterType { %type <[]tree.RangePartition> range_partitions %type opt_all_clause %type opt_privileges_clause -%type distinct_clause opt_with_data +%type distinct_clause opt_with_data opt_create_as_data %type distinct_on_clause %type opt_column_list insert_column_list opt_stats_columns query_stats_cols // Note that "no index" variants exist to disable custom ORDER BY syntax @@ -11850,6 +11850,7 @@ create_table_as_stmt: IfNotExists: false, Defs: $5.tblDefs(), AsSource: $8.slct(), + WithNoData: $9.bool(), StorageParams: $6.storageParams(), OnCommit: $10.createTableOnCommitSetting(), Persistence: $2.persistence(), @@ -11863,6 +11864,7 @@ create_table_as_stmt: IfNotExists: true, Defs: $8.tblDefs(), AsSource: $11.slct(), + WithNoData: $12.bool(), StorageParams: $9.storageParams(), OnCommit: $13.createTableOnCommitSetting(), Persistence: $2.persistence(), @@ -11870,9 +11872,9 @@ create_table_as_stmt: } opt_create_as_data: - /* EMPTY */ { /* no error */ } -| WITH DATA { /* SKIP DOC */ /* This is the default */ } -| WITH NO DATA { return unimplemented(sqllex, "create table as with no data") } + /* EMPTY */ { $$.val = false } +| WITH DATA { /* SKIP DOC */ $$.val = false } +| WITH NO DATA { $$.val = true } /* * Redundancy here is needed to avoid shift/reduce conflicts, diff --git a/pkg/sql/parser/testdata/create_table b/pkg/sql/parser/testdata/create_table index f57d1d708fc3..ee43f48d613a 100644 --- a/pkg/sql/parser/testdata/create_table +++ b/pkg/sql/parser/testdata/create_table @@ -1984,6 +1984,30 @@ CREATE TABLE IF NOT EXISTS a AS SELECT (*) FROM b -- fully parenthesized CREATE TABLE IF NOT EXISTS a AS SELECT * FROM b -- literals removed CREATE TABLE IF NOT EXISTS _ AS SELECT * FROM _ -- identifiers removed +parse +CREATE TABLE a AS SELECT * FROM b WITH NO DATA +---- +CREATE TABLE a AS SELECT * FROM b WITH NO DATA +CREATE TABLE a AS SELECT (*) FROM b WITH NO DATA -- fully parenthesized +CREATE TABLE a AS SELECT * FROM b WITH NO DATA -- literals removed +CREATE TABLE _ AS SELECT * FROM _ WITH NO DATA -- identifiers removed + +parse +CREATE TABLE a AS SELECT * FROM b WITH DATA +---- +CREATE TABLE a AS SELECT * FROM b -- normalized! +CREATE TABLE a AS SELECT (*) FROM b -- fully parenthesized +CREATE TABLE a AS SELECT * FROM b -- literals removed +CREATE TABLE _ AS SELECT * FROM _ -- identifiers removed + +parse +CREATE TABLE IF NOT EXISTS a AS SELECT * FROM b WITH NO DATA +---- +CREATE TABLE IF NOT EXISTS a AS SELECT * FROM b WITH NO DATA +CREATE TABLE IF NOT EXISTS a AS SELECT (*) FROM b WITH NO DATA -- fully parenthesized +CREATE TABLE IF NOT EXISTS a AS SELECT * FROM b WITH NO DATA -- literals removed +CREATE TABLE IF NOT EXISTS _ AS SELECT * FROM _ WITH NO DATA -- identifiers removed + parse CREATE TABLE a AS SELECT * FROM b ORDER BY c ---- diff --git a/pkg/sql/sem/tree/create.go b/pkg/sql/sem/tree/create.go index d1b0ac96faa6..b6e1f17d25a2 100644 --- a/pkg/sql/sem/tree/create.go +++ b/pkg/sql/sem/tree/create.go @@ -1550,9 +1550,10 @@ type CreateTable struct { // In CREATE...AS queries, Defs represents a list of ColumnTableDefs, one for // each column, and a ConstraintTableDef for each constraint on a subset of // these columns. - Defs TableDefs - AsSource *Select - Locality *Locality + Defs TableDefs + AsSource *Select + WithNoData bool + Locality *Locality } // As returns true if this table represents a CREATE TABLE ... AS statement, @@ -1609,6 +1610,9 @@ func (node *CreateTable) FormatBody(ctx *FmtCtx) { } ctx.WriteString(" AS ") ctx.FormatNode(node.AsSource) + if node.WithNoData { + ctx.WriteString(" WITH NO DATA") + } } else { ctx.WriteString(" (") ctx.FormatNode(&node.Defs) diff --git a/pkg/sql/sem/tree/pretty.go b/pkg/sql/sem/tree/pretty.go index 0ffbfe98b78f..6cedf8d0b4be 100644 --- a/pkg/sql/sem/tree/pretty.go +++ b/pkg/sql/sem/tree/pretty.go @@ -1353,6 +1353,9 @@ func (node *CreateTable) Doc(p *PrettyCfg) pretty.Doc { clauses := make([]pretty.Doc, 0, 4) if node.As() { clauses = append(clauses, p.Doc(node.AsSource)) + if node.WithNoData { + clauses = append(clauses, pretty.Keyword("WITH NO DATA")) + } } if node.PartitionByTable != nil { clauses = append(clauses, p.Doc(node.PartitionByTable))