From f58ae321df9380cc1226ebb5522e9c14fb2a1153 Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Fri, 5 Jun 2026 20:05:18 -0300 Subject: [PATCH 1/3] Feature #1181 - CREATE TABLE AS --- .../README.create_table_as_query.md | 55 +++++ src/dsql/DdlNodes.epp | 188 ++++++++++++++++++ src/dsql/DdlNodes.h | 9 +- src/dsql/parse-conflicts.txt | 2 +- src/dsql/parse.y | 42 ++++ 5 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 doc/sql.extensions/README.create_table_as_query.md diff --git a/doc/sql.extensions/README.create_table_as_query.md b/doc/sql.extensions/README.create_table_as_query.md new file mode 100644 index 00000000000..c3fb7e1aa99 --- /dev/null +++ b/doc/sql.extensions/README.create_table_as_query.md @@ -0,0 +1,55 @@ +# `CREATE TABLE ... AS ` (FB 6.0) + +Tables may be created from a query result using the following syntax: + +```sql +CREATE [{GLOBAL | LOCAL} TEMPORARY TABLE] [IF NOT EXISTS] TABLE + [ ( [, ...]) ] + AS + [WITH [NO] DATA] + [ON COMMIT {DELETE | PRESERVE} ROWS] +``` + +The new table columns are derived from the query select list. If a column name list is specified, it must have +the same number of columns as the query result. + +If no column name list is specified, column names are taken from the query output names. Unnamed expressions must +be explicitly aliased. + +`WITH DATA` is the default and inserts the query result into the newly created table. `WITH NO DATA` creates only +the table definition. + +For global and local temporary tables, normal temporary-table data lifetime rules apply. Package temporary tables +do not support this syntax. + +## Character Sets + +For `CHAR` and `VARCHAR` columns, the database default character set is not used. The character set and collation +of the new column are copied from the corresponding query expression. + +String literals use the connection character set, so columns created from string literals inherit the connection +character set. + +## Examples + +```sql +CREATE TABLE employee_copy AS + SELECT emp_no, first_name, last_name + FROM employee; + +CREATE TABLE employee_names (id, full_name) AS + SELECT emp_no, first_name || ' ' || last_name + FROM employee + WITH NO DATA; + +CREATE GLOBAL TEMPORARY TABLE session_report AS + SELECT emp_no, salary + FROM employee + WITH DATA + ON COMMIT PRESERVE ROWS; + +CREATE LOCAL TEMPORARY TABLE tx_work AS + SELECT emp_no, salary + FROM employee + WITH NO DATA; +``` diff --git a/src/dsql/DdlNodes.epp b/src/dsql/DdlNodes.epp index f1a5dbb0d8e..e3694fd4683 100644 --- a/src/dsql/DdlNodes.epp +++ b/src/dsql/DdlNodes.epp @@ -60,6 +60,7 @@ #include "../jrd/vio_proto.h" #include "../jrd/idx_proto.h" #include "../dsql/ddl_proto.h" +#include "../dsql/dsql_proto.h" #include "../dsql/errd_proto.h" #include "../dsql/gen_proto.h" #include "../dsql/make_proto.h" @@ -9646,6 +9647,10 @@ string CreateRelationNode::internalPrint(NodePrinter& printer) const NODE_PRINT(printer, externalFile); NODE_PRINT(printer, tempFlag); NODE_PRINT(printer, tempRowsFlag); + NODE_PRINT(printer, queryColumns); + NODE_PRINT(printer, querySelectExpr); + NODE_PRINT(printer, querySource); + NODE_PRINT(printer, withData); return "CreateRelationNode"; } @@ -9669,6 +9674,9 @@ void CreateRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScrat if (createIfNotExistsOnly && !DYN_UTIL_check_unique_name_nothrow(tdbb, name, obj_relation)) return; + if (querySelectExpr) + defineQueryColumns(tdbb, dsqlScratch); + saveRelation(tdbb, dsqlScratch, name, false, true); if (externalFile) @@ -9701,6 +9709,9 @@ void CreateRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScrat // Update DSQL cache METD_drop_relation(transaction, name); + if (withData) + executeInsert(tdbb, dsqlScratch, transaction); + return; } @@ -9922,9 +9933,186 @@ void CreateRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScrat // Update DSQL cache METD_drop_relation(transaction, name); + if (withData) + executeInsert(tdbb, dsqlScratch, transaction); + savePoint.release(); // everything is ok } +void CreateRelationNode::defineQueryColumns(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch) +{ + fb_assert(querySelectExpr); + + dsqlScratch->resetContextStack(); + dsqlScratch->unionContext.clear(); + dsqlScratch->derivedContext.clear(); + + const auto rse = PASS1_rse(dsqlScratch, querySelectExpr); + const auto items = rse->dsqlSelectList; + + if (queryColumns && queryColumns->items.getCount() != items->items.getCount()) + { + status_exception::raise( + Arg::Gds(isc_sqlerr) << Arg::Num(-607) << + Arg::Gds(isc_dsql_command_err) << + Arg::Gds(isc_num_field_err)); + } + + ObjectsArray names; + FB_SIZE_T position = 0; + + for (auto item : items->items) + { + MetaName fieldName; + + if (queryColumns) + fieldName = nodeAs(queryColumns->items[position])->dsqlName; + else + { + const ValueExprNode* nameNode = item; + const char* aliasName = nullptr; + + while (nodeIs(nameNode) || nodeIs(nameNode) || + nodeIs(nameNode) || nodeAs(nameNode)) + { + if (const auto aliasNode = nodeAs(nameNode)) + { + if (!aliasName) + aliasName = aliasNode->name.c_str(); + + nameNode = aliasNode->value; + } + else if (const auto mapNode = nodeAs(nameNode)) + nameNode = mapNode->map->map_node; + else if (const auto derivedField = nodeAs(nameNode)) + { + if (!aliasName) + aliasName = derivedField->name.c_str(); + + nameNode = derivedField->value; + } + else if (const auto referenceNode = nodeAs(nameNode)) + { + if (!aliasName) + aliasName = referenceNode->getName(); + + nameNode = nullptr; + } + } + + if (aliasName) + fieldName = aliasName; + else if (const auto fieldNode = nodeAs(nameNode)) + fieldName = fieldNode->dsqlField->fld_name; + else + { + status_exception::raise( + Arg::Gds(isc_sqlerr) << Arg::Num(-607) << + Arg::Gds(isc_dsql_command_err) << + Arg::Gds(isc_specify_field_err)); + } + } + + for (const auto& name : names) + { + if (name == fieldName) + { + status_exception::raise( + Arg::Gds(isc_sqlerr) << Arg::Num(-104) << + Arg::Gds(isc_dsql_command_err) << + Arg::Gds(isc_dsql_col_more_than_once_view) << Arg::Str(fieldName.c_str())); + } + } + + names.add() = fieldName; + + dsc desc; + DsqlDescMaker::fromNode(dsqlScratch, &desc, item); + + if (!desc.dsc_dtype) + { + status_exception::raise( + Arg::Gds(isc_sqlerr) << Arg::Num(-607) << + Arg::Gds(isc_dsql_command_err) << + Arg::Gds(isc_dsql_datatype_err)); + } + + auto clause = FB_NEW_POOL(dsqlScratch->getPool()) AddColumnClause(dsqlScratch->getPool()); + clause->field = FB_NEW_POOL(dsqlScratch->getPool()) dsql_fld(dsqlScratch->getPool()); + + auto field = clause->field; + field->fld_name = fieldName; + field->dtype = desc.dsc_dtype; + field->length = desc.dsc_length; + field->scale = desc.dsc_scale; + field->subType = desc.dsc_sub_type; + + if (desc.dsc_flags & DSC_nullable) + field->flags |= FLD_nullable; + + if (desc.isText() || (desc.isBlob() && desc.getBlobSubType() == isc_blob_text)) + { + field->charSetId = desc.getCharSet(); + field->collationId = desc.getCollation(); + } + + if (desc.isText()) + { + const USHORT adjust = (desc.dsc_dtype == dtype_varying) ? sizeof(USHORT) : 0; + const USHORT bpc = METD_get_charset_bpc(dsqlScratch->getTransaction(), field->charSetId.value_or(CS_NONE)); + field->charLength = (field->length - adjust) / bpc; + } + else if (desc.isBlob()) + field->segLength = 80; + + field->setExactPrecision(); + clauses.add(clause); + + ++position; + } + + dsqlScratch->resetContextStack(); + dsqlScratch->unionContext.clear(); + dsqlScratch->derivedContext.clear(); +} + +void CreateRelationNode::executeInsert(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, + jrd_tra* transaction) +{ + string sql; + sql.printf("insert into %s (", name.toQuotedString().c_str()); + + bool first = true; + + for (const auto& clause : clauses) + { + if (clause->type != Clause::TYPE_ADD_COLUMN) + continue; + + const auto addColumnClause = static_cast(clause.getObject()); + + if (!first) + sql += ", "; + + first = false; + sql += addColumnClause->field->fld_name.toQuotedString(); + } + + sql += ") "; + sql += querySource; + + jrd_tra* traHandle = transaction; + const auto attachment = tdbb->getAttachment(); + + AutoSetRestoreFlag autoLttReferences(&dsqlScratch->flags, + DsqlCompilerScratch::FLAG_ALLOW_CREATED_LTT_REFERENCE, tempFlag == REL_temp_ltt); + + DSQL_execute_immediate(tdbb, attachment, &traHandle, sql.length(), sql.c_str(), + dsqlScratch->clientDialect, nullptr, nullptr, nullptr, nullptr, true); + + fb_assert(traHandle == transaction); +} + // Starting from the elements in a table definition, locate the PK columns if given in a // separate table constraint declaration. const ObjectsArray* CreateRelationNode::findPkColumns() diff --git a/src/dsql/DdlNodes.h b/src/dsql/DdlNodes.h index 8834eb524a9..aa6fe04afd8 100644 --- a/src/dsql/DdlNodes.h +++ b/src/dsql/DdlNodes.h @@ -1763,7 +1763,8 @@ class CreateRelationNode final : public RelationNode CreateRelationNode(MemoryPool& p, RelationSourceNode* aDsqlNode, const Firebird::string* aExternalFile = NULL) : RelationNode(p, aDsqlNode), - externalFile(aExternalFile) + externalFile(aExternalFile), + querySource(p) { } @@ -1799,12 +1800,18 @@ class CreateRelationNode final : public RelationNode private: const Firebird::ObjectsArray* findPkColumns(); + void defineQueryColumns(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch); + void executeInsert(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction); void defineLocalTempTable(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction); public: const Firebird::string* externalFile; bool createIfNotExistsOnly = false; bool packagePrivate = false; + NestConst queryColumns; + NestConst querySelectExpr; + Firebird::string querySource; + bool withData = false; }; diff --git a/src/dsql/parse-conflicts.txt b/src/dsql/parse-conflicts.txt index cd0e6b8e56a..c1a81f741c1 100644 --- a/src/dsql/parse-conflicts.txt +++ b/src/dsql/parse-conflicts.txt @@ -1 +1 @@ -147 shift/reduce conflicts, 7 reduce/reduce conflicts. +152 shift/reduce conflicts, 7 reduce/reduce conflicts. diff --git a/src/dsql/parse.y b/src/dsql/parse.y index 953a1c6462d..c435704caaf 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -2438,6 +2438,22 @@ table_clause { $$ = $3; } + | simple_table_name column_parens_opt AS select_expr with_data_opt + { + const auto node = newNode($1); + node->queryColumns = $2; + node->querySelectExpr = $4; + node->querySource = makeParseStr(YYPOSNARG(4), YYPOSNARG(4)); + node->withData = $5; + $$ = node; + } + ; + +%type with_data_opt +with_data_opt + : /* nothing */ { $$ = true; } + | WITH DATA { $$ = true; } + | WITH NO DATA { $$ = false; } ; %type table_attributes() @@ -2477,6 +2493,19 @@ gtt_table_clause { $$ = $2; } + | simple_table_name column_parens_opt AS select_expr with_data_opt + { + $$ = newNode($1); + $$->tempFlag = REL_temp_gtt; + $$->queryColumns = $2; + $$->querySelectExpr = $4; + $$->querySource = makeParseStr(YYPOSNARG(4), YYPOSNARG(4)); + $$->withData = $5; + } + gtt_subclauses_opt($6) + { + $$ = $6; + } ; %type gtt_subclauses_opt() @@ -2517,6 +2546,19 @@ ltt_table_clause { $$ = $2; } + | simple_table_name column_parens_opt AS select_expr with_data_opt + { + $$ = newNode($1); + $$->tempFlag = REL_temp_ltt; + $$->queryColumns = $2; + $$->querySelectExpr = $4; + $$->querySource = makeParseStr(YYPOSNARG(4), YYPOSNARG(4)); + $$->withData = $5; + } + ltt_subclause_opt($6) + { + $$ = $6; + } ; %type packaged_table_clause From 5451ae854978f5ed6e855747fbfa5faefe0576ac Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Thu, 11 Jun 2026 20:02:02 -0300 Subject: [PATCH 2/3] Fix problems with temporary tables --- src/dsql/DdlNodes.epp | 4 ++++ src/dsql/parse.y | 52 ++++++++++++++++++++++--------------------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/dsql/DdlNodes.epp b/src/dsql/DdlNodes.epp index e3694fd4683..e3be5b8a7cd 100644 --- a/src/dsql/DdlNodes.epp +++ b/src/dsql/DdlNodes.epp @@ -10107,6 +10107,10 @@ void CreateRelationNode::executeInsert(thread_db* tdbb, DsqlCompilerScratch* dsq AutoSetRestoreFlag autoLttReferences(&dsqlScratch->flags, DsqlCompilerScratch::FLAG_ALLOW_CREATED_LTT_REFERENCE, tempFlag == REL_temp_ltt); + // Clear TDBB_use_db_page_space so the INSERT uses per-transaction temp pages for GTT/LTT, + // not the structural base pages that were used during table creation. + AutoSetRestoreFlag noDbPageSpace(&tdbb->tdbb_flags, TDBB_use_db_page_space, false); + DSQL_execute_immediate(tdbb, attachment, &traHandle, sql.length(), sql.c_str(), dsqlScratch->clientDialect, nullptr, nullptr, nullptr, nullptr, true); diff --git a/src/dsql/parse.y b/src/dsql/parse.y index c435704caaf..e77d3dd202c 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -2428,6 +2428,21 @@ db_rem_option($alterDatabaseNode) // CREATE TABLE +// Helper rule to capture AS for table creation with a regular trailing action. +// A mid-rule action cannot use YYPOSNARG correctly. +%type table_as_query_clause +table_as_query_clause + : simple_table_name column_parens_opt AS select_expr with_data_opt + { + const auto node = newNode($1); + node->queryColumns = $2; + node->querySelectExpr = $4; + node->querySource = makeParseStr(YYPOSNARG(4), YYPOSNARG(4)); + node->withData = $5; + $$ = node; + } + ; + %type table_clause table_clause : simple_table_name external_file @@ -2438,14 +2453,9 @@ table_clause { $$ = $3; } - | simple_table_name column_parens_opt AS select_expr with_data_opt + | table_as_query_clause { - const auto node = newNode($1); - node->queryColumns = $2; - node->querySelectExpr = $4; - node->querySource = makeParseStr(YYPOSNARG(4), YYPOSNARG(4)); - node->withData = $5; - $$ = node; + $$ = $1; } ; @@ -2493,18 +2503,14 @@ gtt_table_clause { $$ = $2; } - | simple_table_name column_parens_opt AS select_expr with_data_opt + | table_as_query_clause { - $$ = newNode($1); - $$->tempFlag = REL_temp_gtt; - $$->queryColumns = $2; - $$->querySelectExpr = $4; - $$->querySource = makeParseStr(YYPOSNARG(4), YYPOSNARG(4)); - $$->withData = $5; + $1->tempFlag = REL_temp_gtt; + $$ = $1; } - gtt_subclauses_opt($6) + gtt_subclauses_opt($2) { - $$ = $6; + $$ = $2; } ; @@ -2546,18 +2552,14 @@ ltt_table_clause { $$ = $2; } - | simple_table_name column_parens_opt AS select_expr with_data_opt + | table_as_query_clause { - $$ = newNode($1); - $$->tempFlag = REL_temp_ltt; - $$->queryColumns = $2; - $$->querySelectExpr = $4; - $$->querySource = makeParseStr(YYPOSNARG(4), YYPOSNARG(4)); - $$->withData = $5; + $1->tempFlag = REL_temp_ltt; + $$ = $1; } - ltt_subclause_opt($6) + ltt_subclause_opt($2) { - $$ = $6; + $$ = $2; } ; From 3eed5d5d8fa280870a75bbc111b063cf24da2dc4 Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Thu, 11 Jun 2026 20:06:33 -0300 Subject: [PATCH 3/3] Improve documentation --- .../README.create_table_as_query.md | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/doc/sql.extensions/README.create_table_as_query.md b/doc/sql.extensions/README.create_table_as_query.md index c3fb7e1aa99..3d300e589a2 100644 --- a/doc/sql.extensions/README.create_table_as_query.md +++ b/doc/sql.extensions/README.create_table_as_query.md @@ -16,8 +16,8 @@ the same number of columns as the query result. If no column name list is specified, column names are taken from the query output names. Unnamed expressions must be explicitly aliased. -`WITH DATA` is the default and inserts the query result into the newly created table. `WITH NO DATA` creates only -the table definition. +`WITH DATA` is the default and inserts the query result into the newly created table in the same transaction. +`WITH NO DATA` creates only the table definition. For global and local temporary tables, normal temporary-table data lifetime rules apply. Package temporary tables do not support this syntax. @@ -53,3 +53,56 @@ CREATE LOCAL TEMPORARY TABLE tx_work AS FROM employee WITH NO DATA; ``` + +## ISQL behavior + +ISQL in AUTODDL mode (the default) uses separate transactions for DDL and DML statements. +When `CREATE TABLE ... AS ` is executed with `WITH DATA`, the table creation and data population occur in the +DDL transaction. + +For regular tables, the inserted rows are not visible to the DML transaction until the DDL transaction is committed. +For temporary tables, this behavior is even more surprising because the rows belong to the DDL transaction and are +therefore not visible to the DML transaction at all. + +For example: + +```sql +SQL> CREATE TABLE T1 AS SELECT 1 A FROM RDB$DATABASE; +SQL> SELECT * FROM T1; + +SQL> COMMIT; +SQL> SELECT * FROM T1; + + A +============ + 1 +``` + +With a temporary table: + +```sql +SQL> CREATE GLOBAL TEMPORARY TABLE T1 AS SELECT 1 A FROM RDB$DATABASE; +SQL> SELECT * FROM T1; + +SQL> COMMIT; +SQL> SELECT * FROM T1; +``` + +The table exists, but no rows are returned because the data was inserted in the DDL transaction and is not visible to +the DML transaction. + +To avoid this behavior, disable AUTODDL before executing the statement: + +```sql +SQL> SET AUTODDL OFF; + +SQL> CREATE GLOBAL TEMPORARY TABLE T1 AS SELECT 1 A FROM RDB$DATABASE; +SQL> SELECT * FROM T1; + + A +============ + 1 +``` + +When AUTODDL is disabled, both the table creation and data population occur in the current transaction, making the +inserted rows immediately visible.