From a444ff10872227580253f3703a8941e1277ef75d Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Sun, 4 Oct 2026 22:57:40 -0300 Subject: [PATCH] CREATE TABLE AS using bulk-insert path --- .../README.create_table_as_query.md | 10 ++++++ src/dsql/DdlNodes.epp | 32 ++++++++++++++++- src/dsql/DsqlCompilerScratch.h | 1 + src/dsql/StmtNodes.cpp | 10 ++++++ src/dsql/dsql.cpp | 34 ++++++++++++------- src/dsql/dsql_proto.h | 2 +- 6 files changed, 75 insertions(+), 14 deletions(-) diff --git a/doc/sql.extensions/README.create_table_as_query.md b/doc/sql.extensions/README.create_table_as_query.md index 6d44da9bfe4..42541c36e68 100644 --- a/doc/sql.extensions/README.create_table_as_query.md +++ b/doc/sql.extensions/README.create_table_as_query.md @@ -48,6 +48,16 @@ query expression. String literals use the connection character set, so columns created from string literals inherit the connection character set. +## Bulk insert + +With `WITH DATA`, the rows are loaded using the same bulk insert mechanism used by `gbak` restore. Data pages are +filled directly instead of storing each row through the regular record store path, which is considerably faster for +large results. This applies to regular, global temporary and local temporary tables. + +Bulk insert does not evaluate domain `CHECK` constraints. When a new column references a named domain that has a +`CHECK` constraint, the rows are loaded with a regular `INSERT` instead, so the constraint is still enforced. `NOT NULL` +is always enforced. Domains without a `CHECK` constraint do not prevent the use of bulk insert. + ## Examples ```sql diff --git a/src/dsql/DdlNodes.epp b/src/dsql/DdlNodes.epp index 0f0eada69da..cf8b977d871 100644 --- a/src/dsql/DdlNodes.epp +++ b/src/dsql/DdlNodes.epp @@ -10132,6 +10132,25 @@ void CreateRelationNode::defineQueryColumns(thread_db* tdbb, DsqlCompilerScratch dsqlScratch->derivedContext.clear(); } +// Check whether a domain has a CHECK constraint (validation). +static bool domainHasCheck(thread_db* tdbb, jrd_tra* transaction, const QualifiedName& domainName) +{ + AUTO_HANDLE(request); + bool hasCheck = false; + + FOR(REQUEST_HANDLE request TRANSACTION_HANDLE transaction) + FIRST 1 + FLD IN RDB$FIELDS + WITH FLD.RDB$SCHEMA_NAME EQ domainName.schema.c_str() AND + FLD.RDB$FIELD_NAME EQ domainName.object.c_str() + { + hasCheck = !FLD.RDB$VALIDATION_BLR.NULL; + } + END_FOR + + return hasCheck; +} + void CreateRelationNode::executeInsert(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch, jrd_tra* transaction) { @@ -10140,6 +10159,10 @@ void CreateRelationNode::executeInsert(thread_db* tdbb, DsqlCompilerScratch* dsq bool first = true; + // Bulk insert does not run domain CHECK constraints, so use it only when no column + // references a domain that has one. + bool useBulkInsert = true; + for (const auto& clause : clauses) { if (clause->type != Clause::TYPE_ADD_COLUMN) @@ -10147,6 +10170,12 @@ void CreateRelationNode::executeInsert(thread_db* tdbb, DsqlCompilerScratch* dsq const auto addColumnClause = static_cast(clause.getObject()); + if (useBulkInsert && addColumnClause->field->typeOfName.object.hasData() && + domainHasCheck(tdbb, transaction, addColumnClause->field->typeOfName)) + { + useBulkInsert = false; + } + if (!first) sql += ", "; @@ -10168,7 +10197,8 @@ void CreateRelationNode::executeInsert(thread_db* tdbb, DsqlCompilerScratch* dsq 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); + dsqlScratch->clientDialect, nullptr, nullptr, nullptr, nullptr, true, + (useBulkInsert ? DsqlCompilerScratch::FLAG_BULK_INSERT : 0)); fb_assert(traHandle == transaction); } diff --git a/src/dsql/DsqlCompilerScratch.h b/src/dsql/DsqlCompilerScratch.h index ebd3f416d34..abd99b356c8 100644 --- a/src/dsql/DsqlCompilerScratch.h +++ b/src/dsql/DsqlCompilerScratch.h @@ -80,6 +80,7 @@ class DsqlCompilerScratch : public BlrDebugWriter static const unsigned FLAG_ALLOW_CREATED_LTT_REFERENCE = 0x020000; static const unsigned FLAG_USING_STATEMENT = 0x040000; static const unsigned FLAG_ACTUAL_LTT_DDL = 0x080000; + static const unsigned FLAG_BULK_INSERT = 0x100000; static const unsigned MAX_NESTING = 512; diff --git a/src/dsql/StmtNodes.cpp b/src/dsql/StmtNodes.cpp index fdc908cd55b..f9eaa7f2c1a 100644 --- a/src/dsql/StmtNodes.cpp +++ b/src/dsql/StmtNodes.cpp @@ -10247,6 +10247,16 @@ string StoreNode::internalPrint(NodePrinter& printer) const void StoreNode::genBlr(DsqlCompilerScratch* dsqlScratch) { + if ((dsqlScratch->flags & DsqlCompilerScratch::FLAG_BULK_INSERT) && + dsqlRse && !dsqlReturning && !overrideClause.has_value()) + { + dsqlScratch->appendUChar(blr_bulk_insert); + GEN_expr(dsqlScratch, dsqlRse); + GEN_expr(dsqlScratch, target); + statement->genBlr(dsqlScratch); + return; + } + const bool useInternalAutoTrans = (dsqlScratch->flags & DsqlCompilerScratch::FLAG_IN_AUTO_TRANS_BLOCK) && dsqlReturning && !dsqlScratch->isPsql() && dsqlReturningLocalTableNumber.has_value() && diff --git a/src/dsql/dsql.cpp b/src/dsql/dsql.cpp index e33dcd4048d..e65f9689789 100644 --- a/src/dsql/dsql.cpp +++ b/src/dsql/dsql.cpp @@ -87,10 +87,12 @@ using namespace Firebird; static ULONG get_request_info(thread_db*, DsqlRequest*, ULONG, UCHAR*); static dsql_dbb* init(Jrd::thread_db*, Jrd::Attachment*); -static DsqlRequest* prepareRequest(thread_db*, dsql_dbb*, jrd_tra*, ULONG, const TEXT*, USHORT, unsigned, bool); -static DsqlRequest* safePrepareRequest(thread_db*, dsql_dbb*, jrd_tra*, ULONG, const TEXT*, USHORT, unsigned, bool); +static DsqlRequest* prepareRequest(thread_db*, dsql_dbb*, jrd_tra*, ULONG, const TEXT*, USHORT, unsigned, bool, + unsigned scratchFlags = 0); +static DsqlRequest* safePrepareRequest(thread_db*, dsql_dbb*, jrd_tra*, ULONG, const TEXT*, USHORT, unsigned, bool, + unsigned scratchFlags = 0); static RefPtr prepareStatement(thread_db*, dsql_dbb*, jrd_tra*, ULONG, const TEXT*, USHORT, - unsigned, bool, ntrace_result_t* traceResult); + unsigned, bool, ntrace_result_t* traceResult, unsigned scratchFlags = 0); static UCHAR* put_item(UCHAR, const USHORT, const UCHAR*, UCHAR*, const UCHAR* const); static void sql_info(thread_db*, DsqlRequest*, ULONG, const UCHAR*, ULONG, UCHAR*); static UCHAR* var_info(const dsql_msg*, const UCHAR*, const UCHAR* const, UCHAR*, @@ -339,7 +341,7 @@ void DSQL_execute_immediate(thread_db* tdbb, Jrd::Attachment* attachment, jrd_tr ULONG length, const TEXT* string, USHORT dialect, IMessageMetadata* in_meta, const UCHAR* in_msg, IMessageMetadata* out_meta, UCHAR* out_msg, - bool isInternalRequest) + bool isInternalRequest, unsigned scratchFlags) { SET_TDBB(tdbb); @@ -349,7 +351,7 @@ void DSQL_execute_immediate(thread_db* tdbb, Jrd::Attachment* attachment, jrd_tr try { dsqlRequest = safePrepareRequest(tdbb, database, *tra_handle, length, string, dialect, - 0, isInternalRequest); + 0, isInternalRequest, scratchFlags); const auto dsqlStatement = dsqlRequest->getDsqlStatement(); @@ -454,10 +456,14 @@ static dsql_dbb* init(thread_db* tdbb, Jrd::Attachment* attachment) // Use SEH frame when preparing user requests to catch possible stack overflows static DsqlRequest* safePrepareRequest(thread_db* tdbb, dsql_dbb* database, jrd_tra* transaction, - ULONG textLength, const TEXT* text, USHORT clientDialect, unsigned prepareFlags, bool isInternalRequest) + ULONG textLength, const TEXT* text, USHORT clientDialect, unsigned prepareFlags, bool isInternalRequest, + unsigned scratchFlags) { if (isInternalRequest) - return prepareRequest(tdbb, database, transaction, textLength, text, clientDialect, prepareFlags, true); + { + return prepareRequest(tdbb, database, transaction, textLength, text, clientDialect, prepareFlags, true, + scratchFlags); + } //#define SQL_LOG_PRINT #ifdef SQL_LOG_PRINT // for debugging @@ -467,7 +473,8 @@ static DsqlRequest* safePrepareRequest(thread_db* tdbb, dsql_dbb* database, jrd_ #ifdef WIN_NT START_CHECK_FOR_EXCEPTIONS(NULL); #endif - return prepareRequest(tdbb, database, transaction, textLength, text, clientDialect, prepareFlags, false); + return prepareRequest(tdbb, database, transaction, textLength, text, clientDialect, prepareFlags, false, + scratchFlags); #ifdef WIN_NT END_CHECK_FOR_EXCEPTIONS(NULL); @@ -478,7 +485,8 @@ static DsqlRequest* safePrepareRequest(thread_db* tdbb, dsql_dbb* database, jrd_ // Prepare a request for execution. // Note: caller is responsible for pool handling. static DsqlRequest* prepareRequest(thread_db* tdbb, dsql_dbb* database, jrd_tra* transaction, - ULONG textLength, const TEXT* text, USHORT clientDialect, unsigned prepareFlags, bool isInternalRequest) + ULONG textLength, const TEXT* text, USHORT clientDialect, unsigned prepareFlags, bool isInternalRequest, + unsigned scratchFlags) { TraceDSQLPrepare trace(database->dbb_attachment, transaction, textLength, text, isInternalRequest); @@ -486,7 +494,7 @@ static DsqlRequest* prepareRequest(thread_db* tdbb, dsql_dbb* database, jrd_tra* try { auto statement = prepareStatement(tdbb, database, transaction, textLength, text, - clientDialect, prepareFlags, isInternalRequest, &traceResult); + clientDialect, prepareFlags, isInternalRequest, &traceResult, scratchFlags); auto dsqlRequest = statement->createRequest(tdbb, database); @@ -518,7 +526,7 @@ static DsqlRequest* prepareRequest(thread_db* tdbb, dsql_dbb* database, jrd_tra* // Note: caller is responsible for pool handling. static RefPtr prepareStatement(thread_db* tdbb, dsql_dbb* database, jrd_tra* transaction, ULONG textLength, const TEXT* text, USHORT clientDialect, unsigned prepareFlags, bool isInternalRequest, - ntrace_result_t* traceResult) + ntrace_result_t* traceResult, unsigned scratchFlags) { Database* const dbb = tdbb->getDatabase(); @@ -562,7 +570,7 @@ static RefPtr prepareStatement(thread_db* tdbb, dsql_dbb* databas } string textStr(text, textLength); - const bool isStatementCacheActive = database->dbb_statement_cache->isActive() && + const bool isStatementCacheActive = database->dbb_statement_cache->isActive() && !scratchFlags && (transaction ? (!transaction->isDdl()) : true); RefPtr dsqlStatement; @@ -605,6 +613,8 @@ static RefPtr prepareStatement(thread_db* tdbb, dsql_dbb* databas if (isInternalRequest) scratch->flags |= DsqlCompilerScratch::FLAG_INTERNAL_REQUEST; + scratch->flags |= scratchFlags; + Parser parser(tdbb, *scratchPool, statementPool, scratch, clientDialect, dbDialect, (prepareFlags & IStatement::PREPARE_REQUIRE_SEMICOLON), diff --git a/src/dsql/dsql_proto.h b/src/dsql/dsql_proto.h index 89a6e2b4c35..0c55d6a51b8 100644 --- a/src/dsql/dsql_proto.h +++ b/src/dsql/dsql_proto.h @@ -39,7 +39,7 @@ void DSQL_execute(Jrd::thread_db*, Jrd::jrd_tra**, Jrd::DsqlRequest*, Firebird::IMessageMetadata*, const UCHAR*, Firebird::IMessageMetadata*, UCHAR*); void DSQL_execute_immediate(Jrd::thread_db*, Jrd::Attachment*, Jrd::jrd_tra**, ULONG, const TEXT*, USHORT, Firebird::IMessageMetadata*, const UCHAR*, - Firebird::IMessageMetadata*, UCHAR*, bool); + Firebird::IMessageMetadata*, UCHAR*, bool, unsigned scratchFlags = 0); void DSQL_free_statement(Jrd::thread_db*, Jrd::DsqlRequest*, USHORT); Jrd::DsqlRequest* DSQL_prepare(Jrd::thread_db*, Jrd::Attachment*, Jrd::jrd_tra*, ULONG, const TEXT*, USHORT, unsigned, Firebird::Array*, Firebird::Array*, bool);