From d9eb4803eb3c92a49a0638765ca96b6564140b2d Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Thu, 11 Jun 2026 08:19:45 -0300 Subject: [PATCH 1/2] Declared Local Temporary Tables in PSQL --- .../README.declared_local_temporary_tables.md | 209 ++++++++++++ src/dsql/DsqlCompilerScratch.cpp | 16 + src/dsql/DsqlCompilerScratch.h | 4 + src/dsql/ExprNodes.cpp | 3 +- src/dsql/StmtNodes.cpp | 323 +++++++++++++++--- src/dsql/StmtNodes.h | 13 +- src/dsql/dsql.cpp | 4 +- src/dsql/dsql.h | 4 +- src/dsql/parse.y | 17 +- src/dsql/pass1.cpp | 34 +- src/jrd/RecordBuffer.cpp | 32 +- src/jrd/RecordBuffer.h | 6 + src/jrd/RecordSourceNodes.cpp | 10 +- src/jrd/exe.cpp | 5 +- src/jrd/exe.h | 1 + src/jrd/recsrc/LocalTableStream.cpp | 19 +- 16 files changed, 631 insertions(+), 69 deletions(-) create mode 100644 doc/sql.extensions/README.declared_local_temporary_tables.md diff --git a/doc/sql.extensions/README.declared_local_temporary_tables.md b/doc/sql.extensions/README.declared_local_temporary_tables.md new file mode 100644 index 00000000000..1176fff3b40 --- /dev/null +++ b/doc/sql.extensions/README.declared_local_temporary_tables.md @@ -0,0 +1,209 @@ +# Declared Local Temporary Tables in PSQL (FB 6.0) + +Firebird 6.0 supports declaring local temporary tables inside PSQL blocks, procedures, functions and triggers. + +Declared Local Temporary Tables are local to the compiled PSQL object or `EXECUTE BLOCK`. They are not stored in +database metadata and they do not require an `ON COMMIT` clause. Their data is scoped to the current execution frame. + +This feature is distinct from: + +- SQL-created Local Temporary Tables (`CREATE LOCAL TEMPORARY TABLE ...`), whose definitions are attachment-private + DDL objects. +- Packaged temporary tables (`TEMPORARY TABLE ...` inside packages), whose definitions are persistent package-owned + metadata. + +## Syntax + +Declared Local Temporary Tables are declared in the same declaration section as variables, cursors and subroutines. + +```sql +DECLARE LOCAL TEMPORARY TABLE +( + [, ...] +); +``` + +There is no `ON COMMIT` clause. + +Example in `EXECUTE BLOCK`: + +```sql +set term !; + +execute block returns (n integer) +as + declare local temporary table t ( + id integer not null, + val varchar(20) + ); +begin + insert into t(id, val) values (1, 'a'); + insert into t(id, val) values (2, 'b'); + + update t set id = id + 10 where id = 1; + delete from t where id = 2; + + select count(*) from t into n; + suspend; +end! + +set term ;! +``` + +Example in a procedure: + +```sql +set term !; + +create procedure p_count_values returns (n integer) +as + declare local temporary table t ( + id integer not null + ); +begin + insert into t(id) values (1); + insert into t(id) values (2); + + select count(*) from t into n; + suspend; +end! + +set term ;! +``` + +## Semantics + +Declared Local Temporary Tables have execution-frame data scope. + +- The table structure is part of the compiled PSQL statement, procedure, function or trigger. +- Rows are private to the current execution frame. +- Rows are discarded when that execution frame finishes. +- Transaction commit or rollback does not define the table lifetime; there is no `ON COMMIT` behavior. +- An autonomous transaction block inside the same execution frame sees the same rows. +- Recursive calls reuse the same compiled table structure, but each recursive execution frame has separate rows. + +Recursive example: + +```sql +set term !; + +create procedure p_ltt_rec(d integer) + returns (depth integer, cnt_before integer, cnt_after integer) +as + declare local temporary table t ( + id integer + ); +begin + insert into t values (:d); + select count(*) from t into cnt_before; + + if (d > 0) then + begin + for + select depth, cnt_before, cnt_after + from p_ltt_rec(:d - 1) + into depth, cnt_before, cnt_after + do + suspend; + end + + select count(*) from t into cnt_after; + depth = d; + suspend; +end! + +set term ;! +``` + +Each row returned by `p_ltt_rec(2)` reports `cnt_before = 1` and `cnt_after = 1`, because every recursive frame has +its own table data. + +Autonomous transaction example: + +```sql +set term !; + +execute block returns (n integer) +as + declare local temporary table t ( + id integer + ); +begin + insert into t values (10); + + in autonomous transaction do + begin + select count(*) from t into n; + end + + suspend; +end! + +set term ;! +``` + +The autonomous block sees the row inserted by its parent execution frame. + +## Visibility and Name Resolution + +Declared Local Temporary Tables are visible only to SQL statements compiled inside the same PSQL scope. + +- Unqualified references to the declared name resolve to the declared local table. +- The declaration name cannot be schema-qualified or package-qualified. +- The table is not present in `RDB$RELATIONS`, `RDB$RELATION_FIELDS`, monitoring metadata or other persistent metadata. +- Dynamic SQL, including `EXECUTE STATEMENT`, is parsed separately and does not see declared local temporary tables. + +The name shares the local declaration namespace. It cannot duplicate an input parameter, output parameter, local +variable, cursor or another declared local table in the same declaration scope. + +## Supported Operations + +Declared Local Temporary Tables can be used by regular DML statements compiled in the same PSQL scope: + +```sql +insert into t (...) values (...); +select ... from t ...; +update t set ... where ...; +delete from t where ...; +``` + +They can be used in subqueries, joins and cursor loops like other record sources, subject to the restrictions below. + +## Restrictions + +Declared Local Temporary Tables intentionally support a small table definition surface. + +Column restrictions: + +- `DEFAULT` values are not supported. +- `COMPUTED BY` columns are not supported. +- `IDENTITY` columns are not supported. +- Array columns are not supported. +- Field-level `NOT NULL` is supported, but only as an unnamed constraint. + +Constraint restrictions: + +- Table constraints are not supported. +- Primary keys are not supported. +- Foreign keys are not supported. +- Check constraints are not supported. +- Named constraints are not supported. + +Other restrictions: + +- Indexes are not supported. +- Triggers on declared local temporary tables are not supported. +- Explicit privileges are not supported. +- `ALTER TABLE`, `DROP TABLE`, `CREATE INDEX`, `ALTER INDEX` and `DROP INDEX` are not valid for declared local + temporary tables. +- Declared local temporary tables cannot be referenced by persistent metadata objects outside the PSQL object where + they are declared. + +## Notes + +Declared Local Temporary Tables are useful for temporary intermediate data that should not be exposed through database +metadata and should be automatically isolated per PSQL execution frame. + +Use SQL-created Local Temporary Tables when the temporary table definition must be created and addressed dynamically by +the attachment. Use packaged temporary tables when the definition must be part of package metadata and visible through +package name resolution. diff --git a/src/dsql/DsqlCompilerScratch.cpp b/src/dsql/DsqlCompilerScratch.cpp index da42cc39241..b042f0386b8 100644 --- a/src/dsql/DsqlCompilerScratch.cpp +++ b/src/dsql/DsqlCompilerScratch.cpp @@ -553,6 +553,22 @@ dsql_var* DsqlCompilerScratch::resolveVariable(const MetaName& varName) return NULL; } +DeclareLocalTableNode* DsqlCompilerScratch::getLocalTable(const MetaName& name) +{ + DeclareLocalTableNode* table = nullptr; + localTableNames.get(name, table); + + if (!table && mainScratch) + table = mainScratch->getLocalTable(name); + + return table; +} + +void DsqlCompilerScratch::putLocalTable(DeclareLocalTableNode* table) +{ + localTableNames.put(table->dsqlName, table); +} + // Generate BLR for a return. void DsqlCompilerScratch::genReturn(bool eosFlag) { diff --git a/src/dsql/DsqlCompilerScratch.h b/src/dsql/DsqlCompilerScratch.h index 233567c4dc2..8ce76e26c13 100644 --- a/src/dsql/DsqlCompilerScratch.h +++ b/src/dsql/DsqlCompilerScratch.h @@ -97,6 +97,7 @@ class DsqlCompilerScratch : public BlrDebugWriter labels(p), cursors(p), localTables(p), + localTableNames(p), aliasRelationPrefix(p), package(p), currCtes(p), @@ -198,6 +199,8 @@ class DsqlCompilerScratch : public BlrDebugWriter dsql_var* makeVariable(dsql_fld*, const char*, const dsql_var::Type type, USHORT, USHORT, std::optional = std::nullopt); dsql_var* resolveVariable(const MetaName& varName); + DeclareLocalTableNode* getLocalTable(const MetaName& name); + void putLocalTable(DeclareLocalTableNode* table); void genReturn(bool eosFlag = false); void genParameters(Firebird::Array >& parameters, @@ -323,6 +326,7 @@ class DsqlCompilerScratch : public BlrDebugWriter Firebird::Array cursors; // Cursors USHORT localTableNumber = 0; // Local table number Firebird::Array localTables; // Local tables + Firebird::LeftPooledMap localTableNames; USHORT inSelectList = 0; // now processing "select list" USHORT inWhereClause = 0; // processing "where clause" USHORT inGroupByClause = 0; // processing "group by clause" diff --git a/src/dsql/ExprNodes.cpp b/src/dsql/ExprNodes.cpp index 0843f08a9eb..6b1fda3fb02 100644 --- a/src/dsql/ExprNodes.cpp +++ b/src/dsql/ExprNodes.cpp @@ -6681,7 +6681,8 @@ void FieldNode::genBlr(DsqlCompilerScratch* dsqlScratch) if (dsqlIndices) dsqlScratch->appendUChar(blr_index); - if (DDL_ids(dsqlScratch)) + if (DDL_ids(dsqlScratch) || + (dsqlContext->ctx_relation && (dsqlContext->ctx_relation->rel_flags & REL_local_table))) { dsqlScratch->appendUChar(blr_fid); GEN_stuff_context(dsqlScratch, dsqlContext); diff --git a/src/dsql/StmtNodes.cpp b/src/dsql/StmtNodes.cpp index b24dda9f708..14863649a1d 100644 --- a/src/dsql/StmtNodes.cpp +++ b/src/dsql/StmtNodes.cpp @@ -1756,12 +1756,14 @@ DmlNode* DeclareLocalTableNode::parse(thread_db* tdbb, MemoryPool& pool, Compile fieldCount = blrReader.getWord(); node->format = Format::newFormat(pool, fieldCount); node->format->fmt_length = FLAG_BYTES(fieldCount); + node->notNullFields.grow(fieldCount); for (USHORT fieldNum = 0; fieldNum < fieldCount; ++fieldNum) { dsc& fmtDesc = node->format->fmt_desc[fieldNum]; - //// TODO: Support NOT NULL fields with blr_not_nullable. - PAR_desc(tdbb, csb, &fmtDesc, nullptr); + ItemInfo itemInfo; + PAR_desc(tdbb, csb, &fmtDesc, &itemInfo); + node->notNullFields[fieldNum] = !itemInfo.nullable; if (fmtDesc.dsc_dtype >= dtype_aligned) node->format->fmt_length = FB_ALIGN(node->format->fmt_length, type_alignments[fmtDesc.dsc_dtype]); @@ -1785,8 +1787,123 @@ DmlNode* DeclareLocalTableNode::parse(thread_db* tdbb, MemoryPool& pool, Compile DeclareLocalTableNode* DeclareLocalTableNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) { + if (dsqlName.isEmpty()) + { + tableNumber = dsqlScratch->localTableNumber++; + dsqlScratch->localTables.push(this); + return this; + } + + fb_assert(dsqlTable); + + const auto& tableName = dsqlTable->name; + + if (tableName.schema.hasData() || tableName.package.hasData()) + { + status_exception::raise( + Arg::Gds(isc_random) << + "Local temporary table declarations cannot use qualified names"); + } + + if (dsqlScratch->getLocalTable(dsqlName)) + { + ERRD_post( + Arg::Gds(isc_sqlerr) << Arg::Num(-637) << + Arg::Gds(isc_dsql_duplicate_spec) << dsqlName.toQuotedString()); + } + tableNumber = dsqlScratch->localTableNumber++; dsqlScratch->localTables.push(this); + dsqlScratch->putLocalTable(this); + + auto& pool = dsqlScratch->getPool(); + dsqlRelation = FB_NEW_POOL(pool) dsql_rel(pool); + dsqlRelation->rel_name = QualifiedName(dsqlName); + dsqlRelation->rel_flags = REL_local_table; + dsqlRelation->rel_local_table_number = tableNumber; + dsqlRelation->rel_dbkey_length = 8; + + dsql_fld** fieldPtr = &dsqlRelation->rel_fields; + USHORT position = 0; + + for (const auto clause : dsqlTable->clauses) + { + if (clause->type != RelationNode::Clause::TYPE_ADD_COLUMN) + { + status_exception::raise( + Arg::Gds(isc_random) << + "Table constraints are not supported for local temporary table declarations"); + } + + const auto addColumn = static_cast(clause.getObject()); + const auto field = addColumn->field; + + if (addColumn->defaultValue) + { + status_exception::raise( + Arg::Gds(isc_random) << + "DEFAULT is not allowed for LOCAL TEMPORARY TABLE columns"); + } + + if (addColumn->computed) + { + status_exception::raise( + Arg::Gds(isc_random) << + "COMPUTED BY is not allowed for LOCAL TEMPORARY TABLE columns"); + } + + if (addColumn->identityOptions) + { + status_exception::raise( + Arg::Gds(isc_random) << + "IDENTITY columns are not allowed for LOCAL TEMPORARY TABLEs"); + } + + if (field->dimensions != 0) + { + status_exception::raise( + Arg::Gds(isc_random) << + "Array type columns are not allowed for LOCAL TEMPORARY TABLEs"); + } + + bool notNull = false; + + for (const auto& constraint : addColumn->constraints) + { + if (constraint.constraintType != RelationNode::AddConstraintClause::CTYPE_NOT_NULL || + constraint.name.hasData()) + { + status_exception::raise( + Arg::Gds(isc_random) << + "Only NOT NULL constraints without names are supported on LOCAL TEMPORARY TABLEs"); + } + + notNull = true; + } + + for (dsql_fld* existing = dsqlRelation->rel_fields; existing; existing = existing->fld_next) + { + if (existing->fld_name == field->fld_name) + { + ERRD_post( + Arg::Gds(isc_sqlerr) << Arg::Num(-637) << + Arg::Gds(isc_dsql_duplicate_spec) << field->fld_name.toQuotedString()); + } + } + + field->resolve(dsqlScratch); + field->notNull = notNull; + field->fld_relation = dsqlRelation; + field->fld_id = position; + field->fld_pos = position++; + + if (!notNull) + field->flags |= FLD_nullable; + + *fieldPtr = field; + fieldPtr = &field->fld_next; + notNullFields.add(notNull); + } return this; } @@ -1796,6 +1913,7 @@ string DeclareLocalTableNode::internalPrint(NodePrinter& printer) const StmtNode::internalPrint(printer); NODE_PRINT(printer, tableNumber); + NODE_PRINT(printer, dsqlName); return "DeclareLocalTableNode"; } @@ -1804,12 +1922,29 @@ void DeclareLocalTableNode::genBlr(DsqlCompilerScratch* dsqlScratch) { dsqlScratch->appendUChar(blr_dcl_local_table); dsqlScratch->appendUShort(tableNumber); + + if (dsqlRelation) + { + USHORT fieldCount = 0; + + for (auto field = dsqlRelation->rel_fields; field; field = field->fld_next) + ++fieldCount; + + dsqlScratch->appendUChar(blr_dcl_local_table_format); + dsqlScratch->appendUShort(fieldCount); + + for (auto field = dsqlRelation->rel_fields; field; field = field->fld_next) + dsqlScratch->putType(field, true); + + dsqlScratch->appendUChar(blr_end); + } } DeclareLocalTableNode* DeclareLocalTableNode::copy(thread_db* tdbb, NodeCopier& copier) const { const auto node = FB_NEW_POOL(*tdbb->getDefaultPool()) DeclareLocalTableNode(*tdbb->getDefaultPool()); node->format = format; + node->notNullFields = notNullFields; node->tableNumber = tableNumber; return node; } @@ -1833,6 +1968,22 @@ const StmtNode* DeclareLocalTableNode::execute(thread_db* tdbb, Request* request return parentStmt; } +void DeclareLocalTableNode::validateRecord(const DeclareLocalTableNode* table, const Record* record) +{ + if (!table || !record) + return; + + for (FB_SIZE_T i = 0; i < table->notNullFields.getCount(); ++i) + { + if (table->notNullFields[i] && record->isNull(i)) + { + string fieldName; + fieldName.printf("local table field %" SIZEFORMAT, i + 1); + ERR_post(Arg::Gds(isc_not_valid_for_var) << Arg::Str(fieldName) << Arg::Str(NULL_STRING_MARK)); + } + } +} + DeclareLocalTableNode::Impure* DeclareLocalTableNode::getImpure(thread_db* tdbb, Request* request, bool createWhenDead) const { const auto impure = request->getImpure(impureOffset); @@ -2656,6 +2807,7 @@ DmlNode* EraseNode::parse(thread_db* tdbb, MemoryPool& pool, CompilerScratch* cs EraseNode* node = FB_NEW_POOL(pool) EraseNode(pool); node->stream = csb->csb_rpt[n].csb_stream; + node->localTableNumber = csb->csb_rpt[n].csb_local_table_number; if (csb->csb_blr_reader.peekByte() == blr_marks) node->marks |= PAR_marks(csb); @@ -2668,14 +2820,19 @@ DmlNode* EraseNode::parse(thread_db* tdbb, MemoryPool& pool, CompilerScratch* cs StmtNode* EraseNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) { - auto relation = dsqlRelation; + NestConst relation = nodeAs(dsqlRelation); + fb_assert(relation); const auto node = FB_NEW_POOL(dsqlScratch->getPool()) EraseNode(dsqlScratch->getPool()); node->dsqlCursorName = dsqlCursorName; node->dsqlSkipLocked = dsqlSkipLocked; - if (dsqlCursorName.hasData()) + if (relation->dsqlName.schema.hasData() || relation->dsqlName.package.hasData() || + !dsqlScratch->getLocalTable(relation->dsqlName.object) || + dsqlCursorName.hasData()) + { dsqlScratch->qualifyExistingName(relation->dsqlName, obj_relation); + } if (dsqlCursorName.hasData() && dsqlScratch->isPsql()) { @@ -2733,7 +2890,7 @@ StmtNode* EraseNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) rse->dsqlFlags |= RecordSourceNode::DFLAG_SINGLETON; node->dsqlRse = rse; - node->dsqlRelation = nodeAs(rse->dsqlStreams->items[0]); + node->dsqlRelation = rse->dsqlStreams->items[0]; node->dsqlReturning = dsqlProcessReturning(dsqlScratch, node->dsqlRelation->dsqlContext->ctx_relation, dsqlReturning, dsqlCursorName.hasData()); @@ -2887,12 +3044,15 @@ void EraseNode::pass1Erase(thread_db* tdbb, CompilerScratch* csb, EraseNode* nod jrd_rel* const relation = tail->csb_relation(tdbb); - //// TODO: LocalTableSourceNode if (!relation) { - ERR_post( - Arg::Gds(isc_wish_list) << - Arg::Gds(isc_random) << "erase local_table"); + if (tail->csb_local_table_number.has_value()) + { + node->localTableNumber = tail->csb_local_table_number; + return; + } + + ERR_post(Arg::Gds(isc_wish_list) << Arg::Gds(isc_random) << "erase non-relation source"); } view = relation->isView() ? relation : view; @@ -3074,8 +3234,13 @@ const StmtNode* EraseNode::erase(thread_db* tdbb, Request* request, WhichTrigger if (!statement) break; - const Format* format = rpb->rpb_relation->currentFormat(tdbb); - Record* record = VIO_record(tdbb, rpb, format, tdbb->getDefaultPool()); + const auto localTable = localTableNumber.has_value() ? + request->getStatement()->localTables[localTableNumber.value()] : nullptr; + const Format* format = relation ? rpb->rpb_relation->currentFormat(tdbb) : localTable->format.getObject(); + Record* record = relation ? VIO_record(tdbb, rpb, format, tdbb->getDefaultPool()) : rpb->rpb_record; + + if (!record) + record = rpb->rpb_record = FB_NEW_POOL(*tdbb->getDefaultPool()) Record(*tdbb->getDefaultPool(), format); rpb->rpb_address = record->getData(); rpb->rpb_length = format->fmt_length; @@ -3098,27 +3263,29 @@ const StmtNode* EraseNode::erase(thread_db* tdbb, Request* request, WhichTrigger } request->req_operation = Request::req_return; - RLCK_reserve_relation(tdbb, transaction, relation->getPermanent(), true); + + if (relation) + RLCK_reserve_relation(tdbb, transaction, relation->getPermanent(), true); if (rpb->rpb_runtime_flags & RPB_just_deleted) return parentStmt; - if (rpb->rpb_number.isBof() || (!relation->isView() && !rpb->rpb_number.isValid())) + if (rpb->rpb_number.isBof() || ((relation ? !relation->isView() : true) && !rpb->rpb_number.isValid())) ERR_post(Arg::Gds(isc_no_cur_rec)); - if (forNode && forNode->isWriteLockMode(request)) + if (relation && forNode && forNode->isWriteLockMode(request)) { forceWriteLock(tdbb, rpb, transaction); return parentStmt; } - if (forNode && (marks & StmtNode::MARK_MERGE)) + if (relation && forNode && (marks & StmtNode::MARK_MERGE)) forNode->checkRecordUpdated(tdbb, request, rpb); // If the stream was sorted, the various fields in the rpb are probably junk. // Just to make sure that everything is cool, refetch and release the record. - if (rpb->rpb_runtime_flags & RPB_refetch) + if (relation && (rpb->rpb_runtime_flags & RPB_refetch)) { VIO_refetch_record(tdbb, rpb, transaction, false, false); rpb->rpb_runtime_flags &= ~RPB_refetch; @@ -3133,12 +3300,21 @@ const StmtNode* EraseNode::erase(thread_db* tdbb, Request* request, WhichTrigger // transaction and delete should be skipped. const bool skipLocked = rpb->rpb_stream_flags & RPB_s_skipLocked; CondSavepointAndMarker spPreTriggers(tdbb, transaction, - skipLocked && !(transaction->tra_flags & TRA_system) && relation->rel_triggers[TRIGGER_PRE_ERASE]); + relation && skipLocked && !(transaction->tra_flags & TRA_system) && relation->rel_triggers[TRIGGER_PRE_ERASE]); // Handle pre-operation trigger. - preModifyEraseTriggers(tdbb, relation->rel_triggers[TRIGGER_PRE_ERASE], whichTrig, rpb, NULL, TRIGGER_DELETE); + if (relation) + preModifyEraseTriggers(tdbb, relation->rel_triggers[TRIGGER_PRE_ERASE], whichTrig, rpb, NULL, TRIGGER_DELETE); + + if (!relation) + { + fb_assert(localTableNumber.has_value()); + const auto localTable = request->getStatement()->localTables[localTableNumber.value()]; - if (auto* extFile = relation->getExtFile()) + if (!localTable->getImpure(tdbb, request)->recordBuffer->erase(rpb->rpb_number.getValue())) + ERR_post(Arg::Gds(isc_no_cur_rec)); + } + else if (auto* extFile = relation->getExtFile()) extFile->erase(rpb, transaction); else if (relation->isVirtual()) VirtualTable::erase(tdbb, rpb); @@ -3176,28 +3352,28 @@ const StmtNode* EraseNode::erase(thread_db* tdbb, Request* request, WhichTrigger spPreTriggers.release(); // Handle post operation trigger. - if ((relation->rel_triggers[TRIGGER_POST_ERASE] || relation->isSystem()) && whichTrig != PRE_TRIG) + if (relation && (relation->rel_triggers[TRIGGER_POST_ERASE] || relation->isSystem()) && whichTrig != PRE_TRIG) { EXE_execute_triggers(tdbb, relation->rel_triggers[TRIGGER_POST_ERASE], rpb, NULL, TRIGGER_DELETE, POST_TRIG); } - if (forNode && (marks & StmtNode::MARK_MERGE)) + if (relation && forNode && (marks & StmtNode::MARK_MERGE)) forNode->setRecordUpdated(tdbb, request, rpb); // Call IDX_erase (which checks constraints) after all post erase triggers have fired. // This is required for cascading referential integrity, which can be implemented as // post_erase triggers. - if (!relation->isView()) + if (!relation || !relation->isView()) { - if (!relation->getExtFile() && !relation->isVirtual()) + if (relation && !relation->getExtFile() && !relation->isVirtual()) IDX_erase(tdbb, rpb, transaction); // Mark this rpb as already deleted to skip the subsequent attempts rpb->rpb_runtime_flags |= RPB_just_deleted; } - if (!relation->isView() || (whichTrig == ALL_TRIGS || whichTrig == POST_TRIG)) + if (!relation || !relation->isView() || (whichTrig == ALL_TRIGS || whichTrig == POST_TRIG)) { if (!(marks & MARK_AVOID_COUNTERS)) { @@ -6952,6 +7128,8 @@ void LocalDeclarationsNode::checkUniqueFieldsNames(const LocalDeclarationsNode* name = varNode->dsqlDef->name.c_str(); else if (auto cursorNode = nodeAs(statement)) name = cursorNode->dsqlName.c_str(); + else if (auto tableNode = nodeAs(statement)) + name = tableNode->dsqlName.c_str(); else if (nodeAs(statement) || nodeAs(statement)) continue; @@ -7027,6 +7205,7 @@ void LocalDeclarationsNode::genBlr(DsqlCompilerScratch* dsqlScratch) DsqlDescMaker::fromField(&variable->desc, variable->field); } else if (nodeIs(parameter) || + nodeIs(parameter) || nodeIs(parameter) || nodeIs(parameter)) { @@ -8149,12 +8328,15 @@ DmlNode* ModifyNode::parse(thread_db* tdbb, MemoryPool& pool, CompilerScratch* c tail = CMP_csb_element(csb, newStream); tail->csb_relation = csb->csb_rpt[orgStream].csb_relation; + tail->csb_format = csb->csb_rpt[orgStream].csb_format; + tail->csb_local_table_number = csb->csb_rpt[orgStream].csb_local_table_number; // Make the node and parse the sub-expression. ModifyNode* node = FB_NEW_POOL(pool) ModifyNode(pool); node->orgStream = orgStream; node->newStream = newStream; + node->localTableNumber = csb->csb_rpt[orgStream].csb_local_table_number; if (csb->csb_blr_reader.peekByte() == blr_marks) node->marks |= PAR_marks(csb); @@ -8192,8 +8374,12 @@ StmtNode* ModifyNode::internalDsqlPass(DsqlCompilerScratch* dsqlScratch, bool up NestConst relation = nodeAs(dsqlRelation); fb_assert(relation); - if (dsqlCursorName.hasData()) + if (relation->dsqlName.schema.hasData() || relation->dsqlName.package.hasData() || + !dsqlScratch->getLocalTable(relation->dsqlName.object) || + dsqlCursorName.hasData()) + { dsqlScratch->qualifyExistingName(relation->dsqlName, obj_relation); + } NestConst* ptr; @@ -8513,12 +8699,16 @@ void ModifyNode::pass1Modify(thread_db* tdbb, CompilerScratch* csb, ModifyNode* jrd_rel* const relation = tail->csb_relation(tdbb); - //// TODO: LocalTableSourceNode if (!relation) { - ERR_post( - Arg::Gds(isc_wish_list) << - Arg::Gds(isc_random) << "modify local_table"); + if (tail->csb_local_table_number.has_value()) + { + node->localTableNumber = tail->csb_local_table_number; + makeValidation(tdbb, csb, newStream, node->validations); + return; + } + + ERR_post(Arg::Gds(isc_wish_list) << Arg::Gds(isc_random) << "modify non-relation source"); } view = relation->isView() ? relation : view; @@ -8721,7 +8911,7 @@ const StmtNode* ModifyNode::modify(thread_db* tdbb, Request* request, WhichTrigg if (!(marks & MARK_AVOID_COUNTERS)) request->req_records_affected.bumpModified(false); - if (impure->sta_state == 0 && forNode && forNode->isWriteLockMode(request)) + if (relation && impure->sta_state == 0 && forNode && forNode->isWriteLockMode(request)) request->req_operation = Request::req_return; else break; @@ -8740,7 +8930,7 @@ const StmtNode* ModifyNode::modify(thread_db* tdbb, Request* request, WhichTrigg if (impure->sta_state == 0) { - if (forNode && forNode->isWriteLockMode(request)) + if (relation && forNode && forNode->isWriteLockMode(request)) { forceWriteLock(tdbb, orgRpb, transaction); return parentStmt; @@ -8756,15 +8946,32 @@ const StmtNode* ModifyNode::modify(thread_db* tdbb, Request* request, WhichTrigg // transaction and update should be skipped. const bool skipLocked = orgRpb->rpb_stream_flags & RPB_s_skipLocked; CondSavepointAndMarker spPreTriggers(tdbb, transaction, - skipLocked && !(transaction->tra_flags & TRA_system) && relation->rel_triggers[TRIGGER_PRE_MODIFY]); + relation && skipLocked && !(transaction->tra_flags & TRA_system) && + relation->rel_triggers[TRIGGER_PRE_MODIFY]); - preModifyEraseTriggers(tdbb, relation->rel_triggers[TRIGGER_PRE_MODIFY], whichTrig, orgRpb, newRpb, - TRIGGER_UPDATE); + if (relation) + { + preModifyEraseTriggers(tdbb, relation->rel_triggers[TRIGGER_PRE_MODIFY], whichTrig, orgRpb, newRpb, + TRIGGER_UPDATE); + } if (validations.hasData()) validateExpressions(tdbb, validations); - if (auto* extFile = relation->getExtFile()) + if (!relation) + { + fb_assert(localTableNumber.has_value()); + const auto localTable = request->getStatement()->localTables[localTableNumber.value()]; + + DeclareLocalTableNode::validateRecord(localTable, newRpb->rpb_record); + + if (!localTable->getImpure(tdbb, request)->recordBuffer->modify( + orgRpb->rpb_number.getValue(), newRpb->rpb_record)) + { + ERR_post(Arg::Gds(isc_no_cur_rec)); + } + } + else if (auto* extFile = relation->getExtFile()) extFile->modify(orgRpb, newRpb, transaction); else if (relation->isVirtual()) VirtualTable::modify(tdbb, orgRpb, newRpb); @@ -8799,23 +9006,24 @@ const StmtNode* ModifyNode::modify(thread_db* tdbb, Request* request, WhichTrigg newRpb->rpb_number = orgRpb->rpb_number; newRpb->rpb_number.setValid(true); - if ((relation->rel_triggers[TRIGGER_POST_MODIFY] || relation->isSystem()) && whichTrig != PRE_TRIG) + if (relation && (relation->rel_triggers[TRIGGER_POST_MODIFY] || relation->isSystem()) && + whichTrig != PRE_TRIG) { EXE_execute_triggers(tdbb, relation->rel_triggers[TRIGGER_POST_MODIFY], orgRpb, newRpb, TRIGGER_UPDATE, POST_TRIG); } - if (forNode && (marks & StmtNode::MARK_MERGE)) + if (relation && forNode && (marks & StmtNode::MARK_MERGE)) forNode->setRecordUpdated(tdbb, request, orgRpb); // Now call IDX_modify_check_constrints after all post modify triggers // have fired. This is required for cascading referential integrity, // which can be implemented as post_erase triggers. - if (!relation->getExtFile() && !relation->isView() && !relation->isVirtual()) + if (relation && !relation->getExtFile() && !relation->isView() && !relation->isVirtual()) IDX_modify_check_constraints(tdbb, orgRpb, newRpb, transaction); - if (!relation->isView() || + if (!relation || !relation->isView() || (!subMod && (whichTrig == ALL_TRIGS || whichTrig == POST_TRIG))) { if (!(marks & MARK_AVOID_COUNTERS)) @@ -8846,7 +9054,8 @@ const StmtNode* ModifyNode::modify(thread_db* tdbb, Request* request, WhichTrigg } impure->sta_state = 0; - RLCK_reserve_relation(tdbb, transaction, relation->getPermanent(), true); + if (relation) + RLCK_reserve_relation(tdbb, transaction, relation->getPermanent(), true); if (orgRpb->rpb_runtime_flags & RPB_just_deleted) { @@ -8854,17 +9063,17 @@ const StmtNode* ModifyNode::modify(thread_db* tdbb, Request* request, WhichTrigg return parentStmt; } - if (orgRpb->rpb_number.isBof() || (!relation->isView() && !orgRpb->rpb_number.isValid())) + if (orgRpb->rpb_number.isBof() || ((relation ? !relation->isView() : true) && !orgRpb->rpb_number.isValid())) ERR_post(Arg::Gds(isc_no_cur_rec)); - if (forNode && (marks & StmtNode::MARK_MERGE)) + if (relation && forNode && (marks & StmtNode::MARK_MERGE)) forNode->checkRecordUpdated(tdbb, request, orgRpb); // If the stream was sorted, the various fields in the rpb are // probably junk. Just to make sure that everything is cool, // refetch and release the record. - if (orgRpb->rpb_runtime_flags & RPB_refetch) + if (relation && (orgRpb->rpb_runtime_flags & RPB_refetch)) { VIO_refetch_record(tdbb, orgRpb, transaction, false, false); orgRpb->rpb_runtime_flags &= ~RPB_refetch; @@ -8881,8 +9090,14 @@ const StmtNode* ModifyNode::modify(thread_db* tdbb, Request* request, WhichTrigg // exists for the stream and is big enough, and copying fields from the // original record to the new record. - const Format* const newFormat = newRpb->rpb_relation->currentFormat(tdbb); - Record* newRecord = VIO_record(tdbb, newRpb, newFormat, tdbb->getDefaultPool()); + const auto localTable = localTableNumber.has_value() ? + request->getStatement()->localTables[localTableNumber.value()] : nullptr; + const Format* const newFormat = relation ? newRpb->rpb_relation->currentFormat(tdbb) : localTable->format.getObject(); + Record* newRecord = relation ? VIO_record(tdbb, newRpb, newFormat, tdbb->getDefaultPool()) : newRpb->rpb_record; + + if (!newRecord) + newRecord = newRpb->rpb_record = FB_NEW_POOL(*tdbb->getDefaultPool()) Record(*tdbb->getDefaultPool(), newFormat); + newRpb->rpb_address = newRecord->getData(); newRpb->rpb_length = newFormat->fmt_length; newRpb->rpb_format_number = newFormat->fmt_version; @@ -8891,8 +9106,10 @@ const StmtNode* ModifyNode::modify(thread_db* tdbb, Request* request, WhichTrigg if (!orgRecord) { const Format* const orgFormat = newFormat; - orgRecord = VIO_record(tdbb, orgRpb, orgFormat, tdbb->getDefaultPool()); + orgRecord = relation ? VIO_record(tdbb, orgRpb, orgFormat, tdbb->getDefaultPool()) : + FB_NEW_POOL(*tdbb->getDefaultPool()) Record(*tdbb->getDefaultPool(), orgFormat); orgRecord->setTransactionNumber(orgRpb->rpb_transaction_nr); + orgRpb->rpb_record = orgRecord; orgRpb->rpb_address = orgRecord->getData(); orgRpb->rpb_length = orgFormat->fmt_length; orgRpb->rpb_format_number = orgFormat->fmt_version; @@ -8900,7 +9117,10 @@ const StmtNode* ModifyNode::modify(thread_db* tdbb, Request* request, WhichTrigg // Copy the original record to the new record - VIO_copy_record(tdbb, relation, orgRecord, newRecord); + if (relation) + VIO_copy_record(tdbb, relation, orgRecord, newRecord); + else + newRecord->copyDataFrom(orgRecord, true); newRpb->rpb_number = orgRpb->rpb_number; newRpb->rpb_number.setValid(true); @@ -9869,7 +10089,10 @@ const StmtNode* StoreNode::store(thread_db* tdbb, Request* request, WhichTrigger cleanupRpb(tdbb, rpb); if (localTableSource) + { + DeclareLocalTableNode::validateRecord(localTable, rpb->rpb_record); localTableImpure->recordBuffer->store(rpb->rpb_record); + } else if (auto* extFile = relation->getExtFile()) extFile->store(tdbb, rpb); else if (relation->isVirtual()) @@ -11786,7 +12009,8 @@ static dsql_ctx* dsqlGetContext(const RecordSourceNode* node) return relNode->dsqlContext; else if (auto tableValueFunctionNode = nodeAs(node)) return tableValueFunctionNode->dsqlContext; - //// TODO: LocalTableSourceNode + else if (auto localTableNode = nodeAs(node)) + return localTableNode->dsqlContext; else if (auto rseNode = nodeAs(node)) return rseNode->dsqlContext; else @@ -11805,7 +12029,8 @@ static void dsqlGetContexts(DsqlContextStack& contexts, const RecordSourceNode* contexts.push(relNode->dsqlContext); else if (auto tableValueFunctionNode = nodeAs(node)) contexts.push(tableValueFunctionNode->dsqlContext); - //// TODO: LocalTableSourceNode + else if (auto localTableNode = nodeAs(node)) + contexts.push(localTableNode->dsqlContext); else if (auto rseNode = nodeAs(node)) { if (rseNode->dsqlContext) // derived table diff --git a/src/dsql/StmtNodes.h b/src/dsql/StmtNodes.h index f5ffc7fe670..39910d6fddc 100644 --- a/src/dsql/StmtNodes.h +++ b/src/dsql/StmtNodes.h @@ -380,7 +380,9 @@ class DeclareLocalTableNode final : public TypedNode(pool) + : TypedNode(pool), + dsqlName(pool), + notNullFields(pool) { } @@ -401,10 +403,15 @@ class DeclareLocalTableNode final : public TypedNode dsqlTable; + dsql_rel* dsqlRelation = nullptr; NestConst format; + Firebird::Array notNullFields; USHORT tableNumber = 0; }; @@ -581,7 +588,7 @@ class EraseNode final : public TypedNode const StmtNode* erase(thread_db* tdbb, Request* request, WhichTrigger whichTrig) const; public: - NestConst dsqlRelation; + NestConst dsqlRelation; NestConst dsqlBoolean; NestConst dsqlPlan; NestConst dsqlOrder; @@ -597,6 +604,7 @@ class EraseNode final : public TypedNode NestConst forNode; // parent implicit cursor, if present StreamType stream = 0; unsigned marks = 0; // see StmtNode::IUD_MARK_xxx + std::optional localTableNumber; }; @@ -1296,6 +1304,7 @@ class ModifyNode final : public TypedNode unsigned marks = 0; // see StmtNode::IUD_MARK_xxx USHORT dsqlRseFlags = 0; std::optional dsqlReturningLocalTableNumber; + std::optional localTableNumber; }; diff --git a/src/dsql/dsql.cpp b/src/dsql/dsql.cpp index 25017e56d4f..e33dcd4048d 100644 --- a/src/dsql/dsql.cpp +++ b/src/dsql/dsql.cpp @@ -1418,7 +1418,9 @@ dsql_rel::dsql_rel(MemoryPool& p, const dsql_rel* rel) rel_owner(p, rel->rel_owner), rel_id(rel->rel_id), rel_dbkey_length(rel->rel_dbkey_length), - rel_flags(rel->rel_flags) + rel_flags(rel->rel_flags), + rel_local_table_number(rel->rel_local_table_number), + rel_private(rel->rel_private) { auto* from = rel->rel_fields; auto** to = &rel_fields; diff --git a/src/dsql/dsql.h b/src/dsql/dsql.h index 67d07db8d1e..c0e351493e7 100644 --- a/src/dsql/dsql.h +++ b/src/dsql/dsql.h @@ -159,6 +159,7 @@ class dsql_rel : public pool_alloc USHORT rel_id = 0; // Relation id USHORT rel_dbkey_length = 0; USHORT rel_flags = 0; + std::optional rel_local_table_number; bool rel_private = false; // Packaged private relation }; @@ -169,7 +170,8 @@ enum rel_flags_vals { REL_view = 4, // relation is a view REL_external = 8, // relation is an external table REL_creating = 16, // we are creating the bare relation in memory - REL_ltt_created = 32 // relation is created local temporary table + REL_ltt_created = 32, // relation is created local temporary table + REL_local_table = 64 // relation is a PSQL declared local table }; class TypeClause diff --git a/src/dsql/parse.y b/src/dsql/parse.y index 953a1c6462d..9821a230d76 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -3607,7 +3607,22 @@ local_nonforward_declarations %type local_nonforward_declaration local_nonforward_declaration - : DECLARE var_decl_opt local_declaration_item ';' + : DECLARE LOCAL TEMPORARY TABLE valid_symbol_name + { + RelationSourceNode* relationNode = newNode(QualifiedName(*$5)); + $$ = newNode(relationNode); + $$->tempFlag = REL_temp_ltt; + } + '(' table_elements($6) ')' ';' + { + DeclareLocalTableNode* node = newNode(); + node->dsqlName = *$5; + node->dsqlTable = $6; + $$ = node; + $$->line = YYPOSNARG(1).firstLine; + $$->column = YYPOSNARG(1).firstColumn; + } + | DECLARE var_decl_opt local_declaration_item ';' { $$ = $3; $$->line = YYPOSNARG(1).firstLine; diff --git a/src/dsql/pass1.cpp b/src/dsql/pass1.cpp index 66894f9692d..a372ccfb443 100644 --- a/src/dsql/pass1.cpp +++ b/src/dsql/pass1.cpp @@ -383,7 +383,14 @@ dsql_ctx* PASS1_make_context(DsqlCompilerScratch* dsqlScratch, RecordSourceNode* { relationNode = cte; } - else + else if (!tableValueFunctionNode && !(procNode && procNode->inputSources) && + !name.schema.hasData() && !name.package.hasData()) + { + if (const auto localTable = dsqlScratch->getLocalTable(name.object)) + relation = localTable->dsqlRelation; + } + + if (!selNode && !tableValueFunctionNode && !cte && !procedure && !relation) { const auto resolvedObject = dsqlScratch->resolveRoutineOrRelation(name, (((procNode && procNode->inputSources)) ? @@ -1802,10 +1809,21 @@ RecordSourceNode* PASS1_relation(DsqlCompilerScratch* dsqlScratch, RecordSourceN if (context->ctx_relation) { - const auto relNode = FB_NEW_POOL(*tdbb->getDefaultPool()) RelationSourceNode( - *tdbb->getDefaultPool(), context->ctx_relation->rel_name); - relNode->dsqlContext = context; - return relNode; + if (context->ctx_relation->rel_flags & REL_local_table) + { + const auto localTableNode = FB_NEW_POOL(*tdbb->getDefaultPool()) LocalTableSourceNode( + *tdbb->getDefaultPool()); + localTableNode->dsqlContext = context; + localTableNode->tableNumber = context->ctx_relation->rel_local_table_number.value(); + return localTableNode; + } + else + { + const auto relNode = FB_NEW_POOL(*tdbb->getDefaultPool()) RelationSourceNode( + *tdbb->getDefaultPool(), context->ctx_relation->rel_name); + relNode->dsqlContext = context; + return relNode; + } } else if (context->ctx_procedure) { @@ -3034,7 +3052,11 @@ static void remap_streams_to_parent_context(ExprNode* input, dsql_ctx* parent_co DEV_BLKCHK(tableValueFunctionNode->dsqlContext, dsql_type_ctx); tableValueFunctionNode->dsqlContext->ctx_parent = parent_context; } - //// TODO: LocalTableSourceNode + else if (auto localTableNode = nodeAs(input)) + { + DEV_BLKCHK(localTableNode->dsqlContext, dsql_type_ctx); + localTableNode->dsqlContext->ctx_parent = parent_context; + } else if (auto rseNode = nodeAs(input)) remap_streams_to_parent_context(rseNode->dsqlStreams, parent_context); else if (auto unionNode = nodeAs(input)) diff --git a/src/jrd/RecordBuffer.cpp b/src/jrd/RecordBuffer.cpp index 1bf837d570a..40e1614fb37 100644 --- a/src/jrd/RecordBuffer.cpp +++ b/src/jrd/RecordBuffer.cpp @@ -33,6 +33,7 @@ using namespace Jrd; RecordBuffer::RecordBuffer(MemoryPool& pool, const Format* format) : PermanentStorage(pool) + , active(pool) { record = FB_NEW_POOL(pool) Record(pool, format); } @@ -41,6 +42,7 @@ void RecordBuffer::reset() { count = 0; space.reset(); + active.clear(); } offset_t RecordBuffer::store(const Record* new_record) @@ -52,16 +54,44 @@ offset_t RecordBuffer::store(const Record* new_record) space = FB_NEW_POOL(getPool()) TempSpace(getPool(), SCRATCH); space->write(count * length, new_record->getData(), length); + active.add(1); return count++; } +bool RecordBuffer::modify(offset_t position, const Record* new_record) +{ + if (!isValid(position)) + return false; + + const ULONG length = record->getLength(); + fb_assert(new_record->getLength() == length); + fb_assert(space.hasData()); + + space->write(position * length, new_record->getData(), length); + return true; +} + +bool RecordBuffer::erase(offset_t position) +{ + if (!isValid(position)) + return false; + + active[position] = 0; + return true; +} + +bool RecordBuffer::isValid(offset_t position) const +{ + return position < count && active[position] != 0; +} + bool RecordBuffer::fetch(offset_t position, Record* to_record) { const ULONG length = record->getLength(); fb_assert(to_record->getLength() == length); - if (position >= count) + if (!isValid(position)) return false; fb_assert(space.hasData()); diff --git a/src/jrd/RecordBuffer.h b/src/jrd/RecordBuffer.h index 4cd7979faf0..13673b3bbe5 100644 --- a/src/jrd/RecordBuffer.h +++ b/src/jrd/RecordBuffer.h @@ -23,7 +23,9 @@ #ifndef JRD_RECORD_BUFFER_H #define JRD_RECORD_BUFFER_H +#include "firebird.h" #include "../common/classes/alloc.h" +#include "../common/classes/array.h" #include "../common/classes/auto.h" #include "../common/classes/File.h" #include "../jrd/TempSpace.h" @@ -52,12 +54,16 @@ class RecordBuffer : public Firebird::PermanentStorage void reset(); offset_t store(const Record*); + bool modify(offset_t, const Record*); + bool erase(offset_t); + bool isValid(offset_t) const; bool fetch(offset_t, Record*); private: offset_t count = 0; Firebird::AutoPtr record; Firebird::AutoPtr space; + Firebird::Array active; }; } // namespace diff --git a/src/jrd/RecordSourceNodes.cpp b/src/jrd/RecordSourceNodes.cpp index e89bb17506c..7b4e0152608 100644 --- a/src/jrd/RecordSourceNodes.cpp +++ b/src/jrd/RecordSourceNodes.cpp @@ -666,6 +666,7 @@ LocalTableSourceNode* LocalTableSourceNode::parse(thread_db* tdbb, CompilerScrat csb->csb_rpt[node->stream].csb_format = csb->csb_localTables[tableNumber]->format; csb->csb_rpt[node->stream].csb_alias = aliasString.release(); + csb->csb_rpt[node->stream].csb_local_table_number = tableNumber; } return node; @@ -715,6 +716,8 @@ LocalTableSourceNode* LocalTableSourceNode::copy(thread_db* tdbb, NodeCopier& co copier.remap[stream] = newSource->stream; newSource->context = context; + newSource->alias = alias; + newSource->tableNumber = tableNumber; if (tableNumber >= copier.csb->csb_localTables.getCount() || !copier.csb->csb_localTables[tableNumber]) ERR_post(Arg::Gds(isc_bad_loctab_num) << Arg::Num(tableNumber)); @@ -723,6 +726,7 @@ LocalTableSourceNode* LocalTableSourceNode::copy(thread_db* tdbb, NodeCopier& co element->csb_format = copier.csb->csb_localTables[tableNumber]->format; element->csb_view_stream = copier.remap[0]; + element->csb_local_table_number = tableNumber; if (alias.hasData()) { @@ -4525,7 +4529,11 @@ static RecordSourceNode* dsqlPassRelProc(DsqlCompilerScratch* dsqlScratch, Recor relName.object = tblBasedFunNode->dsqlName; relAlias = tblBasedFunNode->alias.c_str(); } - //// TODO: LocalTableSourceNode + else if (const auto localTableNode = nodeAs(source)) + { + fb_assert(localTableNode->dsqlContext); + return source; + } else fb_assert(false); diff --git a/src/jrd/exe.cpp b/src/jrd/exe.cpp index e0d6146dd49..3c3ea529dd1 100644 --- a/src/jrd/exe.cpp +++ b/src/jrd/exe.cpp @@ -1250,7 +1250,9 @@ void EXE_unwind(thread_db* tdbb, Request* request) continue; auto impure = localTable->getImpure(tdbb, request, false); - impure->recordBuffer->reset(); + + if (impure->recordBuffer) + impure->recordBuffer->reset(); } release_blobs(tdbb, request); @@ -2079,4 +2081,3 @@ QualifiedName CompilerScratch::csb_repeat::getName(bool allowEmpty) const return QualifiedName(""); } } - diff --git a/src/jrd/exe.h b/src/jrd/exe.h index beba9b72286..07ec4dbc13d 100644 --- a/src/jrd/exe.h +++ b/src/jrd/exe.h @@ -637,6 +637,7 @@ class CompilerScratch : public pool_alloc QualifiedName getName(bool allowEmpty = true) const; std::optional csb_cursor_number; // Cursor number for this stream + std::optional csb_local_table_number; // Local table number for this stream StreamType csb_stream; // Map user context to internal stream StreamType csb_view_stream; // stream number for view relation, below USHORT csb_flags; diff --git a/src/jrd/recsrc/LocalTableStream.cpp b/src/jrd/recsrc/LocalTableStream.cpp index 5b72e5111cc..9c748d25547 100644 --- a/src/jrd/recsrc/LocalTableStream.cpp +++ b/src/jrd/recsrc/LocalTableStream.cpp @@ -88,12 +88,23 @@ bool LocalTableStream::internalGetRecord(thread_db* tdbb) const if (!rpb->rpb_record) rpb->rpb_record = FB_NEW_POOL(*tdbb->getDefaultPool()) Record(*tdbb->getDefaultPool(), m_format); - rpb->rpb_number.increment(); + const auto recordBuffer = m_table->getImpure(tdbb, request)->recordBuffer; - if (!m_table->getImpure(tdbb, request)->recordBuffer->fetch(rpb->rpb_number.getValue(), rpb->rpb_record)) + while (true) { - rpb->rpb_number.setValid(false); - return false; + rpb->rpb_number.increment(); + + if (rpb->rpb_number.getValue() >= recordBuffer->getCount()) + { + rpb->rpb_number.setValid(false); + return false; + } + + if (recordBuffer->fetch(rpb->rpb_number.getValue(), rpb->rpb_record)) + { + rpb->rpb_number.setValid(true); + break; + } } return true; From d1712f1250c283fb7b36c11f0dae7e1dd396b3ae Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Thu, 11 Jun 2026 08:19:45 -0300 Subject: [PATCH 2/2] Subroutines support --- .../README.declared_local_temporary_tables.md | 47 ++++++++++- src/dsql/DsqlCompilerScratch.cpp | 30 ++++++- src/dsql/DsqlCompilerScratch.h | 5 +- src/dsql/StmtNodes.cpp | 79 ++++++++++++++++--- src/dsql/StmtNodes.h | 2 + src/dsql/dsql.h | 2 + src/dsql/pass1.cpp | 9 ++- src/include/firebird/impl/blr.h | 1 + src/jrd/RecordSourceNodes.cpp | 7 +- src/jrd/RecordSourceNodes.h | 1 + src/jrd/Statement.cpp | 26 ++++++ src/jrd/Statement.h | 1 + src/jrd/exe.cpp | 7 +- src/jrd/exe.h | 3 + src/jrd/recsrc/LocalTableStream.cpp | 9 ++- src/jrd/recsrc/RecordSource.h | 4 +- src/jrd/req.h | 2 + src/yvalve/gds.cpp | 4 +- 18 files changed, 217 insertions(+), 22 deletions(-) diff --git a/doc/sql.extensions/README.declared_local_temporary_tables.md b/doc/sql.extensions/README.declared_local_temporary_tables.md index 1176fff3b40..0ed889338ad 100644 --- a/doc/sql.extensions/README.declared_local_temporary_tables.md +++ b/doc/sql.extensions/README.declared_local_temporary_tables.md @@ -80,6 +80,10 @@ Declared Local Temporary Tables have execution-frame data scope. - Rows are discarded when that execution frame finishes. - Transaction commit or rollback does not define the table lifetime; there is no `ON COMMIT` behavior. - An autonomous transaction block inside the same execution frame sees the same rows. +- Local subroutines may access declared local temporary tables from the containing PSQL scope. They see and modify the + rows of the current containing execution frame. +- Local subroutines may also declare their own local temporary tables. Those rows are scoped to the subroutine execution + frame. - Recursive calls reuse the same compiled table structure, but each recursive execution frame has separate rows. Recursive example: @@ -144,11 +148,52 @@ set term ;! The autonomous block sees the row inserted by its parent execution frame. +Local subroutine example: + +```sql +set term !; + +execute block returns (n integer, s integer) +as + declare local temporary table t ( + id integer + ); + + declare procedure p_add(v integer) + as + begin + insert into t values (:v); + end + + declare function f_count returns integer + as + declare variable ret integer; + begin + select count(*) from t into ret; + return ret; + end +begin + insert into t values (1); + execute procedure p_add(2); + + n = f_count(); + select sum(id) from t into s; + suspend; +end! + +set term ;! +``` + +The local procedure and function access `t` declared by the outer `EXECUTE BLOCK`. They use the same row set as the +current outer execution frame. + ## Visibility and Name Resolution -Declared Local Temporary Tables are visible only to SQL statements compiled inside the same PSQL scope. +Declared Local Temporary Tables are visible only to SQL statements compiled inside the declaring PSQL scope and its +local subroutines. - Unqualified references to the declared name resolve to the declared local table. +- SQL statements in local subroutines may also reference declared local temporary tables from the containing PSQL scope. - The declaration name cannot be schema-qualified or package-qualified. - The table is not present in `RDB$RELATIONS`, `RDB$RELATION_FIELDS`, monitoring metadata or other persistent metadata. - Dynamic SQL, including `EXECUTE STATEMENT`, is parsed separately and does not see declared local temporary tables. diff --git a/src/dsql/DsqlCompilerScratch.cpp b/src/dsql/DsqlCompilerScratch.cpp index b042f0386b8..5b73ecbc3f0 100644 --- a/src/dsql/DsqlCompilerScratch.cpp +++ b/src/dsql/DsqlCompilerScratch.cpp @@ -486,7 +486,7 @@ void DsqlCompilerScratch::putLocalVariableInit(dsql_var* variable, const Declare // Put maps in subroutines for outer variables/parameters usage. void DsqlCompilerScratch::putOuterMaps() { - if (!outerMessagesMap.count() && !outerVarsMap.count()) + if (!outerMessagesMap.count() && !outerVarsMap.count() && !outerLocalTablesMap.count()) return; appendUChar(blr_outer_map); @@ -505,6 +505,13 @@ void DsqlCompilerScratch::putOuterMaps() appendUShort(outer); } + for (auto& [outer, inner] : outerLocalTablesMap) + { + appendUChar(blr_outer_map_local_table); + appendUShort(outer); + appendUShort(inner); + } + appendUChar(blr_end); } @@ -553,17 +560,36 @@ dsql_var* DsqlCompilerScratch::resolveVariable(const MetaName& varName) return NULL; } -DeclareLocalTableNode* DsqlCompilerScratch::getLocalTable(const MetaName& name) +DeclareLocalTableNode* DsqlCompilerScratch::getLocalTable(const MetaName& name, bool* outerDecl) { DeclareLocalTableNode* table = nullptr; localTableNames.get(name, table); + if (outerDecl) + *outerDecl = false; + if (!table && mainScratch) + { table = mainScratch->getLocalTable(name); + if (table && outerDecl) + *outerDecl = true; + } + return table; } +USHORT DsqlCompilerScratch::getOuterLocalTableNumber(USHORT tableNumber) +{ + if (const auto innerNumber = outerLocalTablesMap.get(tableNumber)) + return *innerNumber; + + const auto innerNumber = localTableNumber++; + outerLocalTablesMap.put(tableNumber, innerNumber); + + return innerNumber; +} + void DsqlCompilerScratch::putLocalTable(DeclareLocalTableNode* table) { localTableNames.put(table->dsqlName, table); diff --git a/src/dsql/DsqlCompilerScratch.h b/src/dsql/DsqlCompilerScratch.h index 8ce76e26c13..0cca4a08e59 100644 --- a/src/dsql/DsqlCompilerScratch.h +++ b/src/dsql/DsqlCompilerScratch.h @@ -107,6 +107,7 @@ class DsqlCompilerScratch : public BlrDebugWriter mainScratch(aMainScratch), outerMessagesMap(p), outerVarsMap(p), + outerLocalTablesMap(p), ddlSchema(p), ctes(p), cteAliases(p), @@ -199,7 +200,8 @@ class DsqlCompilerScratch : public BlrDebugWriter dsql_var* makeVariable(dsql_fld*, const char*, const dsql_var::Type type, USHORT, USHORT, std::optional = std::nullopt); dsql_var* resolveVariable(const MetaName& varName); - DeclareLocalTableNode* getLocalTable(const MetaName& name); + DeclareLocalTableNode* getLocalTable(const MetaName& name, bool* outerDecl = nullptr); + USHORT getOuterLocalTableNumber(USHORT tableNumber); void putLocalTable(DeclareLocalTableNode* table); void genReturn(bool eosFlag = false); @@ -354,6 +356,7 @@ class DsqlCompilerScratch : public BlrDebugWriter DsqlCompilerScratch* mainScratch = nullptr; Firebird::NonPooledMap outerMessagesMap; // Firebird::NonPooledMap outerVarsMap; // + Firebird::NonPooledMap outerLocalTablesMap; // MetaName ddlSchema; Firebird::AutoPtr> cachedDdlSchemaSearchPath; dsql_msg* recordKeyMessage = nullptr; // Side message for positioned DML diff --git a/src/dsql/StmtNodes.cpp b/src/dsql/StmtNodes.cpp index 14863649a1d..24df56307be 100644 --- a/src/dsql/StmtNodes.cpp +++ b/src/dsql/StmtNodes.cpp @@ -2808,6 +2808,7 @@ DmlNode* EraseNode::parse(thread_db* tdbb, MemoryPool& pool, CompilerScratch* cs EraseNode* node = FB_NEW_POOL(pool) EraseNode(pool); node->stream = csb->csb_rpt[n].csb_stream; node->localTableNumber = csb->csb_rpt[n].csb_local_table_number; + node->localTableOuterDecl = csb->csb_rpt[n].csb_outer_local_table; if (csb->csb_blr_reader.peekByte() == blr_marks) node->marks |= PAR_marks(csb); @@ -3049,6 +3050,7 @@ void EraseNode::pass1Erase(thread_db* tdbb, CompilerScratch* csb, EraseNode* nod if (tail->csb_local_table_number.has_value()) { node->localTableNumber = tail->csb_local_table_number; + node->localTableOuterDecl = tail->csb_outer_local_table; return; } @@ -3234,8 +3236,9 @@ const StmtNode* EraseNode::erase(thread_db* tdbb, Request* request, WhichTrigger if (!statement) break; + const auto localTableRequest = request->getLocalTableRequest(localTableOuterDecl); const auto localTable = localTableNumber.has_value() ? - request->getStatement()->localTables[localTableNumber.value()] : nullptr; + localTableRequest->getStatement()->localTables[localTableNumber.value()] : nullptr; const Format* format = relation ? rpb->rpb_relation->currentFormat(tdbb) : localTable->format.getObject(); Record* record = relation ? VIO_record(tdbb, rpb, format, tdbb->getDefaultPool()) : rpb->rpb_record; @@ -3309,9 +3312,10 @@ const StmtNode* EraseNode::erase(thread_db* tdbb, Request* request, WhichTrigger if (!relation) { fb_assert(localTableNumber.has_value()); - const auto localTable = request->getStatement()->localTables[localTableNumber.value()]; + const auto localTableRequest = request->getLocalTableRequest(localTableOuterDecl); + const auto localTable = localTableRequest->getStatement()->localTables[localTableNumber.value()]; - if (!localTable->getImpure(tdbb, request)->recordBuffer->erase(rpb->rpb_number.getValue())) + if (!localTable->getImpure(tdbb, localTableRequest)->recordBuffer->erase(rpb->rpb_number.getValue())) ERR_post(Arg::Gds(isc_no_cur_rec)); } else if (auto* extFile = relation->getExtFile()) @@ -8330,6 +8334,7 @@ DmlNode* ModifyNode::parse(thread_db* tdbb, MemoryPool& pool, CompilerScratch* c tail->csb_relation = csb->csb_rpt[orgStream].csb_relation; tail->csb_format = csb->csb_rpt[orgStream].csb_format; tail->csb_local_table_number = csb->csb_rpt[orgStream].csb_local_table_number; + tail->csb_outer_local_table = csb->csb_rpt[orgStream].csb_outer_local_table; // Make the node and parse the sub-expression. @@ -8337,6 +8342,7 @@ DmlNode* ModifyNode::parse(thread_db* tdbb, MemoryPool& pool, CompilerScratch* c node->orgStream = orgStream; node->newStream = newStream; node->localTableNumber = csb->csb_rpt[orgStream].csb_local_table_number; + node->localTableOuterDecl = csb->csb_rpt[orgStream].csb_outer_local_table; if (csb->csb_blr_reader.peekByte() == blr_marks) node->marks |= PAR_marks(csb); @@ -8704,6 +8710,7 @@ void ModifyNode::pass1Modify(thread_db* tdbb, CompilerScratch* csb, ModifyNode* if (tail->csb_local_table_number.has_value()) { node->localTableNumber = tail->csb_local_table_number; + node->localTableOuterDecl = tail->csb_outer_local_table; makeValidation(tdbb, csb, newStream, node->validations); return; } @@ -8961,11 +8968,12 @@ const StmtNode* ModifyNode::modify(thread_db* tdbb, Request* request, WhichTrigg if (!relation) { fb_assert(localTableNumber.has_value()); - const auto localTable = request->getStatement()->localTables[localTableNumber.value()]; + const auto localTableRequest = request->getLocalTableRequest(localTableOuterDecl); + const auto localTable = localTableRequest->getStatement()->localTables[localTableNumber.value()]; DeclareLocalTableNode::validateRecord(localTable, newRpb->rpb_record); - if (!localTable->getImpure(tdbb, request)->recordBuffer->modify( + if (!localTable->getImpure(tdbb, localTableRequest)->recordBuffer->modify( orgRpb->rpb_number.getValue(), newRpb->rpb_record)) { ERR_post(Arg::Gds(isc_no_cur_rec)); @@ -9090,8 +9098,9 @@ const StmtNode* ModifyNode::modify(thread_db* tdbb, Request* request, WhichTrigg // exists for the stream and is big enough, and copying fields from the // original record to the new record. + const auto localTableRequest = request->getLocalTableRequest(localTableOuterDecl); const auto localTable = localTableNumber.has_value() ? - request->getStatement()->localTables[localTableNumber.value()] : nullptr; + localTableRequest->getStatement()->localTables[localTableNumber.value()] : nullptr; const Format* const newFormat = relation ? newRpb->rpb_relation->currentFormat(tdbb) : localTable->format.getObject(); Record* newRecord = relation ? VIO_record(tdbb, newRpb, newFormat, tdbb->getDefaultPool()) : newRpb->rpb_record; @@ -9176,6 +9185,30 @@ DmlNode* OuterMapNode::parse(thread_db* tdbb, MemoryPool& pool, CompilerScratch* break; } + case blr_outer_map_local_table: + { + const USHORT outerNumber = blrReader.getWord(); + const USHORT innerNumber = blrReader.getWord(); + + if (outerNumber >= csb->mainCsb->csb_localTables.getCount() || + !csb->mainCsb->csb_localTables[outerNumber]) + { + PAR_error(csb, Arg::Gds(isc_bad_loctab_num) << Arg::Num(outerNumber)); + } + + csb->csb_localTables.grow(innerNumber + 1); + + if (csb->csb_localTables[innerNumber]) + { + PAR_error(csb, Arg::Gds(isc_random) << + "Invalid blr_outer_map_local_table: inner local table already exists"); + } + + csb->csb_localTables[innerNumber] = csb->mainCsb->csb_localTables[outerNumber]; + csb->outerLocalTablesMap.put(innerNumber, outerNumber); + break; + } + default: PAR_error(csb, Arg::Gds(isc_random) << "Invalid blr_outer_map sub code"); } @@ -9237,6 +9270,32 @@ OuterMapNode* OuterMapNode::pass1(thread_db* tdbb, CompilerScratch* csb) innerVariables[innerNumber] = outerVariables[outerNumber]; } + for (const auto& [innerNumber, outerNumber] : csb->outerLocalTablesMap) + { + if (outerNumber >= csb->mainCsb->csb_localTables.getCount() || + !csb->mainCsb->csb_localTables[outerNumber]) + { + fb_assert(false); + status_exception::raise(Arg::Gds(isc_bad_loctab_num) << Arg::Num(outerNumber)); + } + + const auto outerLocalTable = csb->mainCsb->csb_localTables[outerNumber]; + + csb->csb_localTables.grow(innerNumber + 1); + + if (csb->csb_localTables[innerNumber]) + { + if (csb->csb_localTables[innerNumber] != outerLocalTable) + { + fb_assert(false); + status_exception::raise(Arg::Gds(isc_random) << + "Invalid blr_outer_map_local_table: inner local table already exist"); + } + } + else + csb->csb_localTables[innerNumber] = outerLocalTable; + } + return this; } @@ -10044,10 +10103,12 @@ const StmtNode* StoreNode::store(thread_db* tdbb, Request* request, WhichTrigger jrd_rel* relation = rpb->rpb_relation; const auto localTableSource = nodeAs(target); + const auto localTableRequest = request->getLocalTableRequest( + localTableSource && localTableSource->outerDecl); const auto localTable = localTableSource ? - request->getStatement()->localTables[localTableSource->tableNumber] : + localTableRequest->getStatement()->localTables[localTableSource->tableNumber] : nullptr; - const auto localTableImpure = localTable ? localTable->getImpure(tdbb, request) : nullptr; + const auto localTableImpure = localTable ? localTable->getImpure(tdbb, localTableRequest) : nullptr; switch (request->req_operation) { @@ -10143,7 +10204,7 @@ const StmtNode* StoreNode::store(thread_db* tdbb, Request* request, WhichTrigger // to "missing." const Format* format = localTableSource ? - request->getStatement()->localTables[localTableSource->tableNumber]->format : + localTableRequest->getStatement()->localTables[localTableSource->tableNumber]->format : relation->currentFormat(tdbb); Record* record; diff --git a/src/dsql/StmtNodes.h b/src/dsql/StmtNodes.h index 39910d6fddc..e1aa23c896b 100644 --- a/src/dsql/StmtNodes.h +++ b/src/dsql/StmtNodes.h @@ -605,6 +605,7 @@ class EraseNode final : public TypedNode StreamType stream = 0; unsigned marks = 0; // see StmtNode::IUD_MARK_xxx std::optional localTableNumber; + bool localTableOuterDecl = false; }; @@ -1305,6 +1306,7 @@ class ModifyNode final : public TypedNode USHORT dsqlRseFlags = 0; std::optional dsqlReturningLocalTableNumber; std::optional localTableNumber; + bool localTableOuterDecl = false; }; diff --git a/src/dsql/dsql.h b/src/dsql/dsql.h index c0e351493e7..05197ab851e 100644 --- a/src/dsql/dsql.h +++ b/src/dsql/dsql.h @@ -486,6 +486,7 @@ class dsql_ctx : public pool_alloc dsql_map* ctx_map = nullptr; // Maps for aggregates and unions RseNode* ctx_rse = nullptr; // Sub-rse for aggregates dsql_ctx* ctx_parent = nullptr; // Parent context for aggregates + bool ctx_local_table_outer = false; // Local table belongs to an outer PSQL scope USHORT ctx_context = 0; // Context id USHORT ctx_recursive = 0; // Secondary context id for recursive UNION (nobody referred to this context) USHORT ctx_scope_level = 0; // Subquery level within this request @@ -508,6 +509,7 @@ class dsql_ctx : public pool_alloc ctx_map = v.ctx_map; ctx_rse = v.ctx_rse; ctx_parent = v.ctx_parent; + ctx_local_table_outer = v.ctx_local_table_outer; ctx_alias = v.ctx_alias; ctx_context = v.ctx_context; ctx_recursive = v.ctx_recursive; diff --git a/src/dsql/pass1.cpp b/src/dsql/pass1.cpp index a372ccfb443..fec8bdf19e1 100644 --- a/src/dsql/pass1.cpp +++ b/src/dsql/pass1.cpp @@ -368,6 +368,7 @@ dsql_ctx* PASS1_make_context(DsqlCompilerScratch* dsqlScratch, RecordSourceNode* dsql_rel* relation = NULL; dsql_prc* procedure = NULL; dsql_tab_func* tableValueFunctionContext = nullptr; + bool outerLocalTable = false; if (selNode) { @@ -386,7 +387,7 @@ dsql_ctx* PASS1_make_context(DsqlCompilerScratch* dsqlScratch, RecordSourceNode* else if (!tableValueFunctionNode && !(procNode && procNode->inputSources) && !name.schema.hasData() && !name.package.hasData()) { - if (const auto localTable = dsqlScratch->getLocalTable(name.object)) + if (const auto localTable = dsqlScratch->getLocalTable(name.object, &outerLocalTable)) relation = localTable->dsqlRelation; } @@ -455,6 +456,7 @@ dsql_ctx* PASS1_make_context(DsqlCompilerScratch* dsqlScratch, RecordSourceNode* context->ctx_relation = relation; context->ctx_procedure = procedure; context->ctx_table_value_fun = tableValueFunctionContext; + context->ctx_local_table_outer = outerLocalTable; if (selNode) { @@ -1814,7 +1816,10 @@ RecordSourceNode* PASS1_relation(DsqlCompilerScratch* dsqlScratch, RecordSourceN const auto localTableNode = FB_NEW_POOL(*tdbb->getDefaultPool()) LocalTableSourceNode( *tdbb->getDefaultPool()); localTableNode->dsqlContext = context; - localTableNode->tableNumber = context->ctx_relation->rel_local_table_number.value(); + localTableNode->outerDecl = context->ctx_local_table_outer; + localTableNode->tableNumber = context->ctx_local_table_outer ? + dsqlScratch->getOuterLocalTableNumber(context->ctx_relation->rel_local_table_number.value()) : + context->ctx_relation->rel_local_table_number.value(); return localTableNode; } else diff --git a/src/include/firebird/impl/blr.h b/src/include/firebird/impl/blr.h index d2218377775..f7c756c6962 100644 --- a/src/include/firebird/impl/blr.h +++ b/src/include/firebird/impl/blr.h @@ -469,6 +469,7 @@ #define blr_outer_map (unsigned char) 221 #define blr_outer_map_message (unsigned char) 1 #define blr_outer_map_variable (unsigned char) 2 +#define blr_outer_map_local_table (unsigned char) 3 // json functions (reserved) #define blr_json_function (unsigned char) 222 diff --git a/src/jrd/RecordSourceNodes.cpp b/src/jrd/RecordSourceNodes.cpp index 7b4e0152608..e29b20a9338 100644 --- a/src/jrd/RecordSourceNodes.cpp +++ b/src/jrd/RecordSourceNodes.cpp @@ -646,6 +646,7 @@ LocalTableSourceNode* LocalTableSourceNode::parse(thread_db* tdbb, CompilerScrat *tdbb->getDefaultPool()); node->tableNumber = tableNumber; + node->outerDecl = csb->outerLocalTablesMap.exist(tableNumber); AutoPtr aliasString(FB_NEW_POOL(csb->csb_pool) string(csb->csb_pool)); csb->csb_blr_reader.getString(*aliasString); @@ -667,6 +668,7 @@ LocalTableSourceNode* LocalTableSourceNode::parse(thread_db* tdbb, CompilerScrat csb->csb_rpt[node->stream].csb_format = csb->csb_localTables[tableNumber]->format; csb->csb_rpt[node->stream].csb_alias = aliasString.release(); csb->csb_rpt[node->stream].csb_local_table_number = tableNumber; + csb->csb_rpt[node->stream].csb_outer_local_table = node->outerDecl; } return node; @@ -678,6 +680,7 @@ string LocalTableSourceNode::internalPrint(NodePrinter& printer) const NODE_PRINT(printer, alias); NODE_PRINT(printer, tableNumber); + NODE_PRINT(printer, outerDecl); NODE_PRINT(printer, context); return "LocalTableSourceNode"; @@ -718,6 +721,7 @@ LocalTableSourceNode* LocalTableSourceNode::copy(thread_db* tdbb, NodeCopier& co newSource->context = context; newSource->alias = alias; newSource->tableNumber = tableNumber; + newSource->outerDecl = outerDecl; if (tableNumber >= copier.csb->csb_localTables.getCount() || !copier.csb->csb_localTables[tableNumber]) ERR_post(Arg::Gds(isc_bad_loctab_num) << Arg::Num(tableNumber)); @@ -727,6 +731,7 @@ LocalTableSourceNode* LocalTableSourceNode::copy(thread_db* tdbb, NodeCopier& co element->csb_format = copier.csb->csb_localTables[tableNumber]->format; element->csb_view_stream = copier.remap[0]; element->csb_local_table_number = tableNumber; + element->csb_outer_local_table = outerDecl; if (alias.hasData()) { @@ -763,7 +768,7 @@ RecordSource* LocalTableSourceNode::compile(thread_db* tdbb, Optimizer* opt, boo auto localTable = csb->csb_localTables[tableNumber]; - return FB_NEW_POOL(*tdbb->getDefaultPool()) LocalTableStream(csb, stream, localTable); + return FB_NEW_POOL(*tdbb->getDefaultPool()) LocalTableStream(csb, stream, localTable, outerDecl); } diff --git a/src/jrd/RecordSourceNodes.h b/src/jrd/RecordSourceNodes.h index 927e130f6a3..8a41683b40b 100644 --- a/src/jrd/RecordSourceNodes.h +++ b/src/jrd/RecordSourceNodes.h @@ -348,6 +348,7 @@ class LocalTableSourceNode final : public TypedNodecsb_localTables; csb->csb_localTables.clear(); + if (csb->outerLocalTablesMap.count()) + { + outerLocalTables.grow(localTables.getCount()); + + for (const auto& [innerNumber, outerNumber] : csb->outerLocalTablesMap) + { + fb_assert(innerNumber < outerLocalTables.getCount()); + outerLocalTables[innerNumber] = 1; + } + } + // make a vector of all invariant-type nodes, so that we will // be able to easily reinitialize them when we restart the request invariants.join(csb->csb_invariants); @@ -186,6 +198,7 @@ Statement::Statement(thread_db* tdbb, MemoryPool* p, CompilerScratch* csb) csb->subProcedures.clear(); csb->outerMessagesMap.clear(); csb->outerVarsMap.clear(); + csb->outerLocalTablesMap.clear(); csb->csb_rpt.free(); csb->csb_resources = nullptr; } @@ -1036,6 +1049,19 @@ bool Request::isRoot() const return this == statement->rootRequest(); } +Request* Request::getLocalTableRequest(bool outerDecl) +{ + Request* request = this; + + if (outerDecl) + { + while (request->getStatement()->parentStatement) + request = request->req_caller; + } + + return request; +} + StmtNumber Request::getRequestId() const { if (!req_id) diff --git a/src/jrd/Statement.h b/src/jrd/Statement.h index 01e6faf5cd1..54cae467477 100644 --- a/src/jrd/Statement.h +++ b/src/jrd/Statement.h @@ -147,6 +147,7 @@ class Statement : public pool_alloc const StmtNode* topNode; // top of execution tree Firebird::Array fors; // select expressions Firebird::Array localTables; // local tables + Firebird::Array outerLocalTables; // local tables declared in an outer PSQL scope Firebird::Array invariants; // pointer to nodes invariant offsets Firebird::RefStrPtr sqlText; // SQL text (encoded in the metadata charset) Firebird::Array blr; // BLR for non-SQL query diff --git a/src/jrd/exe.cpp b/src/jrd/exe.cpp index 3c3ea529dd1..bfe1b61d25e 100644 --- a/src/jrd/exe.cpp +++ b/src/jrd/exe.cpp @@ -1244,8 +1244,13 @@ void EXE_unwind(thread_db* tdbb, Request* request) tdbb->setTransaction(old_transaction); } - for (auto localTable : statement->localTables) + for (FB_SIZE_T i = 0; i < statement->localTables.getCount(); ++i) { + if (i < statement->outerLocalTables.getCount() && statement->outerLocalTables[i]) + continue; + + const auto localTable = statement->localTables[i]; + if (!localTable) continue; diff --git a/src/jrd/exe.h b/src/jrd/exe.h index 07ec4dbc13d..d05358a4222 100644 --- a/src/jrd/exe.h +++ b/src/jrd/exe.h @@ -492,6 +492,7 @@ class CompilerScratch : public pool_alloc subProcedures(p), outerMessagesMap(p), outerVarsMap(p), + outerLocalTablesMap(p), csb_schema(p), csb_currentForNode(NULL), csb_currentDMLNode(NULL), @@ -615,6 +616,7 @@ class CompilerScratch : public pool_alloc Firebird::LeftPooledMap subProcedures; Firebird::NonPooledMap outerMessagesMap; // Firebird::NonPooledMap outerVarsMap; // + Firebird::NonPooledMap outerLocalTablesMap; // MetaName csb_schema; @@ -638,6 +640,7 @@ class CompilerScratch : public pool_alloc std::optional csb_cursor_number; // Cursor number for this stream std::optional csb_local_table_number; // Local table number for this stream + bool csb_outer_local_table = false; // Local table belongs to an outer PSQL scope StreamType csb_stream; // Map user context to internal stream StreamType csb_view_stream; // stream number for view relation, below USHORT csb_flags; diff --git a/src/jrd/recsrc/LocalTableStream.cpp b/src/jrd/recsrc/LocalTableStream.cpp index 9c748d25547..f6a6736680d 100644 --- a/src/jrd/recsrc/LocalTableStream.cpp +++ b/src/jrd/recsrc/LocalTableStream.cpp @@ -36,9 +36,11 @@ using namespace Jrd; // Data access: local table // ------------------------ -LocalTableStream::LocalTableStream(CompilerScratch* csb, StreamType stream, const DeclareLocalTableNode* table) +LocalTableStream::LocalTableStream(CompilerScratch* csb, StreamType stream, const DeclareLocalTableNode* table, + bool outerDecl) : RecordStream(csb, stream), - m_table(table) + m_table(table), + m_outerDecl(outerDecl) { fb_assert(m_table); @@ -88,7 +90,8 @@ bool LocalTableStream::internalGetRecord(thread_db* tdbb) const if (!rpb->rpb_record) rpb->rpb_record = FB_NEW_POOL(*tdbb->getDefaultPool()) Record(*tdbb->getDefaultPool(), m_format); - const auto recordBuffer = m_table->getImpure(tdbb, request)->recordBuffer; + const auto localTableRequest = request->getLocalTableRequest(m_outerDecl); + const auto recordBuffer = m_table->getImpure(tdbb, localTableRequest)->recordBuffer; while (true) { diff --git a/src/jrd/recsrc/RecordSource.h b/src/jrd/recsrc/RecordSource.h index e85064bc5d8..b475794fe01 100644 --- a/src/jrd/recsrc/RecordSource.h +++ b/src/jrd/recsrc/RecordSource.h @@ -1452,7 +1452,8 @@ namespace Jrd class LocalTableStream final : public RecordStream { public: - LocalTableStream(CompilerScratch* csb, StreamType stream, const DeclareLocalTableNode* table); + LocalTableStream(CompilerScratch* csb, StreamType stream, const DeclareLocalTableNode* table, + bool outerDecl); void close(thread_db* tdbb) const override; @@ -1468,6 +1469,7 @@ namespace Jrd private: const DeclareLocalTableNode* m_table; + bool m_outerDecl = false; }; class Union final : public RecordStream diff --git a/src/jrd/req.h b/src/jrd/req.h index dcde6cfb0c8..65cf29554d5 100644 --- a/src/jrd/req.h +++ b/src/jrd/req.h @@ -368,6 +368,8 @@ class Request : public pool_alloc return statement; } + Request* getLocalTableRequest(bool outerDecl); + bool hasInternalStatement() const noexcept; bool hasPowerfulStatement() const noexcept; diff --git a/src/yvalve/gds.cpp b/src/yvalve/gds.cpp index d53494c7fcb..f423024476e 100644 --- a/src/yvalve/gds.cpp +++ b/src/yvalve/gds.cpp @@ -4081,7 +4081,8 @@ static void blr_print_verb(gds_ctl* control, SSHORT level) { nullptr, "message", - "variable" + "variable", + "local_table" }; while ((blr_operator = control->ctl_blr_reader.getByte()) != blr_end) @@ -4097,6 +4098,7 @@ static void blr_print_verb(gds_ctl* control, SSHORT level) { case blr_outer_map_message: case blr_outer_map_variable: + case blr_outer_map_local_table: blr_print_word(control); n = blr_print_word(control); offset = blr_print_line(control, offset);