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..f4e139d967f --- /dev/null +++ b/doc/sql.extensions/README.declared_local_temporary_tables.md @@ -0,0 +1,255 @@ +# 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. +- 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: + +```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. + +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 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. + +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: + +- A single PSQL statement, procedure, function or trigger may declare at most 1024 local temporary tables. +- 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/DdlNodes.epp b/src/dsql/DdlNodes.epp index 46914c5a735..e28d74237de 100644 --- a/src/dsql/DdlNodes.epp +++ b/src/dsql/DdlNodes.epp @@ -9963,7 +9963,7 @@ void CreateRelationNode::defineLocalTempTable(thread_db* tdbb, DsqlCompilerScrat if (externalFile) status_exception::raise(Arg::Gds(isc_random) << "External file is not allowed for local temporary tables"); - if (attachment->att_local_temporary_tables.count() >= MAX_LTT_COUNT) + if (attachment->att_local_temporary_tables.count() >= MAX_CREATED_LTT_COUNT) { ERR_post(Arg::Gds(isc_imp_exc) << Arg::Gds(isc_random) << @@ -10033,14 +10033,14 @@ void CreateRelationNode::defineLocalTempTable(thread_db* tdbb, DsqlCompilerScrat // Assign LTT relation ID if (!attachment->att_next_ltt_id.has_value()) - attachment->att_next_ltt_id = MAX_LTT_ID; + attachment->att_next_ltt_id = MAX_CREATED_LTT_ID; const auto startLttId = attachment->att_next_ltt_id.value(); while (true) { - if (attachment->att_next_ltt_id.value() >= MAX_LTT_ID) - attachment->att_next_ltt_id = MIN_LTT_ID; + if (attachment->att_next_ltt_id.value() >= MAX_CREATED_LTT_ID) + attachment->att_next_ltt_id = MIN_CREATED_LTT_ID; else ++attachment->att_next_ltt_id.value(); diff --git a/src/dsql/DsqlCompilerScratch.cpp b/src/dsql/DsqlCompilerScratch.cpp index da42cc39241..9eaca3598be 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.isEmpty() && outerVarsMap.isEmpty() && outerLocalTablesMap.isEmpty()) 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,6 +560,41 @@ dsql_var* DsqlCompilerScratch::resolveVariable(const MetaName& varName) return NULL; } +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); +} + // Generate BLR for a return. void DsqlCompilerScratch::genReturn(bool eosFlag) { diff --git a/src/dsql/DsqlCompilerScratch.h b/src/dsql/DsqlCompilerScratch.h index 233567c4dc2..ebd3f416d34 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), @@ -106,6 +107,7 @@ class DsqlCompilerScratch : public BlrDebugWriter mainScratch(aMainScratch), outerMessagesMap(p), outerVarsMap(p), + outerLocalTablesMap(p), ddlSchema(p), ctes(p), cteAliases(p), @@ -198,6 +200,11 @@ 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, bool* outerDecl = nullptr); + USHORT getOuterLocalTableNumber(USHORT tableNumber); + void putLocalTable(DeclareLocalTableNode* table); + void genReturn(bool eosFlag = false); void genParameters(Firebird::Array >& parameters, @@ -323,6 +330,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" @@ -350,6 +358,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/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..8fee28ced14 100644 --- a/src/dsql/StmtNodes.cpp +++ b/src/dsql/StmtNodes.cpp @@ -37,6 +37,7 @@ #include "../jrd/Coercion.h" #include "../jrd/Function.h" #include "../jrd/optimizer/Optimizer.h" +#include "../jrd/RecordBuffer.h" #include "../jrd/RecordSourceNodes.h" #include "../jrd/VirtualTable.h" #include "../jrd/extds/ExtDS.h" @@ -121,6 +122,48 @@ static void validateExpressions(thread_db* tdbb, const Array& vali namespace { + class AutoLocalTableContext + { + public: + AutoLocalTableContext(thread_db* aTdbb, Request* aRequest, Request* localTableRequest, + jrd_tra* transaction) + : tdbb(aTdbb), + request(aRequest), + oldTransaction(aTdbb->getTransaction()), + oldFrameId(aTdbb->tdbb_temp_frame_id) + { + oldSnapshot.init(); + tdbb->setTransaction(transaction); + tdbb->tdbb_temp_frame_id = localTableRequest ? localTableRequest->getLocalTableInstanceId(tdbb) : 0; + + Request::AutoTranCtx autoTranCtx; + + if (localTableRequest && localTableRequest->getLocalTableAutoTranCtx(autoTranCtx)) + { + restoreSnapshot = true; + oldSnapshot = request->req_snapshot; + request->req_snapshot = autoTranCtx.m_snapshot; + } + } + + ~AutoLocalTableContext() + { + if (restoreSnapshot) + request->req_snapshot = oldSnapshot; + + tdbb->tdbb_temp_frame_id = oldFrameId; + tdbb->setTransaction(oldTransaction); + } + + private: + thread_db* tdbb; + Request* request; + jrd_tra* oldTransaction; + FB_UINT64 oldFrameId; + Request::SnapshotData oldSnapshot; + bool restoreSnapshot = false; + }; + // Node copier that remaps the field id 0 of stream 0 to a given field id. class RemapFieldNodeCopier : public NodeCopier { @@ -1749,6 +1792,10 @@ DmlNode* DeclareLocalTableNode::parse(thread_db* tdbb, MemoryPool& pool, Compile { switch (verb) { + case blr_dcl_local_table_ltt: + node->useLtt = true; + break; + case blr_dcl_local_table_format: if (node->format) PAR_error(csb, Arg::Gds(isc_random) << "duplicate local table format"); @@ -1756,12 +1803,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 +1834,124 @@ 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()); + } + + useLtt = true; 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 +1961,8 @@ string DeclareLocalTableNode::internalPrint(NodePrinter& printer) const StmtNode::internalPrint(printer); NODE_PRINT(printer, tableNumber); + NODE_PRINT(printer, dsqlName); + NODE_PRINT(printer, useLtt); return "DeclareLocalTableNode"; } @@ -1804,19 +1971,44 @@ void DeclareLocalTableNode::genBlr(DsqlCompilerScratch* dsqlScratch) { dsqlScratch->appendUChar(blr_dcl_local_table); dsqlScratch->appendUShort(tableNumber); + + if (useLtt) + dsqlScratch->appendUChar(blr_dcl_local_table_ltt); + + 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; + node->useLtt = useLtt; return node; } -DeclareLocalTableNode* DeclareLocalTableNode::pass2(thread_db* /*tdbb*/, CompilerScratch* csb) +DeclareLocalTableNode* DeclareLocalTableNode::pass2(thread_db* tdbb, CompilerScratch* csb) { impureOffset = csb->allocImpure(); + + if (useLtt) + getRelation(tdbb, nullptr); + return this; } @@ -1824,7 +2016,12 @@ const StmtNode* DeclareLocalTableNode::execute(thread_db* tdbb, Request* request { if (request->req_operation == Request::req_evaluate) { - if (auto& recordBuffer = getImpure(tdbb, request, false)->recordBuffer) + if (useLtt) + { + reset(tdbb, request); + getRelation(tdbb, request); + } + else if (auto& recordBuffer = getImpure(tdbb, request, false)->recordBuffer) recordBuffer->reset(); request->req_operation = Request::req_return; @@ -1833,12 +2030,31 @@ const StmtNode* DeclareLocalTableNode::execute(thread_db* tdbb, Request* request return parentStmt; } -DeclareLocalTableNode::Impure* DeclareLocalTableNode::getImpure(thread_db* tdbb, Request* request, bool createWhenDead) const +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); if (createWhenDead && !impure->recordBuffer) { + fb_assert(!useLtt); + impure->recordBuffer = FB_NEW_POOL(*tdbb->getDefaultPool()) RecordBuffer(*tdbb->getDefaultPool(), format); } @@ -1846,6 +2062,79 @@ DeclareLocalTableNode::Impure* DeclareLocalTableNode::getImpure(thread_db* tdbb, return impure; } +jrd_rel* DeclareLocalTableNode::getRelation(thread_db* tdbb, Request* request) const +{ + if (relation) + return relation; + + auto& pool = request ? *request->req_pool : *tdbb->getDefaultPool(); + + if (tableNumber >= MAX_DECLARED_LTT_COUNT) + { + ERR_post(Arg::Gds(isc_imp_exc) << + Arg::Gds(isc_random) << + Arg::Str("Too many local temporary tables declared in a single statement")); + } + + const auto id = MIN_DECLARED_LTT_ID + tableNumber; + + QualifiedName name; + name.object.printf("RDB$PSQL_LTT_%u", tableNumber); + + const auto permanent = FB_NEW_POOL(pool) Cached::Relation(tdbb, pool, id); + permanent->rel_name = name; + permanent->rel_flags = REL_sql_relation | REL_temp_ltt | REL_temp_frame; + permanent->getBasePages()->rel_pg_space_id = tdbb->getDatabase()->dbb_page_manager.getTempPageSpaceID(tdbb); + + const auto newRelation = FB_NEW_POOL(pool) jrd_rel(pool, permanent); + newRelation->rel_current_fmt = 1; + newRelation->rel_dbkey_length = 8; + newRelation->rel_fields = vec::newVector(pool, newRelation->rel_fields, format->fmt_count); + + const auto relFormat = Format::newFormat(pool, format->fmt_count); + relFormat->fmt_length = format->fmt_length; + relFormat->fmt_version = newRelation->rel_current_fmt; + + for (FB_SIZE_T i = 0; i < format->fmt_count; ++i) + { + relFormat->fmt_desc[i] = format->fmt_desc[i]; + + auto& field = (*newRelation->rel_fields)[i]; + field = FB_NEW_POOL(pool) jrd_fld(pool); + field->fld_name.printf("FIELD_%" SIZEFORMAT, i + 1); + field->fld_length = relFormat->fmt_desc[i].dsc_length; + field->fld_pos = i; + field->fld_flags = notNullFields[i] ? FLD_not_null : 0; + } + + permanent->addFormat(relFormat); + newRelation->rel_current_format = relFormat; + relation = newRelation; + + return relation; +} + +void DeclareLocalTableNode::reset(thread_db* tdbb, Request* request) const +{ + if (relation) + { + const auto permanent = relation->getPermanent(); + permanent->delPages(tdbb, request->getLocalTableInstanceId(tdbb)); + } +} + +void DeclareLocalTableNode::destroyRelation(thread_db* tdbb) const +{ + if (relation) + { + const auto permanent = relation->getPermanent(); + permanent->freePages(tdbb); + jrd_rel::destroy(tdbb, relation); + Cached::Relation::cleanup(tdbb, permanent); + relation = nullptr; + } +} + //-------------------- @@ -2656,6 +2945,8 @@ 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); @@ -2668,14 +2959,20 @@ 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 +3030,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 +3184,16 @@ 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; + node->localTableOuterDecl = tail->csb_outer_local_table; + return; + } + + ERR_post(Arg::Gds(isc_wish_list) << Arg::Gds(isc_random) << "erase non-relation source"); } view = relation->isView() ? relation : view; @@ -3061,6 +3362,25 @@ const StmtNode* EraseNode::erase(thread_db* tdbb, Request* request, WhichTrigger jrd_tra* transaction = request->req_transaction; record_param* rpb = &request->req_rpb[stream]; jrd_rel* relation = rpb->rpb_relation; + Request* localTableRequest = request; + const DeclareLocalTableNode* localTable = nullptr; + + if (localTableNumber.has_value()) + { + localTableRequest = request->getLocalTableRequest(localTableOuterDecl); + localTable = localTableRequest->getStatement()->localTables[localTableNumber.value()]; + + if (localTable->useLtt) + { + transaction = localTableRequest->getLocalTableTransaction(); + + if (!relation) + relation = rpb->rpb_relation = localTable->getRelation(tdbb, localTableRequest); + } + } + + AutoLocalTableContext autoTransaction(tdbb, request, + (localTable && localTable->useLtt ? localTableRequest : nullptr), transaction); switch (request->req_operation) { @@ -3074,8 +3394,11 @@ 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 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 +3421,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 +3458,19 @@ 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 (auto* extFile = relation->getExtFile()) + if (!relation) + { + fb_assert(localTable); + fb_assert(false); + ERR_post(Arg::Gds(isc_wish_list)); + } + else if (auto* extFile = relation->getExtFile()) extFile->erase(rpb, transaction); else if (relation->isVirtual()) VirtualTable::erase(tdbb, rpb); @@ -3176,28 +3508,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 +7284,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 +7361,7 @@ void LocalDeclarationsNode::genBlr(DsqlCompilerScratch* dsqlScratch) DsqlDescMaker::fromField(&variable->desc, variable->field); } else if (nodeIs(parameter) || + nodeIs(parameter) || nodeIs(parameter) || nodeIs(parameter)) { @@ -8149,12 +8484,17 @@ 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; + tail->csb_outer_local_table = csb->csb_rpt[orgStream].csb_outer_local_table; // 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; + node->localTableOuterDecl = csb->csb_rpt[orgStream].csb_outer_local_table; if (csb->csb_blr_reader.peekByte() == blr_marks) node->marks |= PAR_marks(csb); @@ -8192,8 +8532,13 @@ 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 +8858,17 @@ 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; + node->localTableOuterDecl = tail->csb_outer_local_table; + 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; @@ -8715,13 +9065,31 @@ const StmtNode* ModifyNode::modify(thread_db* tdbb, Request* request, WhichTrigg record_param* newRpb = &request->req_rpb[newStream]; + const auto localTableRequest = request->getLocalTableRequest(localTableOuterDecl); + const auto localTable = localTableNumber.has_value() ? + localTableRequest->getStatement()->localTables[localTableNumber.value()] : nullptr; + + if (localTable && localTable->useLtt) + { + transaction = localTableRequest->getLocalTableTransaction(); + + if (!relation) + relation = orgRpb->rpb_relation = localTable->getRelation(tdbb, localTableRequest); + + if (!newRpb->rpb_relation) + newRpb->rpb_relation = relation; + } + + AutoLocalTableContext autoTransaction(tdbb, request, + (localTable && localTable->useLtt ? localTableRequest : nullptr), transaction); + switch (request->req_operation) { case Request::req_evaluate: 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 +9108,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 +9124,28 @@ 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 (localTable) + DeclareLocalTableNode::validateRecord(localTable, newRpb->rpb_record); + + if (!relation) + { + fb_assert(localTable && !localTable->useLtt); + fb_assert(false); + ERR_post(Arg::Gds(isc_wish_list)); + } + else if (auto* extFile = relation->getExtFile()) extFile->modify(orgRpb, newRpb, transaction); else if (relation->isVirtual()) VirtualTable::modify(tdbb, orgRpb, newRpb); @@ -8799,23 +9180,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 +9228,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 +9237,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 +9264,12 @@ 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 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 +9278,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 +9289,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); @@ -8956,6 +9348,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"); } @@ -9017,6 +9433,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; } @@ -9824,10 +10266,22 @@ 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->useLtt ? + localTable->getImpure(tdbb, localTableRequest) : nullptr; + + if (localTable && localTable->useLtt) + { + transaction = localTableRequest->getLocalTableTransaction(); + relation = rpb->rpb_relation = localTable->getRelation(tdbb, localTableRequest); + } + + AutoLocalTableContext autoTransaction(tdbb, request, + (localTable && localTable->useLtt ? localTableRequest : nullptr), transaction); switch (request->req_operation) { @@ -9850,7 +10304,8 @@ const StmtNode* StoreNode::store(thread_db* tdbb, Request* request, WhichTrigger { SavepointChangeMarker scMarker(transaction); - if (relation && (relation->rel_triggers[TRIGGER_PRE_STORE] || relation->isSystem()) && whichTrig != POST_TRIG) + if (!localTable && relation && + (relation->rel_triggers[TRIGGER_PRE_STORE] || relation->isSystem()) && whichTrig != POST_TRIG) { EXE_execute_triggers(tdbb, relation->rel_triggers[TRIGGER_PRE_STORE], NULL, rpb, TRIGGER_INSERT, PRE_TRIG); @@ -9868,7 +10323,10 @@ const StmtNode* StoreNode::store(thread_db* tdbb, Request* request, WhichTrigger cleanupRpb(tdbb, rpb); - if (localTableSource) + if (localTable) + DeclareLocalTableNode::validateRecord(localTable, rpb->rpb_record); + + if (localTable && !localTable->useLtt) localTableImpure->recordBuffer->store(rpb->rpb_record); else if (auto* extFile = relation->getExtFile()) extFile->store(tdbb, rpb); @@ -9883,7 +10341,8 @@ const StmtNode* StoreNode::store(thread_db* tdbb, Request* request, WhichTrigger rpb->rpb_number.setValid(true); - if (relation && (relation->rel_triggers[TRIGGER_POST_STORE] || relation->isSystem()) && + if (!localTable && relation && + (relation->rel_triggers[TRIGGER_POST_STORE] || relation->isSystem()) && whichTrig != PRE_TRIG) { EXE_execute_triggers(tdbb, relation->rel_triggers[TRIGGER_POST_STORE], NULL, rpb, @@ -9919,13 +10378,12 @@ const StmtNode* StoreNode::store(thread_db* tdbb, Request* request, WhichTrigger // exists for the stream and is big enough, and initialize all null flags // to "missing." - const Format* format = localTableSource ? - request->getStatement()->localTables[localTableSource->tableNumber]->format : + const Format* format = localTable && !localTable->useLtt ? localTable->format.getObject() : relation->currentFormat(tdbb); Record* record; - if (localTableSource) + if (localTable && !localTable->useLtt) { record = rpb->rpb_record; @@ -11032,7 +11490,12 @@ const StmtNode* TruncateLocalTableNode::execute(thread_db* tdbb, Request* reques { const auto localTable = request->getStatement()->localTables[tableNumber]; - if (auto& recordBuffer = localTable->getImpure(tdbb, request, false)->recordBuffer) + if (localTable->useLtt) + { + localTable->reset(tdbb, request); + localTable->getRelation(tdbb, request); + } + else if (auto& recordBuffer = localTable->getImpure(tdbb, request, false)->recordBuffer) recordBuffer->reset(); request->req_operation = Request::req_return; @@ -11786,7 +12249,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 +12269,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..dc052b6a9d4 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,11 +403,22 @@ class DeclareLocalTableNode final : public TypedNode dsqlTable; + dsql_rel* dsqlRelation = nullptr; NestConst format; + Firebird::Array notNullFields; + mutable jrd_rel* relation = nullptr; USHORT tableNumber = 0; + bool useLtt = false; }; @@ -581,7 +594,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 +610,8 @@ 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; + bool localTableOuterDecl = false; }; @@ -1296,6 +1311,8 @@ class ModifyNode final : public TypedNode unsigned marks = 0; // see StmtNode::IUD_MARK_xxx USHORT dsqlRseFlags = 0; std::optional dsqlReturningLocalTableNumber; + std::optional localTableNumber; + bool localTableOuterDecl = false; }; 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..05197ab851e 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 @@ -484,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 @@ -506,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/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..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) { @@ -383,7 +384,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, &outerLocalTable)) + relation = localTable->dsqlRelation; + } + + if (!selNode && !tableValueFunctionNode && !cte && !procedure && !relation) { const auto resolvedObject = dsqlScratch->resolveRoutineOrRelation(name, (((procNode && procNode->inputSources)) ? @@ -448,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) { @@ -1802,10 +1811,24 @@ 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->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 + { + 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 +3057,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/include/firebird/impl/blr.h b/src/include/firebird/impl/blr.h index d2218377775..f6ea7abeb38 100644 --- a/src/include/firebird/impl/blr.h +++ b/src/include/firebird/impl/blr.h @@ -462,6 +462,7 @@ // subcodes of blr_dcl_local_table #define blr_dcl_local_table_format (unsigned char) 1 +#define blr_dcl_local_table_ltt (unsigned char) 2 #define blr_local_table_truncate (unsigned char) 219 #define blr_local_table_id (unsigned char) 220 @@ -469,6 +470,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/RecordBuffer.h b/src/jrd/RecordBuffer.h index 4cd7979faf0..7d5b6d9cfec 100644 --- a/src/jrd/RecordBuffer.h +++ b/src/jrd/RecordBuffer.h @@ -23,6 +23,7 @@ #ifndef JRD_RECORD_BUFFER_H #define JRD_RECORD_BUFFER_H +#include "firebird.h" #include "../common/classes/alloc.h" #include "../common/classes/auto.h" #include "../common/classes/File.h" diff --git a/src/jrd/RecordSourceNodes.cpp b/src/jrd/RecordSourceNodes.cpp index e89bb17506c..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); @@ -666,6 +667,8 @@ 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; @@ -677,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"; @@ -715,6 +719,9 @@ LocalTableSourceNode* LocalTableSourceNode::copy(thread_db* tdbb, NodeCopier& co copier.remap[stream] = newSource->stream; 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)); @@ -723,6 +730,8 @@ 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()) { @@ -759,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); } @@ -4525,7 +4534,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/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 TypedNodetdbb_temp_frame_id) + inst_id = tdbb->tdbb_temp_frame_id; + else // called without a local table execution frame, maybe from OPT or CMP + return &rel_pages_base; + } + else if (rel_flags & REL_temp_tran) { if (tran != 0 && tran != MAX_TRA_NUMBER) inst_id = tran; @@ -352,6 +359,9 @@ RelationPages* RelationPermanent::getPagesInternal(thread_db* tdbb, TraNumber tr newPages); #endif + if (rel_flags & REL_temp_frame) + return newPages; + // create indexes MemoryPool* pool = tdbb->getDefaultPool(); const bool poolCreated = !pool; @@ -423,13 +433,15 @@ RelationPages* RelationPermanent::getAttPages(thread_db* tdbb, RelationPages::In return pages; } -bool RelationPermanent::delPages(thread_db* tdbb, TraNumber tran, RelationPages* aPages) +bool RelationPermanent::delPages(thread_db* tdbb, RelationPages::InstanceId inst_id, RelationPages* aPages) { - RelationPages* pages = aPages ? aPages : getPages(tdbb, tran, false); + RelationPages* pages = aPages ? aPages : + (rel_flags & REL_temp_frame ? getAttPages(tdbb, inst_id) : getPages(tdbb, inst_id, false)); if (!pages || !pages->rel_instance_id) return false; - fb_assert(tran == 0 || tran == MAX_TRA_NUMBER || pages->rel_instance_id == tran); + fb_assert(inst_id == 0 || inst_id == MAX_TRA_NUMBER || pages->rel_instance_id == inst_id || + (rel_flags & REL_temp_frame)); fb_assert(pages->useCount > 0); diff --git a/src/jrd/Relation.h b/src/jrd/Relation.h index 4e1164c67ec..f0f90456653 100644 --- a/src/jrd/Relation.h +++ b/src/jrd/Relation.h @@ -720,6 +720,7 @@ inline constexpr ULONG REL_jrd_view = 0x0080; // relation is VIEW inline constexpr ULONG REL_temp_gtt = 0x0100; // relation is a GTT inline constexpr ULONG REL_temp_ltt = 0x0200; // relation is a LTT inline constexpr ULONG REL_private = 0x0400; // relation is private to its package +inline constexpr ULONG REL_temp_frame = 0x0800; // relation data is scoped to an execution frame class GCLock { @@ -908,7 +909,7 @@ class RelationPermanent : public Firebird::PermanentStorage RelationPages* getPages(thread_db* tdbb, TraNumber tran = MAX_TRA_NUMBER, bool allocPages = true); void fillPages(thread_db* tdbb); RelationPages* getAttPages(thread_db* tdbb, RelationPages::InstanceId inst_id); - bool delPages(thread_db* tdbb, TraNumber tran = MAX_TRA_NUMBER, RelationPages* aPages = NULL); + bool delPages(thread_db* tdbb, RelationPages::InstanceId inst_id = MAX_TRA_NUMBER, RelationPages* aPages = NULL); void freePages(thread_db* tdbb); void retainPages(thread_db* tdbb, TraNumber oldNumber, TraNumber newNumber); void cleanUp() noexcept; @@ -1141,7 +1142,7 @@ inline bool RelationPermanent::isSystem() const noexcept inline bool RelationPermanent::isTemporary() const noexcept { - return (rel_flags & (REL_temp_tran | REL_temp_conn)); + return (rel_flags & (REL_temp_tran | REL_temp_conn | REL_temp_frame)); } inline bool RelationPermanent::isVirtual() const noexcept diff --git a/src/jrd/Savepoint.cpp b/src/jrd/Savepoint.cpp index 714a9041e1c..ca12a83c08a 100644 --- a/src/jrd/Savepoint.cpp +++ b/src/jrd/Savepoint.cpp @@ -30,6 +30,7 @@ #include "../jrd/LocalTemporaryTable.h" #include "../jrd/Relation.h" #include "../dsql/metd_proto.h" +#include "../common/classes/auto.h" #include "Savepoint.h" @@ -85,6 +86,8 @@ void UndoItem::release(jrd_tra* transaction) void VerbAction::garbageCollectIdxLite(thread_db* tdbb, jrd_tra* transaction, SINT64 recordNumber, VerbAction* nextAction, Record* goingRecord) { + AutoSetRestore autoFrameId(&tdbb->tdbb_temp_frame_id, vct_temp_instance_id); + // Clean up index entries and referenced BLOBs. // This routine uses smaller set of staying record than original VIO_garbage_collect_idx(). // @@ -163,6 +166,8 @@ void VerbAction::garbageCollectIdxLite(thread_db* tdbb, jrd_tra* transaction, SI void VerbAction::mergeTo(thread_db* tdbb, jrd_tra* transaction, VerbAction* nextAction) { + AutoSetRestore autoFrameId(&tdbb->tdbb_temp_frame_id, vct_temp_instance_id); + // Post bitmap of modified records and undo data to the next savepoint. // // Notes: @@ -246,11 +251,13 @@ void VerbAction::mergeTo(thread_db* tdbb, jrd_tra* transaction, VerbAction* next } } - release(transaction); + release(tdbb, transaction); } void VerbAction::undo(thread_db* tdbb, jrd_tra* transaction, bool preserveLocks, VerbAction* preserveAction) { + AutoSetRestore autoFrameId(&tdbb->tdbb_temp_frame_id, vct_temp_instance_id); + // Undo changes recorded for this verb action. // After that, clear the verb action and prepare it for later reuse. @@ -359,11 +366,13 @@ void VerbAction::undo(thread_db* tdbb, jrd_tra* transaction, bool preserveLocks, delete rpb.rpb_record; } - release(transaction); + release(tdbb, transaction); } -void VerbAction::release(jrd_tra* transaction) +void VerbAction::release(thread_db* tdbb, jrd_tra* transaction) { + AutoSetRestore autoFrameId(&tdbb->tdbb_temp_frame_id, vct_temp_instance_id); + // Release resources used by this verb action RecordBitmap::reset(vct_records); @@ -385,11 +394,16 @@ void VerbAction::release(jrd_tra* transaction) // Savepoint implementation -VerbAction* Savepoint::createAction(jrd_rel* relation) +VerbAction* Savepoint::createAction(thread_db* tdbb, jrd_rel* relation, FB_UINT64 tempInstanceId) { // Create action for the given relation. If it already exists, just return. - VerbAction* action = getAction(relation); + if (!(relation->getPermanent()->rel_flags & REL_temp_frame)) + tempInstanceId = 0; + else if (!tempInstanceId) + tempInstanceId = tdbb->tdbb_temp_frame_id; + + VerbAction* action = getAction(relation, tempInstanceId); if (!action) { @@ -402,6 +416,7 @@ VerbAction* Savepoint::createAction(jrd_rel* relation) m_actions = action; action->vct_relation = relation; + action->vct_temp_instance_id = tempInstanceId; } return action; @@ -469,7 +484,7 @@ Savepoint* Savepoint::rollback(thread_db* tdbb, Savepoint* prior, bool preserveL VerbAction* preserveAction = nullptr; if (preserveLocks && m_next) - preserveAction = m_next->createAction(action->vct_relation); + preserveAction = m_next->createAction(tdbb, action->vct_relation, action->vct_temp_instance_id); action->undo(tdbb, m_transaction, preserveLocks, preserveAction); @@ -595,7 +610,7 @@ Savepoint* Savepoint::rollforward(thread_db* tdbb, Savepoint* prior) if (m_next) { - nextAction = m_next->getAction(action->vct_relation); + nextAction = m_next->getAction(action->vct_relation, action->vct_temp_instance_id); if (!nextAction) // next savepoint didn't touch this table yet - send whole action { diff --git a/src/jrd/Savepoint.h b/src/jrd/Savepoint.h index e5010921264..07f0c3ea6ee 100644 --- a/src/jrd/Savepoint.h +++ b/src/jrd/Savepoint.h @@ -81,7 +81,8 @@ namespace Jrd { public: VerbAction() - : vct_next(NULL), vct_records(NULL), vct_undo(NULL) + : vct_next(NULL), vct_relation(NULL), vct_records(NULL), vct_undo(NULL), + vct_temp_instance_id(0) {} ~VerbAction() @@ -94,6 +95,7 @@ namespace Jrd jrd_rel* vct_relation; // Relation involved RecordBitmap* vct_records; // Record involved UndoItemTree* vct_undo; // Data for undo records + FB_UINT64 vct_temp_instance_id; // Frame-scoped temporary relation instance void mergeTo(thread_db* tdbb, jrd_tra* transaction, VerbAction* nextAction); void undo(thread_db* tdbb, jrd_tra* transaction, bool preserveLocks, @@ -102,7 +104,7 @@ namespace Jrd VerbAction* nextAction, Record* goingRecord); private: - void release(jrd_tra* transaction); + void release(thread_db* tdbb, jrd_tra* transaction); }; // LTT undo item - stores original state of a LocalTemporaryTable for savepoint rollback @@ -180,13 +182,13 @@ namespace Jrd fb_assert(m_next != this); } - VerbAction* getAction(const jrd_rel* relation) const + VerbAction* getAction(const jrd_rel* relation, FB_UINT64 tempInstanceId = 0) const { // Find and return (if exists) action that belongs to the given relation for (VerbAction* action = m_actions; action; action = action->vct_next) { - if (action->vct_relation == relation) + if (action->vct_relation == relation && action->vct_temp_instance_id == tempInstanceId) return action; } @@ -260,7 +262,7 @@ namespace Jrd return next; } - VerbAction* createAction(jrd_rel* relation); + VerbAction* createAction(thread_db* tdbb, jrd_rel* relation, FB_UINT64 tempInstanceId = 0); void createLttAction(LttUndoItem::UndoType type, const QualifiedName& name, LocalTemporaryTable* original = nullptr); diff --git a/src/jrd/Statement.cpp b/src/jrd/Statement.cpp index 6b1741b15cc..28cd5028040 100644 --- a/src/jrd/Statement.cpp +++ b/src/jrd/Statement.cpp @@ -74,6 +74,7 @@ Statement::Statement(thread_db* tdbb, MemoryPool* p, CompilerScratch* csb) subStatements(*p), fors(*p), localTables(*p), + outerLocalTables(*p), invariants(*p), blr(*p), mapFieldInfo(*p), @@ -115,6 +116,17 @@ Statement::Statement(thread_db* tdbb, MemoryPool* p, CompilerScratch* csb) localTables = csb->csb_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] = true; + } + } + // 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; } @@ -773,6 +786,15 @@ void Statement::release(thread_db* tdbb) } } + for (FB_SIZE_T i = 0; i < localTables.getCount(); ++i) + { + if (i < outerLocalTables.getCount() && outerLocalTables[i]) + continue; + + if (const auto localTable = localTables[i]) + localTable->destroyRelation(tdbb); + } + sqlText = NULL; // ~Statement is never called :-( @@ -1036,6 +1058,50 @@ 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; +} + +jrd_tra* Request::getLocalTableTransaction() const +{ + Stack::const_iterator autoTran(req_auto_trans); + + if (autoTran.hasData()) + return autoTran.object().m_transaction; + + return req_transaction; +} + +FB_UINT64 Request::getLocalTableInstanceId(thread_db* tdbb) const +{ + if (!req_local_table_instance_id) + req_local_table_instance_id = tdbb->getDatabase()->generateStatementId(); + + return req_local_table_instance_id; +} + +bool Request::getLocalTableAutoTranCtx(AutoTranCtx& ctx) const +{ + Stack::const_iterator autoTran(req_auto_trans); + + if (autoTran.hasData()) + { + ctx = autoTran.object(); + return true; + } + + return false; +} + StmtNumber Request::getRequestId() const { if (!req_id) @@ -1050,6 +1116,8 @@ StmtNumber Request::getRequestId() const Request::Request(Firebird::AutoMemoryPool& pool, Database* dbb, /*const*/ Statement* aStatement) : statement(aStatement), + req_id(0), + req_local_table_instance_id(0), req_inUse(false), req_pool(pool), req_memory_stats(&aStatement->pool->getStatsGroup()), diff --git a/src/jrd/Statement.h b/src/jrd/Statement.h index 01e6faf5cd1..98ac296b517 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 e0d6146dd49..ec201544e1a 100644 --- a/src/jrd/exe.cpp +++ b/src/jrd/exe.cpp @@ -1244,15 +1244,6 @@ void EXE_unwind(thread_db* tdbb, Request* request) tdbb->setTransaction(old_transaction); } - for (auto localTable : statement->localTables) - { - if (!localTable) - continue; - - auto impure = localTable->getImpure(tdbb, request, false); - impure->recordBuffer->reset(); - } - release_blobs(tdbb, request); const auto attachment = request->req_attachment; @@ -1264,6 +1255,21 @@ void EXE_unwind(thread_db* tdbb, Request* request) } } + const Statement* statement = request->getStatement(); + + 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; + + localTable->reset(tdbb, request); + } + request->req_sorts.unlinkAll(); if (request->req_transaction) @@ -2079,4 +2085,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..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; @@ -637,6 +639,8 @@ 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 + 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/ods.h b/src/jrd/ods.h index a8a0f62b89d..4095453093f 100644 --- a/src/jrd/ods.h +++ b/src/jrd/ods.h @@ -193,10 +193,27 @@ inline constexpr USHORT USER_DEF_REL_INIT_ID = 128; // ODS >= 9 // Define range of user relation and LTT ids inline constexpr USHORT MIN_RELATION_ID = USER_DEF_REL_INIT_ID; inline constexpr USHORT MAX_RELATION_ID = 32767; -inline constexpr USHORT MAX_LTT_COUNT = 1024; -inline constexpr USHORT MAX_LTT_ID = MAX_USHORT; -inline constexpr USHORT MIN_LTT_ID = MAX_LTT_ID - MAX_LTT_COUNT + 1; + +// Local temporary table relation ids occupy the band above MAX_RELATION_ID, split +// into two disjoint sub-bands so the two id-allocation schemes can never overlap: +// - Created LTTs (CREATE LOCAL TEMPORARY TABLE) are attachment things; their ids are +// assigned by a per-attachment rotating allocator coordinated through the metadata cache. +// - Declared LTTs (PSQL DECLARE ... LOCAL TABLE) are statement things (the relation lives +// on the shared Statement); their id is deterministic: MIN_DECLARED_LTT_ID + tableNumber. +inline constexpr USHORT MAX_CREATED_LTT_COUNT = 1024; // created LTTs per attachment +inline constexpr USHORT MAX_CREATED_LTT_ID = MAX_USHORT; +inline constexpr USHORT MIN_CREATED_LTT_ID = MAX_CREATED_LTT_ID - MAX_CREATED_LTT_COUNT + 1; + +inline constexpr USHORT MAX_DECLARED_LTT_COUNT = 1024; // declared LTTs per statement +inline constexpr USHORT MAX_DECLARED_LTT_ID = MIN_CREATED_LTT_ID - 1; +inline constexpr USHORT MIN_DECLARED_LTT_ID = MAX_DECLARED_LTT_ID - MAX_DECLARED_LTT_COUNT + 1; + +// Full LTT union (used to classify an id as belonging to any LTT) +inline constexpr USHORT MIN_LTT_ID = MIN_DECLARED_LTT_ID; +inline constexpr USHORT MAX_LTT_ID = MAX_CREATED_LTT_ID; + static_assert(MIN_LTT_ID > MAX_RELATION_ID); +static_assert(MAX_DECLARED_LTT_ID < MIN_CREATED_LTT_ID); // Page types diff --git a/src/jrd/optimizer/Optimizer.cpp b/src/jrd/optimizer/Optimizer.cpp index acc1c986a26..4a6a62d9b73 100644 --- a/src/jrd/optimizer/Optimizer.cpp +++ b/src/jrd/optimizer/Optimizer.cpp @@ -644,6 +644,8 @@ Optimizer::Optimizer(thread_db* aTdbb, CompilerScratch* aCsb, RseNode* aRse, { if (csb->csb_rpt[stream].csb_relation) compileRelation(stream); + else if (csb->csb_rpt[stream].csb_local_table_number.has_value()) + compileLocalTable(stream); } } @@ -1349,6 +1351,51 @@ void Optimizer::compileRelation(StreamType stream) } +void Optimizer::compileLocalTable(StreamType stream) +{ + compileStreams.add(stream); + + const auto tail = &csb->csb_rpt[stream]; + tail->csb_cardinality = DEFAULT_CARDINALITY; + tail->csb_idx = nullptr; + + if (!tail->csb_local_table_number.has_value()) + return; + + const auto tableNumber = tail->csb_local_table_number.value(); + + if (tableNumber >= csb->csb_localTables.getCount() || !csb->csb_localTables[tableNumber]) + return; + + const bool needIndices = conjuncts.hasData() || (rse->rse_sorted || rse->rse_aggregate); + + if (!needIndices) + return; + + const auto relation = csb->csb_localTables[tableNumber]->getRelation(tdbb, nullptr)->getPermanent(); + const auto relPages = relation->getPages(tdbb); + IndexDescList idxList; + BTR_all(tdbb, relation, idxList, relPages, csb->csb_g_flags & csb_internal); + + MetaId n = idxList.getCount(); + while (n--) + { + auto id = idxList[n].idx_id; + auto* idv = relation->lookup_index(tdbb, id, CacheFlag::AUTOCREATE); + if (idv && idv->getActive() != MET_index_active) + idv = nullptr; + if (!idv) + idxList.remove(n); + } + + if (idxList.hasData()) + tail->csb_idx = FB_NEW_POOL(getPool()) IndexDescList(getPool(), idxList); + + if (tail->csb_plan) + markIndices(tail, relation->getId()); +} + + // // Decompose a boolean into a stack of conjuctions. // diff --git a/src/jrd/optimizer/Optimizer.h b/src/jrd/optimizer/Optimizer.h index baee13be759..aa68e2d71ec 100644 --- a/src/jrd/optimizer/Optimizer.h +++ b/src/jrd/optimizer/Optimizer.h @@ -508,6 +508,7 @@ class Optimizer final : public Firebird::PermanentStorage ~Optimizer(); RecordSource* compile(RseNode* subRse, BoolExprNodeStack* parentStack); + void compileLocalTable(StreamType stream); void compileRelation(StreamType stream); unsigned decomposeBoolean(BoolExprNode* boolNode, BoolExprNodeStack& stack); void generateAggregateDistincts(MapNode* map); diff --git a/src/jrd/recsrc/LocalTableStream.cpp b/src/jrd/recsrc/LocalTableStream.cpp index 5b72e5111cc..6084b0f6a9b 100644 --- a/src/jrd/recsrc/LocalTableStream.cpp +++ b/src/jrd/recsrc/LocalTableStream.cpp @@ -26,6 +26,10 @@ #include "../jrd/req.h" #include "../dsql/StmtNodes.h" #include "../jrd/optimizer/Optimizer.h" +#include "../jrd/dpm_proto.h" +#include "../jrd/rlck_proto.h" +#include "../jrd/vio_proto.h" +#include "../common/classes/auto.h" #include "RecordSource.h" @@ -36,9 +40,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); @@ -56,7 +62,31 @@ void LocalTableStream::internalOpen(thread_db* tdbb) const const auto rpb = &request->req_rpb[m_stream]; rpb->getWindow(tdbb).win_flags = 0; + const auto localTableRequest = request->getLocalTableRequest(m_outerDecl); + + if (m_table->useLtt) + { + AutoSetRestore autoFrameId( + &tdbb->tdbb_temp_frame_id, localTableRequest->getLocalTableInstanceId(tdbb)); + + rpb->rpb_relation = m_table->getRelation(tdbb, localTableRequest); + } + rpb->rpb_number.setValue(BOF_NUMBER); + + // Inside an autonomous transaction block, reading a DLTT uses the parent transaction. + // The parent's top savepoint already holds a verb action for this relation (from the + // INSERT that made the data visible), so get_undo_data returns udForceBack, hiding + // all freshly-inserted rows. Bypass cursor stability for this case only — non-autonomous + // reads must keep cursor stability so that INSERT...SELECT from the same DLTT does not + // recurse into rows inserted by the current statement. + if (m_table->useLtt) + { + if (localTableRequest->req_auto_trans.hasData()) + rpb->rpb_stream_flags |= RPB_s_unstable; + else + rpb->rpb_stream_flags &= ~RPB_s_unstable; + } } void LocalTableStream::close(thread_db* tdbb) const @@ -85,18 +115,52 @@ bool LocalTableStream::internalGetRecord(thread_db* tdbb) const return false; } - if (!rpb->rpb_record) - rpb->rpb_record = FB_NEW_POOL(*tdbb->getDefaultPool()) Record(*tdbb->getDefaultPool(), m_format); + if (!m_table->useLtt) + { + if (!rpb->rpb_record) + rpb->rpb_record = FB_NEW_POOL(*tdbb->getDefaultPool()) Record(*tdbb->getDefaultPool(), m_format); + + const auto localTableRequest = request->getLocalTableRequest(m_outerDecl); + const auto recordBuffer = m_table->getImpure(tdbb, localTableRequest)->recordBuffer; + + while (true) + { + 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; + } + + const auto localTableRequest = request->getLocalTableRequest(m_outerDecl); + const auto transaction = localTableRequest->getLocalTableTransaction(); + AutoSetRestore autoFrameId( + &tdbb->tdbb_temp_frame_id, localTableRequest->getLocalTableInstanceId(tdbb)); + + AutoSetRestore2 autoTransaction( + tdbb, &thread_db::getTransaction, &thread_db::setTransaction, transaction); - rpb->rpb_number.increment(); + const bool found = VIO_next_record(tdbb, rpb, transaction, request->req_pool, DPM_next_all, nullptr); - if (!m_table->getImpure(tdbb, request)->recordBuffer->fetch(rpb->rpb_number.getValue(), rpb->rpb_record)) + if (found) { - rpb->rpb_number.setValid(false); - return false; + rpb->rpb_number.setValid(true); + return true; } - return true; + rpb->rpb_number.setValid(false); + return false; } bool LocalTableStream::refetchRecord(thread_db* tdbb) const 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..ed098812c79 100644 --- a/src/jrd/req.h +++ b/src/jrd/req.h @@ -313,6 +313,7 @@ class Request : public pool_alloc mutable ISC_TIME localTime; // gmtTimeStamp converted to local time (WITH TZ) }; +public: // Fields to support read consistency in READ COMMITTED transactions struct SnapshotData @@ -368,6 +369,11 @@ class Request : public pool_alloc return statement; } + Request* getLocalTableRequest(bool outerDecl); + jrd_tra* getLocalTableTransaction() const; + FB_UINT64 getLocalTableInstanceId(thread_db* tdbb) const; + bool getLocalTableAutoTranCtx(AutoTranCtx& ctx) const; + bool hasInternalStatement() const noexcept; bool hasPowerfulStatement() const noexcept; @@ -397,6 +403,7 @@ class Request : public pool_alloc private: Statement* const statement; mutable StmtNumber req_id; // request identifier + mutable FB_UINT64 req_local_table_instance_id; // declared local table page instance identifier TimeStampCache req_timeStampCache; // time stamp cache std::atomic req_inUse; diff --git a/src/jrd/tdbb.h b/src/jrd/tdbb.h index 73d4dfd9b27..6c9d71faa30 100644 --- a/src/jrd/tdbb.h +++ b/src/jrd/tdbb.h @@ -218,6 +218,7 @@ class thread_db final : public Firebird::ThreadData tdbb_quantum(QUANTUM), tdbb_flags(0), tdbb_temp_traid(0), + tdbb_temp_frame_id(0), tdbb_bdbs(*getDefaultMemoryPool()), tdbb_thread(Firebird::ThreadSync::getThread("thread_db")) { @@ -242,6 +243,7 @@ class thread_db final : public Firebird::ThreadData ULONG tdbb_flags; TraNumber tdbb_temp_traid; // current temporary table scope + FB_UINT64 tdbb_temp_frame_id; // current frame-scoped temporary table scope // BDB's held by thread Firebird::HalfStaticArray tdbb_bdbs; diff --git a/src/jrd/tra.cpp b/src/jrd/tra.cpp index b1b3fff9eec..1d06d0e0b7e 100644 --- a/src/jrd/tra.cpp +++ b/src/jrd/tra.cpp @@ -3973,10 +3973,12 @@ Record* jrd_tra::findNextUndo(VerbAction* stopAction, jrd_rel* relation, SINT64 **************************************/ { UndoItem* result = NULL; + const auto tempInstanceId = (relation->getPermanent()->rel_flags & REL_temp_frame) ? + stopAction->vct_temp_instance_id : 0; for (Savepoint::Iterator iter(tra_save_point); *iter; ++iter) { - VerbAction* const action = (*iter)->getAction(relation); + VerbAction* const action = (*iter)->getAction(relation, tempInstanceId); if (action == stopAction) return result ? result->setupRecord(this) : NULL; @@ -3989,7 +3991,7 @@ Record* jrd_tra::findNextUndo(VerbAction* stopAction, jrd_rel* relation, SINT64 return NULL; } -void jrd_tra::listStayingUndo(jrd_rel* relation, SINT64 number, RecordStack &staying) +void jrd_tra::listStayingUndo(thread_db* tdbb, jrd_rel* relation, SINT64 number, RecordStack &staying) /************************************** * * l i s t S t a y i n g U n d o @@ -4002,9 +4004,12 @@ void jrd_tra::listStayingUndo(jrd_rel* relation, SINT64 number, RecordStack &sta * **************************************/ { + const auto tempInstanceId = (relation->getPermanent()->rel_flags & REL_temp_frame) ? + tdbb->tdbb_temp_frame_id : 0; + for (Savepoint::Iterator iter(tra_save_point); *iter; ++iter) { - VerbAction* const action = (*iter)->getAction(relation); + VerbAction* const action = (*iter)->getAction(relation, tempInstanceId); if (action && action->vct_undo && action->vct_undo->locate(number)) staying.push(action->vct_undo->current().setupRecord(this)); @@ -4435,4 +4440,3 @@ void jrd_tra::eraseSecDbContext() noexcept delete tra_sec_db_context; tra_sec_db_context = NULL; } - diff --git a/src/jrd/tra.h b/src/jrd/tra.h index 82d41858208..aba060d19f7 100644 --- a/src/jrd/tra.h +++ b/src/jrd/tra.h @@ -403,7 +403,7 @@ class jrd_tra final : public pool_alloc void eraseSecDbContext() noexcept; MappingList* getMappingList(); Record* findNextUndo(VerbAction* before_this, jrd_rel* relation, SINT64 number); - void listStayingUndo(jrd_rel* relation, SINT64 number, RecordStack &staying); + void listStayingUndo(thread_db* tdbb, jrd_rel* relation, SINT64 number, RecordStack &staying); Savepoint* startSavepoint(bool root = false); void rollbackSavepoint(thread_db* tdbb, bool preserveLocks = false); void rollbackToSavepoint(thread_db* tdbb, SavNumber number); diff --git a/src/jrd/vio.cpp b/src/jrd/vio.cpp index b6f25675eb6..b89ef4a65c1 100644 --- a/src/jrd/vio.cpp +++ b/src/jrd/vio.cpp @@ -5682,7 +5682,7 @@ void VIO_garbage_collect_idx(thread_db* tdbb, jrd_tra* transaction, RecordStack going, staying; list_staying(tdbb, org_rpb, staying); // Add not-so-old versions from undo log for transaction - transaction->listStayingUndo(org_rpb->rpb_relation, org_rpb->rpb_number.getValue(), staying); + transaction->listStayingUndo(tdbb, org_rpb->rpb_relation, org_rpb->rpb_number.getValue(), staying); // The data that is going is passed via old_data. It is up to caller to make sure that it isn't in one of two lists above @@ -6101,7 +6101,9 @@ static UndoDataRet get_undo_data(thread_db* tdbb, jrd_tra* transaction, if (!transaction->tra_save_point) return udNone; - VerbAction* const action = transaction->tra_save_point->getAction(rpb->rpb_relation); + const auto tempInstanceId = (rpb->rpb_relation->getPermanent()->rel_flags & REL_temp_frame) ? + tdbb->tdbb_temp_frame_id : 0; + VerbAction* const action = transaction->tra_save_point->getAction(rpb->rpb_relation, tempInstanceId); if (action) { @@ -7516,7 +7518,7 @@ static void verb_post(thread_db* tdbb, **************************************/ SET_TDBB(tdbb); - VerbAction* const action = transaction->tra_save_point->createAction(rpb->rpb_relation); + VerbAction* const action = transaction->tra_save_point->createAction(tdbb, rpb->rpb_relation); if (!RecordBitmap::test(action->vct_records, rpb->rpb_number.getValue())) { diff --git a/src/yvalve/gds.cpp b/src/yvalve/gds.cpp index d53494c7fcb..8fc95348103 100644 --- a/src/yvalve/gds.cpp +++ b/src/yvalve/gds.cpp @@ -4033,7 +4033,8 @@ static void blr_print_verb(gds_ctl* control, SSHORT level) static const char* subCodes[] = { nullptr, - "format" + "format", + "ltt" }; while ((blr_operator = control->ctl_blr_reader.getByte()) != blr_end) @@ -4047,6 +4048,10 @@ static void blr_print_verb(gds_ctl* control, SSHORT level) switch (blr_operator) { + case blr_dcl_local_table_ltt: + offset = blr_print_line(control, offset); + break; + case blr_dcl_local_table_format: n = blr_print_word(control); offset = blr_print_line(control, offset); @@ -4081,7 +4086,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 +4103,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);