diff --git a/doc/sql.extensions/README.declared_local_temporary_tables.md b/doc/sql.extensions/README.declared_local_temporary_tables.md index e855af29440..1ec93f42184 100644 --- a/doc/sql.extensions/README.declared_local_temporary_tables.md +++ b/doc/sql.extensions/README.declared_local_temporary_tables.md @@ -20,7 +20,8 @@ Declared Local Temporary Tables are declared in the same declaration section as DECLARE [LOCAL] TEMPORARY TABLE ( [, ...] -); +) +[[UNIQUE] [ASC | DESC] INDEX ( [, ...])]...; ``` There is no `ON COMMIT` clause. @@ -35,7 +36,8 @@ as declare local temporary table t ( id integer not null, val varchar(20) - ); + ) + index idx_t_id (id); begin insert into t(id, val) values (1, 'a'); insert into t(id, val) values (2, 'b'); @@ -59,7 +61,8 @@ create procedure p_count_values returns (n integer) as declare local temporary table t ( id integer not null - ); + ) + unique index uq_t_id (id); begin insert into t(id) values (1); insert into t(id) values (2); @@ -214,6 +217,17 @@ delete from t where ...; They can be used in subqueries, joins and cursor loops like other record sources, subject to the restrictions below. +Indexes can be declared inline as part of the table declaration: + +```sql +declare local temporary table t ( + id integer not null, + val varchar(20) +) + index idx_t_id (id) + descending index idx_t_val_desc (val); +``` + ## Restrictions Declared Local Temporary Tables intentionally support a small table definition surface. @@ -237,7 +251,7 @@ Constraint restrictions: Other restrictions: - A single PSQL statement, procedure, function or trigger may declare at most 1024 local temporary tables. -- Indexes are not supported. +- Expression-based indexes and partial 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 diff --git a/src/dsql/DdlNodes.epp b/src/dsql/DdlNodes.epp index 92c59348124..42d47d7f1bf 100644 --- a/src/dsql/DdlNodes.epp +++ b/src/dsql/DdlNodes.epp @@ -7076,17 +7076,17 @@ DdlNode* RelationNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) case Clause::TYPE_ALTER_PUBLICATION: break; - case Clause::TYPE_ADD_PACKAGED_TABLE_INDEX: + case Clause::TYPE_ADD_INLINE_TABLE_INDEX: { - auto addPackagedTableIndexClause = static_cast(clause.getObject()); + auto addInlineTableIndexClause = static_cast(clause.getObject()); - if (!addPackagedTableIndexClause->indexNode->relation) + if (!addInlineTableIndexClause->indexNode->relation) { - addPackagedTableIndexClause->indexNode->relation = FB_NEW_POOL(dsqlScratch->getPool()) + addInlineTableIndexClause->indexNode->relation = FB_NEW_POOL(dsqlScratch->getPool()) RelationSourceNode(dsqlScratch->getPool(), name); } - addPackagedTableIndexClause->indexNode->dsqlPass(dsqlScratch); + addInlineTableIndexClause->indexNode->dsqlPass(dsqlScratch); break; } @@ -7335,7 +7335,7 @@ void RelationNode::validateLttClauses(const Array>& clauses) "Only NOT NULL constraints without names are supported on LOCAL TEMPORARY TABLEs"); break; - case Clause::TYPE_ADD_PACKAGED_TABLE_INDEX: + case Clause::TYPE_ADD_INLINE_TABLE_INDEX: break; default: @@ -9864,14 +9864,14 @@ void CreateRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScrat static_cast(i->getObject()), constraints); break; - case Clause::TYPE_ADD_PACKAGED_TABLE_INDEX: + case Clause::TYPE_ADD_INLINE_TABLE_INDEX: { - auto addPackagedTableIndexClause = static_cast(i->getObject()); - const auto indexNode = addPackagedTableIndexClause->indexNode; + auto addInlineTableIndexClause = static_cast(i->getObject()); + const auto indexNode = addInlineTableIndexClause->indexNode; if (!indexNode->relation) { - addPackagedTableIndexClause->indexNode->relation = FB_NEW_POOL(dsqlScratch->getPool()) + addInlineTableIndexClause->indexNode->relation = FB_NEW_POOL(dsqlScratch->getPool()) RelationSourceNode(dsqlScratch->getPool(), name); } @@ -9893,7 +9893,7 @@ void CreateRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScrat } indexList.push(CreateIndexNode::store(tdbb, indexList.getPool(), transaction, - addPackagedTableIndexClause->indexNode->name, definition)); + addInlineTableIndexClause->indexNode->name, definition)); break; } diff --git a/src/dsql/DdlNodes.h b/src/dsql/DdlNodes.h index fdcbf5ec827..2368d1b3405 100644 --- a/src/dsql/DdlNodes.h +++ b/src/dsql/DdlNodes.h @@ -1459,7 +1459,7 @@ class RelationNode : public DdlNode TYPE_DROP_CONSTRAINT, TYPE_ALTER_SQL_SECURITY, TYPE_ALTER_PUBLICATION, - TYPE_ADD_PACKAGED_TABLE_INDEX + TYPE_ADD_INLINE_TABLE_INDEX }; explicit Clause(MemoryPool& p, Type aType) noexcept @@ -1487,10 +1487,10 @@ class RelationNode : public DdlNode unsigned deleteAction; }; - struct AddPackagedTableIndexClause : public Clause + struct AddInlineTableIndexClause : public Clause { - explicit AddPackagedTableIndexClause(MemoryPool& p, CreateIndexNode* aIndexNode) - : Clause(p, TYPE_ADD_PACKAGED_TABLE_INDEX), + explicit AddInlineTableIndexClause(MemoryPool& p, CreateIndexNode* aIndexNode) + : Clause(p, TYPE_ADD_INLINE_TABLE_INDEX), indexNode(aIndexNode) { } diff --git a/src/dsql/StmtNodes.cpp b/src/dsql/StmtNodes.cpp index b44aa50dac5..fdc908cd55b 100644 --- a/src/dsql/StmtNodes.cpp +++ b/src/dsql/StmtNodes.cpp @@ -40,12 +40,15 @@ #include "../jrd/RecordBuffer.h" #include "../jrd/RecordSourceNodes.h" #include "../jrd/VirtualTable.h" +#include "../jrd/btr.h" +#include "../jrd/sort.h" #include "../jrd/extds/ExtDS.h" #include "../jrd/recsrc/RecordSource.h" #include "../jrd/recsrc/Cursor.h" #include "../jrd/replication/Publisher.h" #include "../jrd/trace/TraceManager.h" #include "../jrd/trace/TraceJrdHelpers.h" +#include "../jrd/btr_proto.h" #include "../jrd/cmp_proto.h" #include "../jrd/dfw_proto.h" #include "../jrd/dpm_proto.h" @@ -1784,8 +1787,13 @@ DmlNode* DeclareLocalTableNode::parse(thread_db* tdbb, MemoryPool& pool, Compile const auto node = FB_NEW_POOL(pool) DeclareLocalTableNode(pool); node->tableNumber = blrReader.getWord(); + if (node->tableNumber >= MAX_DECLARED_LTT_COUNT) + PAR_error(csb, Arg::Gds(isc_random) << "Invalid local table number"); + csb->csb_localTables.grow(node->tableNumber + 1); - fb_assert(!csb->csb_localTables[node->tableNumber]); + if (csb->csb_localTables[node->tableNumber]) + PAR_error(csb, Arg::Gds(isc_random) << "Duplicate local table number"); + csb->csb_localTables[node->tableNumber] = node; USHORT fieldCount = 0; @@ -1795,9 +1803,35 @@ DmlNode* DeclareLocalTableNode::parse(thread_db* tdbb, MemoryPool& pool, Compile switch (verb) { case blr_dcl_local_table_ltt: + if (node->useLtt) + PAR_error(csb, Arg::Gds(isc_random) << "Duplicate local table ltt sub code"); + node->useLtt = true; break; + case blr_dcl_local_table_index: + { + auto& index = node->indexes.add(); + blrReader.getMetaName(index.name); + const auto flags = blrReader.getByte(); + + if (flags & ~(blr_dcl_local_table_index_unique | blr_dcl_local_table_index_descending)) + PAR_error(csb, Arg::Gds(isc_random) << "Invalid local table index flags"); + + index.unique = flags & blr_dcl_local_table_index_unique; + index.descending = flags & blr_dcl_local_table_index_descending; + + const auto segmentCount = blrReader.getByte(); + + if (segmentCount == 0 || segmentCount > MAX_INDEX_SEGMENTS) + PAR_error(csb, Arg::Gds(isc_random) << "Invalid local table index segment count"); + + for (USHORT i = 0; i < segmentCount; ++i) + index.fieldIds.add(blrReader.getWord()); + + break; + } + case blr_dcl_local_table_format: if (node->format) PAR_error(csb, Arg::Gds(isc_random) << "duplicate local table format"); @@ -1851,6 +1885,34 @@ DmlNode* DeclareLocalTableNode::parse(thread_db* tdbb, MemoryPool& pool, Compile if (fieldCount == 0) PAR_error(csb, Arg::Gds(isc_random) << "Local table without fields"); + if (node->indexes.hasData()) + { + if (!node->useLtt) + PAR_error(csb, Arg::Gds(isc_random) << "Indexes are not supported for non-LTT local tables"); + + SortedArray indexNames; + + for (const auto& index : node->indexes) + { + if (index.name.isEmpty()) + PAR_error(csb, Arg::Gds(isc_random) << "Local table index without a name"); + + if (indexNames.exist(index.name)) + PAR_error(csb, Arg::Gds(isc_random) << "Duplicate local table index name"); + + indexNames.add(index.name); + + for (const auto fieldId : index.fieldIds) + { + if (fieldId >= fieldCount) + PAR_error(csb, Arg::Gds(isc_random) << "Invalid local table index field id"); + + if (node->format->fmt_desc[fieldId].dsc_dtype == dtype_blob) + PAR_error(csb, Arg::Gds(isc_random) << "BLOB fields cannot be indexed in local tables"); + } + } + } + return node; } @@ -1899,11 +1961,7 @@ DeclareLocalTableNode* DeclareLocalTableNode::dsqlPass(DsqlCompilerScratch* dsql 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"); - } + continue; const auto addColumn = static_cast(clause.getObject()); const auto field = addColumn->field; @@ -1975,6 +2033,83 @@ DeclareLocalTableNode* DeclareLocalTableNode::dsqlPass(DsqlCompilerScratch* dsql notNullFields.add(notNull); } + for (const auto clause : dsqlTable->clauses) + { + switch (clause->type) + { + case RelationNode::Clause::TYPE_ADD_COLUMN: + break; + + case RelationNode::Clause::TYPE_ADD_INLINE_TABLE_INDEX: + { + const auto addIndex = static_cast(clause.getObject()); + const auto indexNode = addIndex->indexNode; + + if (indexNode->name.schema.hasData() || indexNode->name.package.hasData()) + { + status_exception::raise( + Arg::Gds(isc_random) << + "Local temporary table index declarations cannot use qualified names"); + } + + for (const auto& index : indexes) + { + if (index.name == indexNode->name.object) + ERRD_post(Arg::Gds(isc_no_dup) << indexNode->name.toQuotedString()); + } + + fb_assert(indexNode->columns); + + if (indexNode->columns->items.getCount() == 0 || + indexNode->columns->items.getCount() > MAX_INDEX_SEGMENTS) + { + status_exception::raise(Arg::Gds(isc_idx_key_err) << indexNode->name.toQuotedString()); + } + + auto& index = indexes.add(); + index.name = indexNode->name.object; + index.unique = indexNode->unique; + index.descending = indexNode->descending; + + for (const auto indexColumn : indexNode->columns->items) + { + const auto fieldName = nodeAs(indexColumn)->dsqlName; + const dsql_fld* field = nullptr; + + for (auto existing = dsqlRelation->rel_fields; existing; existing = existing->fld_next) + { + if (existing->fld_name == fieldName) + { + field = existing; + break; + } + } + + if (!field) + { + status_exception::raise( + Arg::Gds(isc_dyn_column_does_not_exist) << fieldName.c_str() << dsqlName.toQuotedString()); + } + + if (field->dtype == dtype_blob) + status_exception::raise(Arg::Gds(isc_blob_idx_err) << fieldName.c_str()); + + if (std::find(index.fieldIds.begin(), index.fieldIds.end(), field->fld_id) != index.fieldIds.end()) + status_exception::raise(Arg::Gds(isc_key_field_err) << indexNode->name.toQuotedString()); + + index.fieldIds.add(field->fld_id); + } + + break; + } + + default: + status_exception::raise( + Arg::Gds(isc_random) << + "Table constraints are not supported for local temporary table declarations"); + } + } + return this; } @@ -2018,6 +2153,26 @@ void DeclareLocalTableNode::genBlr(DsqlCompilerScratch* dsqlScratch) for (auto field = dsqlRelation->rel_fields; field; field = field->fld_next) dsqlScratch->appendNullString(field->fld_name.c_str()); + for (const auto& index : indexes) + { + dsqlScratch->appendUChar(blr_dcl_local_table_index); + dsqlScratch->appendNullString(index.name.c_str()); + + UCHAR flags = 0; + + if (index.unique) + flags |= blr_dcl_local_table_index_unique; + + if (index.descending) + flags |= blr_dcl_local_table_index_descending; + + dsqlScratch->appendUChar(flags); + dsqlScratch->appendUChar(index.fieldIds.getCount()); + + for (const auto fieldId : index.fieldIds) + dsqlScratch->appendUShort(fieldId); + } + dsqlScratch->appendUChar(blr_end); } } @@ -2028,6 +2183,7 @@ DeclareLocalTableNode* DeclareLocalTableNode::copy(thread_db* tdbb, NodeCopier& node->format = format; node->notNullFields = notNullFields; node->fieldNames = fieldNames; + node->indexes = indexes; node->tableNumber = tableNumber; node->useLtt = useLtt; return node; @@ -2051,6 +2207,7 @@ const StmtNode* DeclareLocalTableNode::execute(thread_db* tdbb, Request* request { reset(tdbb, request); getRelation(tdbb, request); + createFrameIndexes(tdbb, request); } else if (auto& recordBuffer = getImpure(tdbb, request, false)->recordBuffer) recordBuffer->reset(); @@ -2150,11 +2307,121 @@ jrd_rel* DeclareLocalTableNode::getRelation(thread_db* tdbb, Request* request) c permanent->addFormat(relFormat); newRelation->rel_current_format = relFormat; + permanent->storeObject(tdbb, newRelation, 0); + + for (FB_SIZE_T i = 0; i < indexes.getCount(); ++i) + { + const auto& index = indexes[i]; + auto* idp = permanent->rel_indices.ensurePermanent(tdbb, i); + AutoPtr idv(FB_NEW_POOL(pool) IndexVersion(pool, idp)); + idv->setLtt(tdbb, QualifiedName(index.name), index.unique, index.descending, index.fieldIds.getCount()); + idp->storeObject(tdbb, idv, 0); + idv.release(); + } + relation = newRelation; return relation; } +void DeclareLocalTableNode::getIndexDescription(thread_db* tdbb, FB_SIZE_T indexId, index_desc* idx) const +{ + fb_assert(idx); + fb_assert(indexId < indexes.getCount()); + + memset(idx, 0, sizeof(*idx)); + + const auto& index = indexes[indexId]; + idx->idx_id = indexId; + idx->idx_count = index.fieldIds.getCount(); + + if (index.unique) + idx->idx_flags |= idx_unique; + + if (index.descending) + idx->idx_flags |= idx_descending; + + for (FB_SIZE_T i = 0; i < index.fieldIds.getCount(); ++i) + { + const auto fieldId = index.fieldIds[i]; + const auto& desc = format->fmt_desc[fieldId]; + + idx->idx_rpt[i].idx_field = fieldId; + idx->idx_rpt[i].idx_itype = DFW_assign_index_type(tdbb, QualifiedName(index.name), desc.dsc_dtype, + desc.isText() ? desc.getTextType() : ttype_none); + } +} + +void DeclareLocalTableNode::createFrameIndexes(thread_db* tdbb, Request* request) const +{ + if (indexes.isEmpty()) + return; + + fb_assert(relation); + + const auto frameId = request->getLocalTableInstanceId(tdbb); + AutoSetRestore autoFrameId(&tdbb->tdbb_temp_frame_id, frameId); + relation->getPages(tdbb); + + const auto transaction = request->req_transaction ? + request->req_transaction : tdbb->getAttachment()->getSysTransaction(); + + for (FB_SIZE_T indexId = 0; indexId < indexes.getCount(); ++indexId) + createFrameIndex(tdbb, indexId, transaction); +} + +void DeclareLocalTableNode::createFrameIndex(thread_db* tdbb, FB_SIZE_T indexId, jrd_tra* transaction) const +{ + // Frame relations are statement-owned and are not visible through the metadata cache used by + // IDX_create_index's worker attachment, so create their empty indexes synchronously here. + index_desc idx; + getIndexDescription(tdbb, indexId, &idx); + + const auto& index = indexes[indexId]; + const auto& indexName = index.name; + SelectivityList selectivity(*tdbb->getDefaultPool()); + + const auto dbb = tdbb->getDatabase(); + const auto nullIndLen = !(idx.idx_flags & idx_descending) && idx.idx_count == 1 ? 1 : 0; + const auto keyLength = ROUNDUP(BTR_key_length(tdbb, relation, &idx) + nullIndLen, sizeof(SINT64)); + + if (keyLength >= dbb->getMaxIndexKeyLength()) + ERR_post(Arg::Gds(isc_no_meta_update) << Arg::Gds(isc_keytoobig) << indexName.toQuotedString()); + + sort_key_def keyDesc[2]; + keyDesc[0].setSkdLength(SKD_bytes, keyLength); + keyDesc[0].skd_flags = SKD_ascending; + keyDesc[0].setSkdOffset(); + keyDesc[0].skd_vary_offset = 0; + keyDesc[1].setSkdLength(SKD_int64, sizeof(RecordNumber)); + keyDesc[1].skd_flags = SKD_ascending; + keyDesc[1].setSkdOffset(keyDesc); + keyDesc[1].skd_vary_offset = 0; + + PartitionedSort sort(dbb, &transaction->tra_sorts); + + IndexCreation creation; + creation.index = &idx; + creation.index_name = QualifiedName(indexName); + creation.relation = relation; + creation.transaction = transaction; + creation.sort = &sort; + creation.key_desc = keyDesc; + creation.key_length = keyLength; + creation.nullIndLen = nullIndLen; + creation.dup_recno = -1; + creation.duplicates.setValue(0); + creation.createMethod = IdxCreate::AtOnce; + + BTR_reserve_slot(tdbb, creation); + + const auto scb = FB_NEW_POOL(transaction->tra_sorts.getPool()) + Sort(dbb, &transaction->tra_sorts, keyLength + sizeof(index_sort_record), 2, 1, keyDesc, nullptr, nullptr); + sort.addPartition(scb); + sort.buildMergeTree(); + BTR_create(tdbb, creation, selectivity); +} + void DeclareLocalTableNode::reset(thread_db* tdbb, Request* request) const { if (relation) @@ -2201,7 +2468,6 @@ void DeclareLocalTableNode::destroyRelation(thread_db* tdbb) const { const auto permanent = relation->getPermanent(); permanent->freePages(tdbb); - jrd_rel::destroy(tdbb, relation); Cached::Relation::cleanup(tdbb, permanent); relation = nullptr; } @@ -11533,6 +11799,9 @@ DmlNode* TruncateLocalTableNode::parse(thread_db* tdbb, MemoryPool& pool, Compil if (node->tableNumber >= csb->csb_localTables.getCount() || !csb->csb_localTables[node->tableNumber]) PAR_error(csb, Arg::Gds(isc_bad_loctab_num) << Arg::Num(node->tableNumber)); + if (csb->csb_localTables[node->tableNumber]->useLtt) + PAR_error(csb, Arg::Gds(isc_random) << "TRUNCATE is not supported for declared local temporary tables"); + return node; } @@ -11564,12 +11833,9 @@ const StmtNode* TruncateLocalTableNode::execute(thread_db* tdbb, Request* reques { const auto localTable = request->getStatement()->localTables[tableNumber]; - if (localTable->useLtt) - { - localTable->reset(tdbb, request); - localTable->getRelation(tdbb, request); - } - else if (auto& recordBuffer = localTable->getImpure(tdbb, request, false)->recordBuffer) + fb_assert(!localTable->useLtt); + + if (auto& recordBuffer = localTable->getImpure(tdbb, request, false)->recordBuffer) recordBuffer->reset(); request->req_operation = Request::req_return; diff --git a/src/dsql/StmtNodes.h b/src/dsql/StmtNodes.h index a9e310c3bb9..ab6bf03221d 100644 --- a/src/dsql/StmtNodes.h +++ b/src/dsql/StmtNodes.h @@ -44,6 +44,7 @@ class RecordBuffer; class RelationSourceNode; class SelectNode; class GeneratorItem; +struct index_desc; class ExceptionItem final : public Firebird::PermanentStorage, public Printable @@ -373,6 +374,28 @@ class DeclareCursorNode final : public TypedNode { public: + struct Index + { + explicit Index(MemoryPool& pool) + : name(pool), + fieldIds(pool) + { + } + + Index(MemoryPool& pool, const Index& other) + : name(pool, other.name), + fieldIds(pool, other.fieldIds), + unique(other.unique), + descending(other.descending) + { + } + + MetaName name; + Firebird::Array fieldIds; + bool unique = false; + bool descending = false; + }; + struct Impure { RecordBuffer* recordBuffer; @@ -383,7 +406,8 @@ class DeclareLocalTableNode final : public TypedNode(pool), dsqlName(pool), notNullFields(pool), - fieldNames(pool) + fieldNames(pool), + indexes(pool) { } @@ -408,6 +432,9 @@ class DeclareLocalTableNode final : public TypedNode format; Firebird::Array notNullFields; Firebird::Array fieldNames; + Firebird::ObjectsArray indexes; mutable jrd_rel* relation = nullptr; USHORT tableNumber = 0; bool useLtt = false; diff --git a/src/dsql/parse.y b/src/dsql/parse.y index a8e8c16cb96..11ff5aaafc8 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -2589,26 +2589,26 @@ packaged_table_clause $$ = newNode($1); $$->tempFlag = REL_temp_ltt; } - '(' table_elements($2) ')' [YYVALID;] ltt_subclause_opt($2) packaged_table_indexes_opt($2) + '(' table_elements($2) ')' [YYVALID;] ltt_subclause_opt($2) inline_table_indexes_opt($2) { $$ = $2; } ; -%type packaged_table_indexes_opt() -packaged_table_indexes_opt($createRelationNode) +%type inline_table_indexes_opt() +inline_table_indexes_opt($createRelationNode) : /* nothing */ - | packaged_table_indexes($createRelationNode) + | inline_table_indexes($createRelationNode) ; -%type packaged_table_indexes() -packaged_table_indexes($createRelationNode) - : packaged_table_index($createRelationNode) - | packaged_table_indexes packaged_table_index($createRelationNode) +%type inline_table_indexes() +inline_table_indexes($createRelationNode) + : inline_table_index($createRelationNode) + | inline_table_indexes inline_table_index($createRelationNode) ; -%type packaged_table_index() -packaged_table_index($createRelationNode) +%type inline_table_index() +inline_table_index($createRelationNode) : unique_opt order_direction INDEX valid_symbol_name [YYVALID;] column_parens { const auto node = newNode(QualifiedName(*$4)); @@ -2616,7 +2616,7 @@ packaged_table_index($createRelationNode) node->descending = $2; node->columns = $6; - auto clause = newNode(node); + auto clause = newNode(node); $createRelationNode->clauses.add(clause); } ; @@ -3676,7 +3676,7 @@ local_nonforward_declaration $$ = newNode(relationNode); $$->tempFlag = REL_temp_ltt; } - '(' table_elements($6) ')' ';' + '(' table_elements($6) ')' [YYVALID;] inline_table_indexes_opt($6) ';' { DeclareLocalTableNode* node = newNode(); node->dsqlName = *$5; diff --git a/src/include/firebird/impl/blr.h b/src/include/firebird/impl/blr.h index 2baa002347b..cc3b5d70a9e 100644 --- a/src/include/firebird/impl/blr.h +++ b/src/include/firebird/impl/blr.h @@ -464,6 +464,9 @@ #define blr_dcl_local_table_format (unsigned char) 1 #define blr_dcl_local_table_ltt (unsigned char) 2 #define blr_dcl_local_table_field_names (unsigned char) 3 +#define blr_dcl_local_table_index (unsigned char) 4 +#define blr_dcl_local_table_index_unique (unsigned char) 1 +#define blr_dcl_local_table_index_descending (unsigned char) 2 #define blr_local_table_truncate (unsigned char) 219 #define blr_local_table_id (unsigned char) 220 diff --git a/src/jrd/RecordSourceNodes.cpp b/src/jrd/RecordSourceNodes.cpp index 506cc628813..23f405dc95a 100644 --- a/src/jrd/RecordSourceNodes.cpp +++ b/src/jrd/RecordSourceNodes.cpp @@ -414,23 +414,36 @@ PlanNode* PlanNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) if (context->ctx_relation) { - RelationSourceNode* relNode = FB_NEW_POOL(pool) RelationSourceNode(pool); - relNode->dsqlContext = context; - node->recordSourceNode = relNode; + if (context->ctx_relation->rel_flags & REL_ltt_declared) + { + const auto localTableNode = FB_NEW_POOL(pool) LocalTableSourceNode(pool); + localTableNode->dsqlContext = context; + localTableNode->outerDecl = context->ctx_local_table_outer; + fb_assert(context->ctx_relation->rel_local_table_number.has_value()); + 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(); + node->recordSourceNode = localTableNode; + } + else + { + const auto relNode = FB_NEW_POOL(pool) RelationSourceNode(pool); + relNode->dsqlContext = context; + node->recordSourceNode = relNode; + } } else if (context->ctx_procedure) { - ProcedureSourceNode* procNode = FB_NEW_POOL(pool) ProcedureSourceNode(pool); + const auto procNode = FB_NEW_POOL(pool) ProcedureSourceNode(pool); procNode->dsqlContext = context; node->recordSourceNode = procNode; } else if (context->ctx_table_value_fun) { - auto tableValueFunctionNode = FB_NEW_POOL(pool) TableValueFunctionSourceNode(pool); + const auto tableValueFunctionNode = FB_NEW_POOL(pool) TableValueFunctionSourceNode(pool); tableValueFunctionNode->dsqlContext = context; node->recordSourceNode = tableValueFunctionNode; } - //// TODO: LocalTableSourceNode // ASF: I think it's a error to let node->recordSourceNode be NULL here, but it happens // at least since v2.5. See gen.cpp/gen_plan for more information. @@ -757,6 +770,18 @@ void LocalTableSourceNode::pass2Rse(thread_db* tdbb, CompilerScratch* csb) csb->csb_rpt[stream].activate(); pass2(tdbb, csb); + + if (tableNumber >= csb->csb_localTables.getCount() || !csb->csb_localTables[tableNumber]) + ERR_post(Arg::Gds(isc_bad_loctab_num) << Arg::Num(tableNumber)); + + const auto localTable = csb->csb_localTables[tableNumber]; + + if (localTable->useLtt) + { + const auto relation = localTable->getRelation(tdbb, nullptr)->getPermanent(); + csb->csb_rpt[stream].csb_relation = + csb->csb_resources->relations.registerResource(relation); + } } RecordSource* LocalTableSourceNode::compile(thread_db* tdbb, Optimizer* opt, bool /*innerSubStream*/) @@ -768,6 +793,12 @@ RecordSource* LocalTableSourceNode::compile(thread_db* tdbb, Optimizer* opt, boo auto localTable = csb->csb_localTables[tableNumber]; + if (localTable->useLtt) + { + opt->compileLocalTable(stream); + return nullptr; + } + return FB_NEW_POOL(*tdbb->getDefaultPool()) LocalTableStream(csb, stream, localTable, outerDecl); } @@ -3705,7 +3736,9 @@ void RseNode::planCheck(thread_db* tdbb, const CompilerScratch* csb) const for (const auto node : rse_relations) { - if (nodeIs(node) || nodeIs(node)) + if (nodeIs(node) || + nodeIs(node) || + nodeIs(node)) { const auto stream = node->getStream(); @@ -3743,11 +3776,28 @@ void RseNode::planSet(thread_db* tdbb, CompilerScratch* csb, PlanNode* plan) string planAlias; RelationPermanent* planRelation = nullptr; + const DeclareLocalTableNode* planLocalTable = nullptr; + if (const auto relationNode = nodeAs(plan->recordSourceNode)) { planRelation = relationNode->relation(); planAlias = relationNode->alias; } + else if (const auto localTableNode = nodeAs(plan->recordSourceNode)) + { + if (localTableNode->tableNumber >= csb->csb_localTables.getCount() || + !csb->csb_localTables[localTableNode->tableNumber]) + { + ERR_post(Arg::Gds(isc_bad_loctab_num) << Arg::Num(localTableNode->tableNumber)); + } + + planLocalTable = csb->csb_localTables[localTableNode->tableNumber]; + + if (planLocalTable->useLtt) + planRelation = planLocalTable->getRelation(tdbb, nullptr)->getPermanent(); + + planAlias = localTableNode->alias; + } RoutinePermanent* planProcedure = nullptr; if (const auto procedureNode = nodeAs(plan->recordSourceNode)) @@ -3756,7 +3806,7 @@ void RseNode::planSet(thread_db* tdbb, CompilerScratch* csb, PlanNode* plan) planAlias = procedureNode->alias; } - fb_assert(planRelation || planProcedure); + fb_assert(planRelation || planProcedure || planLocalTable); ObjectsArray planAliasList; if (planAlias.hasData()) @@ -3936,7 +3986,17 @@ void RseNode::planSet(thread_db* tdbb, CompilerScratch* csb, PlanNode* plan) // Make some validity checks - if (!tail->csb_relation && !tail->csb_procedure) + const DeclareLocalTableNode* tailLocalTable = nullptr; + if (tail->csb_local_table_number.has_value()) + { + const auto tableNumber = tail->csb_local_table_number.value(); + if (tableNumber >= csb->csb_localTables.getCount() || !csb->csb_localTables[tableNumber]) + ERR_post(Arg::Gds(isc_bad_loctab_num) << Arg::Num(tableNumber)); + + tailLocalTable = csb->csb_localTables[tableNumber]; + } + + if (!tail->csb_relation && !tail->csb_procedure && !tailLocalTable) { // table or procedure %s is referenced in the plan but not the from list ERR_post(Arg::Gds(isc_stream_not_found) << name.toQuotedString()); @@ -3944,6 +4004,9 @@ void RseNode::planSet(thread_db* tdbb, CompilerScratch* csb, PlanNode* plan) if ((tail->csb_relation && planRelation && tail->csb_relation()->getId() != planRelation->getId() && !viewRelation) || + (tailLocalTable && !planLocalTable) || + (!tailLocalTable && planLocalTable) || + (tailLocalTable && planLocalTable && tailLocalTable != planLocalTable) || (tail->csb_procedure && planProcedure && tail->csb_procedure()->getId() != planProcedure->getId() && !viewProcedure)) { diff --git a/src/jrd/Relation.h b/src/jrd/Relation.h index e0f709daa8d..53710d394df 100644 --- a/src/jrd/Relation.h +++ b/src/jrd/Relation.h @@ -608,7 +608,8 @@ class IndexVersion final : public ObjectBase return true; } - void setLtt(thread_db* tdbb, LocalTemporaryTable::Index* ltt); + void setLtt(thread_db* tdbb, const QualifiedName& name, bool unique, bool descending, + USHORT segmentCount, bool inactive = false); static const enum lck_t LOCKTYPE = LCK_idx_rescan; diff --git a/src/jrd/Savepoint.cpp b/src/jrd/Savepoint.cpp index 38955b5cc44..215bffe1ab9 100644 --- a/src/jrd/Savepoint.cpp +++ b/src/jrd/Savepoint.cpp @@ -852,34 +852,39 @@ void AutoSavePoint::rollback(bool preserveLocks) // StableCursorSavePoint implementation -StableCursorSavePoint::StableCursorSavePoint(thread_db* tdbb, jrd_tra* trans, bool start) +StableCursorSavePoint::StableCursorSavePoint(thread_db* tdbb, jrd_tra* trans, bool shouldStart) : m_tdbb(tdbb), m_transaction(trans), m_number(0) { - if (!start) - return; - - if (trans->tra_flags & TRA_system) - return; + if (shouldStart) + m_number = startSavepoint(trans); +} - if (!trans->tra_save_point) - return; +SavNumber StableCursorSavePoint::startSavepoint(jrd_tra* trans) +{ + if (!trans || (trans->tra_flags & TRA_system) || !trans->tra_save_point) + return 0; - const auto savepoint = trans->startSavepoint(); - m_number = savepoint->getNumber(); + return trans->startSavepoint()->getNumber(); } - -void StableCursorSavePoint::release() +void StableCursorSavePoint::releaseSavepoint(thread_db* tdbb, jrd_tra* trans, SavNumber& number) { - if (!m_number) + if (!number || !trans) + { + number = 0; return; + } - while (m_transaction->tra_save_point && - m_transaction->tra_save_point->getNumber() >= m_number) + while (trans->tra_save_point && trans->tra_save_point->getNumber() >= number) { - fb_assert(!m_transaction->tra_save_point->isChanging()); - m_transaction->releaseSavepoint(m_tdbb); + fb_assert(!trans->tra_save_point->isChanging()); + trans->releaseSavepoint(tdbb); } - m_number = 0; + number = 0; +} + +void StableCursorSavePoint::release() +{ + releaseSavepoint(m_tdbb, m_transaction, m_number); } diff --git a/src/jrd/Savepoint.h b/src/jrd/Savepoint.h index 52c00e3165c..4cb8554ef7d 100644 --- a/src/jrd/Savepoint.h +++ b/src/jrd/Savepoint.h @@ -381,14 +381,18 @@ namespace Jrd SavNumber m_number; }; - // Conditional savepoint used to ensure cursor stability in sub-queries + // Conditional savepoint used to ensure cursor stability in sub-queries and record sources - class StableCursorSavePoint + class StableCursorSavePoint final { public: - StableCursorSavePoint(thread_db* tdbb, jrd_tra* trans, bool start); + StableCursorSavePoint(thread_db* tdbb, jrd_tra* trans, bool shouldStart); ~StableCursorSavePoint() {} // undo is left up to the callers + public: + static SavNumber startSavepoint(jrd_tra* trans); + static void releaseSavepoint(thread_db* tdbb, jrd_tra* trans, SavNumber& number); + void release(); private: diff --git a/src/jrd/btr.cpp b/src/jrd/btr.cpp index d5968fbfb15..f07e2d411b9 100644 --- a/src/jrd/btr.cpp +++ b/src/jrd/btr.cpp @@ -432,7 +432,10 @@ void IndexErrorContext::raise(thread_db* tdbb, idx_e result, Record* record) } if (indexName.object.hasData()) - MET_lookup_cnstrt_for_index(tdbb, constraintName, indexName); + { + if (!relation->isLTT()) + MET_lookup_cnstrt_for_index(tdbb, constraintName, indexName); + } else indexName.object = "***unknown***"; diff --git a/src/jrd/dfw.epp b/src/jrd/dfw.epp index 5bef5ff1ab7..c746674b51e 100644 --- a/src/jrd/dfw.epp +++ b/src/jrd/dfw.epp @@ -4986,7 +4986,8 @@ static bool create_ltt_index(thread_db* tdbb, SSHORT phase, DeferredWork* work, // create versioned part auto& attPool = *attachment->att_pool; AutoPtr idv = FB_NEW_POOL(attPool) IndexVersion(attPool, idp); - idv->setLtt(tdbb, lttIndex); + idv->setLtt(tdbb, lttIndex->name, lttIndex->unique, lttIndex->descending, + lttIndex->columns.getCount(), lttIndex->inactive); idp->storeObject(tdbb, idv, 0); idv.release(); diff --git a/src/jrd/met.epp b/src/jrd/met.epp index 2efe74ad0a3..dc37d8225d7 100644 --- a/src/jrd/met.epp +++ b/src/jrd/met.epp @@ -5168,17 +5168,18 @@ ScanResult IndexVersion::scan(thread_db* tdbb, ObjectBase::Flag flags) return found ? ScanResult::COMPLETE : ScanResult::MISS; } -void IndexVersion::setLtt(thread_db* tdbb, LocalTemporaryTable::Index* lttIndex) +void IndexVersion::setLtt(thread_db* tdbb, const QualifiedName& name, bool unique, bool descending, + USHORT segmentCount, bool inactive) { SET_TDBB(tdbb); fb_assert(getPermanent()->getRelation()->rel_flags & REL_temp_ltt); - idv_name = lttIndex->name; - idv_uniqFlag = lttIndex->unique; - idv_segmentCount = lttIndex->columns.getCount(); - idv_type = lttIndex->descending; - idv_active = lttIndex->inactive ? MET_index_inactive : MET_index_active; + idv_name = name; + idv_uniqFlag = unique; + idv_segmentCount = segmentCount; + idv_type = descending; + idv_active = inactive ? MET_index_inactive : MET_index_active; } IndexVersion* RelationPermanent::lookup_index(thread_db* tdbb, const QualifiedName& name, ObjectBase::Flag flags) diff --git a/src/jrd/optimizer/Optimizer.cpp b/src/jrd/optimizer/Optimizer.cpp index ca7436fb06f..2eccaef6e85 100644 --- a/src/jrd/optimizer/Optimizer.cpp +++ b/src/jrd/optimizer/Optimizer.cpp @@ -642,10 +642,10 @@ Optimizer::Optimizer(thread_db* aTdbb, CompilerScratch* aCsb, RseNode* aRse, for (const auto stream : rseStreams) { - if (csb->csb_rpt[stream].csb_relation) - compileRelation(stream); - else if (csb->csb_rpt[stream].csb_local_table_number.has_value()) + if (csb->csb_rpt[stream].csb_local_table_number.has_value()) compileLocalTable(stream); + else if (csb->csb_rpt[stream].csb_relation) + compileRelation(stream); } } @@ -1185,6 +1185,9 @@ RecordSource* Optimizer::compile(BoolExprNodeStack* parentStack) const auto tail = &csb->csb_rpt[compileStream]; tail->csb_flags |= csb_update; + if (tail->csb_local_table_number.has_value()) + continue; + fb_assert(tail->csb_relation); const SLONG ssRelationId = tail->csb_view ? tail->csb_view()->rel_id : 0; @@ -1356,6 +1359,8 @@ void Optimizer::compileLocalTable(StreamType stream) compileStreams.add(stream); const auto tail = &csb->csb_rpt[stream]; + // Declared LTT data is stored in a frame-scoped instance. No instance is active + // during optimization, so use the default estimate instead of reading relation pages. tail->csb_cardinality = DEFAULT_CARDINALITY; tail->csb_idx = nullptr; @@ -1372,32 +1377,33 @@ void Optimizer::compileLocalTable(StreamType stream) if (!localTable->useLtt) return; - const bool needIndices = conjuncts.hasData() || (rse->rse_sorted || rse->rse_aggregate); + if (!tail->csb_relation) + { + const auto relation = localTable->getRelation(tdbb, nullptr)->getPermanent(); + tail->csb_relation = csb->csb_resources->relations.registerResource(relation); + } + + const bool needIndices = conjuncts.hasData() || (rse->rse_sorted || rse->rse_aggregate) || + (tail->csb_plan && tail->csb_plan->accessType); if (!needIndices) return; - const auto relation = localTable->getRelation(tdbb, nullptr)->getPermanent(); - const auto relPages = relation->getPages(tdbb); + const auto relation = tail->csb_relation; IndexDescList idxList; - BTR_all(tdbb, relation, idxList, relPages, csb->csb_g_flags & csb_internal); - - MetaId n = idxList.getCount(); - while (n--) + for (FB_SIZE_T indexId = 0; indexId < localTable->indexes.getCount(); ++indexId) { - 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); + index_desc idx; + localTable->getIndexDescription(tdbb, indexId, &idx); + idx.idx_fraction = MAXIMUM_SELECTIVITY; + idxList.add(idx); } if (idxList.hasData()) tail->csb_idx = FB_NEW_POOL(getPool()) IndexDescList(getPool(), idxList); if (tail->csb_plan) - markIndices(tail, relation->getId()); + markIndices(tail, relation()->getId()); } @@ -2029,9 +2035,9 @@ void Optimizer::checkIndices() if (plan->type != PlanNode::TYPE_RETRIEVE) continue; - auto* const relation = tail->csb_relation(); + auto* const relation = tail->csb_relation ? tail->csb_relation() : nullptr; if (!relation) - return; + continue; // If there were no indices fetched at all but the user specified some, // error out using the first index specified @@ -2705,8 +2711,11 @@ void Optimizer::formRivers(const StreamList& streams, // the stream into the river fb_assert(planNode->type == PlanNode::TYPE_RETRIEVE); - if (!nodeIs(planNode->recordSourceNode)) + if (!nodeIs(planNode->recordSourceNode) && + !nodeIs(planNode->recordSourceNode)) + { continue; + } const auto stream = planNode->recordSourceNode->getStream(); @@ -3051,6 +3060,9 @@ RecordSource* Optimizer::generateRetrieval(StreamType stream, const auto relation = tail->csb_relation; fb_assert(relation); + const auto localTable = tail->csb_local_table_number.has_value() ? + csb->csb_localTables[tail->csb_local_table_number.value()] : nullptr; + const string alias = makeAlias(stream); tail->activate(); @@ -3225,7 +3237,16 @@ RecordSource* Optimizer::generateRetrieval(StreamType stream, } } - return boolean ? FB_NEW_POOL(getPool()) FilteredStream(csb, rsb, boolean, filterSelectivity) : rsb; + if (localTable && localTable->useLtt) + { + rsb = FB_NEW_POOL(getPool()) LocalTableRecordSource(csb, stream, rsb, localTable, + tail->csb_outer_local_table); + } + + if (boolean) + rsb = FB_NEW_POOL(getPool()) FilteredStream(csb, rsb, boolean, filterSelectivity); + + return rsb; } diff --git a/src/jrd/optimizer/Optimizer.h b/src/jrd/optimizer/Optimizer.h index aa68e2d71ec..b18faae5d16 100644 --- a/src/jrd/optimizer/Optimizer.h +++ b/src/jrd/optimizer/Optimizer.h @@ -79,6 +79,7 @@ inline constexpr double DEFAULT_INDEX_COST = 3.0; struct index_desc; class jrd_rel; class IndexTableScan; +class DeclareLocalTableNode; class ComparativeBoolNode; class InversionNode; class PlanNode; @@ -809,6 +810,7 @@ class Retrieval final : private Firebird::PermanentStorage const bool outerFlag; SortNode* const sort; Rsc::Rel relation; + const DeclareLocalTableNode* localTable = nullptr; const bool createIndexScanNodes; const bool setConjunctionsMatched; Firebird::string alias; diff --git a/src/jrd/optimizer/OuterJoin.cpp b/src/jrd/optimizer/OuterJoin.cpp index 20573dd260e..0011c3959b1 100644 --- a/src/jrd/optimizer/OuterJoin.cpp +++ b/src/jrd/optimizer/OuterJoin.cpp @@ -57,7 +57,8 @@ OuterJoin::OuterJoin(thread_db* aTdbb, Optimizer* opt, auto& joinStream = joinStreams[pos]; joinStream.node = node; - if (nodeIs(node)) + if (nodeIs(node) || + (nodeIs(node) && csb->csb_rpt[node->getStream()].csb_relation)) { const auto stream = node->getStream(); fb_assert(!(csb->csb_rpt[stream].csb_flags & csb_active)); @@ -215,5 +216,3 @@ RecordSource* OuterJoin::process() return FB_NEW_POOL(getPool()) NestedLoopJoin(csb, outerRsb, innerRsb, boolean); }; - - diff --git a/src/jrd/optimizer/Retrieval.cpp b/src/jrd/optimizer/Retrieval.cpp index 168a51e5383..4ab26cdc2da 100644 --- a/src/jrd/optimizer/Retrieval.cpp +++ b/src/jrd/optimizer/Retrieval.cpp @@ -159,6 +159,14 @@ Retrieval::Retrieval(thread_db* aTdbb, Optimizer* opt, StreamType streamNumber, const auto tail = &csb->csb_rpt[stream]; relation = tail->csb_relation; + if (tail->csb_local_table_number.has_value()) + { + const auto tableNumber = tail->csb_local_table_number.value(); + + if (tableNumber < csb->csb_localTables.getCount()) + localTable = csb->csb_localTables[tableNumber]; + } + if (!tail->csb_idx) return; @@ -173,7 +181,8 @@ Retrieval::Retrieval(thread_db* aTdbb, Optimizer* opt, StreamType streamNumber, if ((index.idx_flags & idx_condition) && !checkIndexCondition(index, matches)) continue; - const auto length = ROUNDUP(BTR_key_length(tdbb, relation(tdbb), &index), sizeof(SLONG)); + const auto relationForKey = localTable ? localTable->getRelation(tdbb, nullptr) : relation(tdbb); + const auto length = ROUNDUP(BTR_key_length(tdbb, relationForKey, &index), sizeof(SLONG)); // AB: Calculate the cardinality which should reflect the total number // of index pages for this index. @@ -417,8 +426,9 @@ IndexTableScan* Retrieval::getNavigation() const auto indexNode = makeIndexScanNode(scratch); + const auto relationForKey = localTable ? localTable->getRelation(tdbb, nullptr) : relation(tdbb); const USHORT keyLength = - ROUNDUP(BTR_key_length(tdbb, relation(tdbb), scratch->index), sizeof(SLONG)); + ROUNDUP(BTR_key_length(tdbb, relationForKey, scratch->index), sizeof(SLONG)); return FB_NEW_POOL(getPool()) IndexTableScan(csb, getAlias(), stream, relation, indexNode, keyLength, diff --git a/src/jrd/par.cpp b/src/jrd/par.cpp index 7532166d6bd..31e07dd873c 100644 --- a/src/jrd/par.cpp +++ b/src/jrd/par.cpp @@ -1023,6 +1023,25 @@ static void checkIndexStatus(CompilerScratch* csb, bool isGbak, IndexStatus idx_ } } +static ElementBase::ReturnedId lookupPlanIndex(thread_db* tdbb, Cached::Relation* relation, + const QualifiedName& name, MetaId& foundRelationId, IndexStatus& idxStatus) +{ + if (relation->isLTT()) + { + if (const auto index = relation->lookup_index(tdbb, name, 0)) + { + foundRelationId = relation->getId(); + idxStatus = index->getActive(); + return index->getId(); + } + + idxStatus = MET_index_state_unknown; + return 0; + } + + return MetadataCache::lookup_index_name(tdbb, name, &foundRelationId, &idxStatus); +} + static PlanNode* par_plan(thread_db* tdbb, CompilerScratch* csb) { @@ -1106,7 +1125,21 @@ static PlanNode* par_plan(thread_db* tdbb, CompilerScratch* csb) } case blr_local_table_id: - // TODO + { + const auto localTableNode = LocalTableSourceNode::parse(tdbb, csb, blrOp, false); + plan->recordSourceNode = localTableNode; + + if (localTableNode->tableNumber >= csb->csb_localTables.getCount() || + !csb->csb_localTables[localTableNode->tableNumber]) + { + PAR_error(csb, Arg::Gds(isc_bad_loctab_num) << Arg::Num(localTableNode->tableNumber)); + } + + const auto localTable = csb->csb_localTables[localTableNode->tableNumber]; + if (localTable->useLtt) + relation = localTable->getRelation(tdbb, nullptr)->getPermanent(); + break; + } default: PAR_syntax_error(csb, "TABLE or PROCEDURE"); @@ -1141,6 +1174,9 @@ static PlanNode* par_plan(thread_db* tdbb, CompilerScratch* csb) if (procedure) PAR_error(csb, Arg::Gds(isc_wrong_proc_plan)); + if (!relation) + PAR_error(csb, Arg::Gds(isc_random) << "Index access plans are not supported for non-LTT local tables"); + plan->accessType = FB_NEW_POOL(csb->csb_pool) PlanNode::AccessType(csb->csb_pool, PlanNode::AccessType::TYPE_NAVIGATIONAL); @@ -1153,7 +1189,7 @@ static PlanNode* par_plan(thread_db* tdbb, CompilerScratch* csb) IndexStatus idx_status; MetaId foundRelationId; const ElementBase::ReturnedId index_id = - MetadataCache::lookup_index_name(tdbb, name, &foundRelationId, &idx_status); + lookupPlanIndex(tdbb, relation, name, foundRelationId, idx_status); checkIndexStatus(csb, isGbak, idx_status, name, relation); // save both the relation id and the index id, since @@ -1165,7 +1201,7 @@ static PlanNode* par_plan(thread_db* tdbb, CompilerScratch* csb) item.indexId = index_id; item.indexName = name; - if (csb->collectingDependencies()) + if (csb->collectingDependencies() && !relation->isLTT()) { Dependency dependency(obj_index); dependency.name = item.indexName; @@ -1184,6 +1220,9 @@ static PlanNode* par_plan(thread_db* tdbb, CompilerScratch* csb) if (procedure) PAR_error(csb, Arg::Gds(isc_wrong_proc_plan)); + if (!relation) + PAR_error(csb, Arg::Gds(isc_random) << "Index access plans are not supported for non-LTT local tables"); + if (plan->accessType) csb->csb_blr_reader.getByte(); // skip blr_indices else @@ -1205,7 +1244,7 @@ static PlanNode* par_plan(thread_db* tdbb, CompilerScratch* csb) IndexStatus idx_status; MetaId foundRelationId; const ElementBase::ReturnedId index_id = - MetadataCache::lookup_index_name(tdbb, name, &foundRelationId, &idx_status); + lookupPlanIndex(tdbb, relation, name, foundRelationId, idx_status); checkIndexStatus(csb, isGbak, idx_status, name, relation); // save both the relation id and the index id, since @@ -1217,7 +1256,7 @@ static PlanNode* par_plan(thread_db* tdbb, CompilerScratch* csb) item.indexId = index_id; item.indexName = name; - if (csb->collectingDependencies()) + if (csb->collectingDependencies() && !relation->isLTT()) { Dependency dependency(obj_index); dependency.name = item.indexName; diff --git a/src/jrd/recsrc/BitmapTableScan.cpp b/src/jrd/recsrc/BitmapTableScan.cpp index e7cb01d77fb..2c021e077f2 100644 --- a/src/jrd/recsrc/BitmapTableScan.cpp +++ b/src/jrd/recsrc/BitmapTableScan.cpp @@ -56,7 +56,8 @@ void BitmapTableScan::internalOpen(thread_db* tdbb) const impure->irsb_bitmap = EVL_bitmap(tdbb, m_inversion, NULL); record_param* const rpb = &request->req_rpb[m_stream]; - RLCK_reserve_relation(tdbb, request->req_transaction, m_relation(), false); + const auto transaction = m_relation()->isLTT() ? tdbb->getTransaction() : request->req_transaction; + RLCK_reserve_relation(tdbb, transaction, m_relation(), false); rpb->rpb_number.setValue(BOF_NUMBER); } @@ -110,7 +111,9 @@ bool BitmapTableScan::internalGetRecord(thread_db* tdbb) const { rpb->rpb_number.setValue(bitmap->current()); - if (VIO_get(tdbb, rpb, request->req_transaction, request->req_pool)) + const auto transaction = m_relation()->isLTT() ? tdbb->getTransaction() : request->req_transaction; + + if (VIO_get(tdbb, rpb, transaction, request->req_pool)) { rpb->rpb_number.setValid(true); return true; diff --git a/src/jrd/recsrc/FullTableScan.cpp b/src/jrd/recsrc/FullTableScan.cpp index 810abec4eb9..56ec0435eee 100644 --- a/src/jrd/recsrc/FullTableScan.cpp +++ b/src/jrd/recsrc/FullTableScan.cpp @@ -57,7 +57,8 @@ void FullTableScan::internalOpen(thread_db* tdbb) const impure->irsb_flags = irsb_open; - RLCK_reserve_relation(tdbb, request->req_transaction, m_relation(), false); + const auto transaction = m_relation()->isLTT() ? tdbb->getTransaction() : request->req_transaction; + RLCK_reserve_relation(tdbb, transaction, m_relation(), false); record_param* const rpb = &request->req_rpb[m_stream]; rpb->getWindow(tdbb).win_flags = 0; @@ -148,7 +149,9 @@ bool FullTableScan::internalGetRecord(thread_db* tdbb) const const RecordNumber* upper = impure->irsb_upper.isValid() ? &impure->irsb_upper : nullptr; - if (VIO_next_record(tdbb, rpb, request->req_transaction, request->req_pool, DPM_next_all, upper)) + const auto transaction = m_relation()->isLTT() ? tdbb->getTransaction() : request->req_transaction; + + if (VIO_next_record(tdbb, rpb, transaction, request->req_pool, DPM_next_all, upper)) { rpb->rpb_number.setValid(true); return true; diff --git a/src/jrd/recsrc/IndexTableScan.cpp b/src/jrd/recsrc/IndexTableScan.cpp index 61ac3d9bbea..02ed32fbd9f 100644 --- a/src/jrd/recsrc/IndexTableScan.cpp +++ b/src/jrd/recsrc/IndexTableScan.cpp @@ -69,7 +69,8 @@ void IndexTableScan::internalOpen(thread_db* tdbb) const impure->irsb_flags = irsb_first | irsb_open; record_param* const rpb = &request->req_rpb[m_stream]; - RLCK_reserve_relation(tdbb, request->req_transaction, m_relation(), false); + const auto transaction = m_relation()->isLTT() ? tdbb->getTransaction() : request->req_transaction; + RLCK_reserve_relation(tdbb, transaction, m_relation(), false); rpb->rpb_number.setValue(BOF_NUMBER); @@ -332,7 +333,9 @@ bool IndexTableScan::internalGetRecord(thread_db* tdbb) const CCH_RELEASE(tdbb, &window); - if (VIO_get(tdbb, rpb, request->req_transaction, request->req_pool)) + const auto transaction = m_relation()->isLTT() ? tdbb->getTransaction() : request->req_transaction; + + if (VIO_get(tdbb, rpb, transaction, request->req_pool)) { if (const auto result = recordKey.compose(rpb->rpb_record)) { diff --git a/src/jrd/recsrc/LocalTableStream.cpp b/src/jrd/recsrc/LocalTableStream.cpp index 7e1b5f700f0..7955d3191e5 100644 --- a/src/jrd/recsrc/LocalTableStream.cpp +++ b/src/jrd/recsrc/LocalTableStream.cpp @@ -24,7 +24,7 @@ #include "../jrd/align.h" #include "../jrd/jrd.h" #include "../jrd/req.h" -#include "../jrd/tra.h" +#include "../jrd/Savepoint.h" #include "../dsql/StmtNodes.h" #include "../jrd/optimizer/Optimizer.h" #include "../jrd/dpm_proto.h" @@ -37,58 +37,286 @@ using namespace Firebird; using namespace Jrd; -// ------------------------ -// Data access: local table -// ------------------------ -LocalTableStream::LocalTableStream(CompilerScratch* csb, StreamType stream, const DeclareLocalTableNode* table, - bool outerDecl) - : RecordStream(csb, stream), - m_table(table), +static SavNumber startLocalTableCursorSavepoint(const LocalTableContext& context) +{ + const auto localTableRequest = context.getLocalTableRequest(); + + if (!localTableRequest || !localTableRequest->req_auto_trans.hasData()) + return 0; + + return StableCursorSavePoint::startSavepoint(context.getTransaction()); +} + + +LocalTableContext::LocalTableContext(thread_db* tdbb, Request* request, + const DeclareLocalTableNode* table, bool outerDecl) + : m_tdbb(tdbb), + m_request(request), + m_localTableRequest(table ? request->getLocalTableRequest(outerDecl) : request), + m_oldTransaction(tdbb->getTransaction()), + m_transaction(request->req_transaction), + m_oldFrameId(tdbb->tdbb_temp_frame_id), + m_frameId(0) +{ + m_oldSnapshot.init(); + + if (table && table->useLtt) + { + m_transaction = m_localTableRequest->getLocalTableTransaction(); + tdbb->setTransaction(m_transaction); + m_frameId = m_localTableRequest->getLocalTableInstanceId(tdbb); + tdbb->tdbb_temp_frame_id = m_frameId; + m_switched = true; + + Request::AutoTranCtx autoTranCtx; + if (m_localTableRequest->getLocalTableAutoTranCtx(autoTranCtx)) + { + m_oldSnapshot = request->req_snapshot; + request->req_snapshot = autoTranCtx.m_snapshot; + m_restoreSnapshot = true; + } + } +} + +LocalTableContext::~LocalTableContext() +{ + if (m_restoreSnapshot) + m_request->req_snapshot = m_oldSnapshot; + + if (m_switched) + { + m_tdbb->tdbb_temp_frame_id = m_oldFrameId; + m_tdbb->setTransaction(m_oldTransaction); + } +} + + +LocalTableScan::LocalTableScan(CompilerScratch* csb, StreamType stream, + const DeclareLocalTableNode* table, bool outerDecl, const Format* format) + : RecordStream(csb, stream, format), + m_localTable(table), + m_outerDecl(outerDecl), + m_localImpure(table ? csb->allocImpure() : 0) +{ + if (m_localImpure) + m_impure = m_localImpure; +} + +void LocalTableScan::setupLocalTable(thread_db* tdbb, const LocalTableContext& context) const +{ + if (!m_localTable || !m_localTable->useLtt) + return; + + const auto rpb = &tdbb->getRequest()->req_rpb[m_stream]; + rpb->rpb_relation = m_localTable->getRelation(tdbb, context.getLocalTableRequest()); + rpb->rpb_temp_instance_id = context.getFrameId(); +} + +void LocalTableScan::initializeLocalTable(const LocalTableContext& context) const +{ + if (!m_localTable) + return; + + const auto request = context.getLocalTableRequest(); + const auto impure = context.getRequest()->getImpure(m_localImpure); + impure->localTableRequest = request; + impure->cursorTransaction = nullptr; + impure->cursorSavepoint = 0; + + if (!m_localTable->useLtt) + return; + + impure->cursorSavepoint = startLocalTableCursorSavepoint(context); + if (impure->cursorSavepoint) + impure->cursorTransaction = context.getTransaction(); +} + +void LocalTableScan::closeLocalTable(thread_db* tdbb) const +{ + if (!m_localTable || !m_localTable->useLtt || !m_localImpure) + return; + + const auto request = tdbb->getRequest(); + const auto impure = request->getImpure(m_localImpure); + if (!impure->cursorSavepoint) + return; + + StableCursorSavePoint::releaseSavepoint(tdbb, impure->cursorTransaction, impure->cursorSavepoint); + impure->cursorTransaction = nullptr; +} + +bool LocalTableScan::refetchRecord(thread_db* tdbb) const +{ + if (m_localTable) + return true; + + return RecordStream::refetchRecord(tdbb); +} + +WriteLockResult LocalTableScan::lockRecord(thread_db* tdbb) const +{ + if (m_localTable) + status_exception::raise(Arg::Gds(isc_record_lock_not_supp)); + + return RecordStream::lockRecord(tdbb); +} + +void LocalTableScan::nullRecords(thread_db* tdbb) const +{ + if (!m_localTable) + { + RecordStream::nullRecords(tdbb); + return; + } + + const auto request = tdbb->getRequest(); + LocalTableContext context(tdbb, request, m_localTable, m_outerDecl); + RecordStream::nullRecords(tdbb); +} + + +// ---------------------------------- +// Data access: local table decorator +// ---------------------------------- + +LocalTableRecordSource::LocalTableRecordSource(CompilerScratch* csb, StreamType stream, + RecordSource* next, const DeclareLocalTableNode* table, bool outerDecl) + : RecordSource(csb), + m_stream(stream), + m_next(next), + m_localTable(table), m_outerDecl(outerDecl) { - fb_assert(m_table); + fb_assert(m_next && m_localTable && m_localTable->useLtt); m_impure = csb->allocImpure(); - m_cardinality = DEFAULT_CARDINALITY; + m_cardinality = next->getCardinality(); } -void LocalTableStream::internalOpen(thread_db* tdbb) const +void LocalTableRecordSource::internalOpen(thread_db* tdbb) const { const auto request = tdbb->getRequest(); const auto impure = request->getImpure(m_impure); - - impure->irsb_flags = irsb_open; + LocalTableContext context(tdbb, request, m_localTable, m_outerDecl); const auto rpb = &request->req_rpb[m_stream]; - rpb->getWindow(tdbb).win_flags = 0; - rpb->rpb_number.setValue(BOF_NUMBER); + rpb->rpb_relation = m_localTable->getRelation(tdbb, context.getLocalTableRequest()); + rpb->rpb_temp_instance_id = context.getFrameId(); + impure->localTableRequest = context.getLocalTableRequest(); + impure->cursorTransaction = nullptr; impure->cursorSavepoint = 0; - const auto localTableRequest = impure->localTableRequest = request->getLocalTableRequest(m_outerDecl); + impure->cursorSavepoint = startLocalTableCursorSavepoint(context); + if (impure->cursorSavepoint) + impure->cursorTransaction = context.getTransaction(); - if (m_table->useLtt) - { - const auto tempInstanceId = localTableRequest->getLocalTableInstanceId(tdbb); - AutoSetRestore autoFrameId( - &tdbb->tdbb_temp_frame_id, tempInstanceId); + impure->irsb_flags = irsb_open; + m_next->open(tdbb); +} - rpb->rpb_relation = m_table->getRelation(tdbb, localTableRequest); - rpb->rpb_temp_instance_id = tempInstanceId; +void LocalTableRecordSource::close(thread_db* tdbb) const +{ + const auto request = tdbb->getRequest(); - if (localTableRequest->req_auto_trans.hasData()) - { - const auto transaction = localTableRequest->getLocalTableTransaction(); + invalidateRecords(request); - // Keep rows inserted after this stream was opened out of the scan while - // retaining visibility of rows inserted before the autonomous block. - if (!(transaction->tra_flags & TRA_system) && transaction->tra_save_point) - impure->cursorSavepoint = transaction->startSavepoint()->getNumber(); - } + const auto impure = request->getImpure(m_impure); + + if (impure->irsb_flags & irsb_open) + { + impure->irsb_flags &= ~irsb_open; + + LocalTableContext context(tdbb, request, m_localTable, m_outerDecl); + m_next->close(tdbb); + + StableCursorSavePoint::releaseSavepoint(tdbb, impure->cursorTransaction, impure->cursorSavepoint); + impure->cursorTransaction = nullptr; } } +bool LocalTableRecordSource::internalGetRecord(thread_db* tdbb) const +{ + LocalTableContext context(tdbb, tdbb->getRequest(), m_localTable, m_outerDecl); + return m_next->getRecord(tdbb); +} + +bool LocalTableRecordSource::refetchRecord(thread_db* /*tdbb*/) const +{ + return true; +} + +WriteLockResult LocalTableRecordSource::lockRecord(thread_db* /*tdbb*/) const +{ + status_exception::raise(Arg::Gds(isc_record_lock_not_supp)); +} + +void LocalTableRecordSource::getLegacyPlan(thread_db* tdbb, string& plan, unsigned level) const +{ + m_next->getLegacyPlan(tdbb, plan, level); +} + +void LocalTableRecordSource::internalGetPlan(thread_db* tdbb, PlanEntry& planEntry, + unsigned level, bool recurse) const +{ + m_next->getPlan(tdbb, planEntry, level, recurse); +} + +void LocalTableRecordSource::markRecursive() +{ + m_next->markRecursive(); +} + +void LocalTableRecordSource::invalidateRecords(Request* request) const +{ + m_next->invalidateRecords(request); +} + +void LocalTableRecordSource::findUsedStreams(StreamList& streams, bool expandAll) const +{ + m_next->findUsedStreams(streams, expandAll); +} + +bool LocalTableRecordSource::isDependent(const StreamList& streams) const +{ + return m_next->isDependent(streams); +} + +void LocalTableRecordSource::nullRecords(thread_db* tdbb) const +{ + LocalTableContext context(tdbb, tdbb->getRequest(), m_localTable, m_outerDecl); + m_next->nullRecords(tdbb); +} + +// ------------------------ +// Data access: local table +// ------------------------ + +LocalTableStream::LocalTableStream(CompilerScratch* csb, StreamType stream, const DeclareLocalTableNode* table, + bool outerDecl) + : LocalTableScan(csb, stream, table, outerDecl) +{ + fb_assert(m_localTable); + + m_cardinality = DEFAULT_CARDINALITY; +} + +void LocalTableStream::internalOpen(thread_db* tdbb) const +{ + const auto request = tdbb->getRequest(); + const auto impure = request->getImpure(m_impure); + LocalTableContext context(tdbb, request, m_localTable, m_outerDecl); + setupLocalTable(tdbb, context); + initializeLocalTable(context); + + impure->irsb_flags = irsb_open; + + const auto rpb = &request->req_rpb[m_stream]; + rpb->getWindow(tdbb).win_flags = 0; + rpb->rpb_number.setValue(BOF_NUMBER); +} + void LocalTableStream::close(thread_db* tdbb) const { const auto request = tdbb->getRequest(); @@ -100,19 +328,7 @@ void LocalTableStream::close(thread_db* tdbb) const if (impure->irsb_flags & irsb_open) impure->irsb_flags &= ~irsb_open; - if (impure->cursorSavepoint) - { - const auto transaction = impure->localTableRequest->getLocalTableTransaction(); - - while (transaction->tra_save_point && - transaction->tra_save_point->getNumber() >= impure->cursorSavepoint) - { - fb_assert(!transaction->tra_save_point->isChanging()); - transaction->releaseSavepoint(tdbb); - } - - impure->cursorSavepoint = 0; - } + closeLocalTable(tdbb); } bool LocalTableStream::internalGetRecord(thread_db* tdbb) const @@ -122,6 +338,7 @@ bool LocalTableStream::internalGetRecord(thread_db* tdbb) const const auto request = tdbb->getRequest(); const auto rpb = &request->req_rpb[m_stream]; const auto impure = request->getImpure(m_impure); + LocalTableContext context(tdbb, request, m_localTable, m_outerDecl); if (!(impure->irsb_flags & irsb_open)) { @@ -129,12 +346,12 @@ bool LocalTableStream::internalGetRecord(thread_db* tdbb) const return false; } - if (!m_table->useLtt) + if (!m_localTable->useLtt) { if (!rpb->rpb_record) rpb->rpb_record = FB_NEW_POOL(*tdbb->getDefaultPool()) Record(*tdbb->getDefaultPool(), m_format); - const auto recordBuffer = m_table->getImpure(tdbb, impure->localTableRequest)->recordBuffer; + const auto recordBuffer = m_localTable->getImpure(tdbb, impure->localTableRequest)->recordBuffer; while (true) { @@ -156,15 +373,8 @@ bool LocalTableStream::internalGetRecord(thread_db* tdbb) const return true; } - const auto localTableRequest = impure->localTableRequest; - 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); - - const bool found = VIO_next_record(tdbb, rpb, transaction, request->req_pool, DPM_next_all, nullptr); + const bool found = VIO_next_record(tdbb, rpb, context.getTransaction(), request->req_pool, + DPM_next_all, nullptr); if (found) { diff --git a/src/jrd/recsrc/RecordSource.h b/src/jrd/recsrc/RecordSource.h index 59ffb7f52fd..a07e808fa43 100644 --- a/src/jrd/recsrc/RecordSource.h +++ b/src/jrd/recsrc/RecordSource.h @@ -229,6 +229,125 @@ namespace Jrd // Primary (table scan) access methods + class LocalTableContext final + { + public: + LocalTableContext(thread_db* tdbb, Request* request, + const DeclareLocalTableNode* table, bool outerDecl); + ~LocalTableContext(); + + LocalTableContext(const LocalTableContext&) = delete; + LocalTableContext& operator= (const LocalTableContext&) = delete; + + public: + jrd_tra* getTransaction() const noexcept + { + return m_transaction; + } + + Request* getLocalTableRequest() const noexcept + { + return m_localTableRequest; + } + + Request* getRequest() const noexcept + { + return m_request; + } + + FB_UINT64 getFrameId() const noexcept + { + return m_frameId; + } + + private: + thread_db* m_tdbb; + Request* m_request; + Request* m_localTableRequest; + jrd_tra* m_oldTransaction; + jrd_tra* m_transaction; + FB_UINT64 m_oldFrameId; + FB_UINT64 m_frameId; + Request::SnapshotData m_oldSnapshot; + bool m_restoreSnapshot = false; + bool m_switched = false; + }; + + class LocalTableScan : public RecordStream + { + protected: + struct LocalImpure : public RecordSource::Impure + { + Request* localTableRequest = nullptr; + jrd_tra* cursorTransaction = nullptr; + SavNumber cursorSavepoint; + }; + + protected: + LocalTableScan(CompilerScratch* csb, StreamType stream, + const DeclareLocalTableNode* table = nullptr, bool outerDecl = false, + const Format* format = nullptr); + + protected: + void setupLocalTable(thread_db* tdbb, const LocalTableContext& context) const; + void initializeLocalTable(const LocalTableContext& context) const; + void closeLocalTable(thread_db* tdbb) const; + + bool refetchRecord(thread_db* tdbb) const override; + WriteLockResult lockRecord(thread_db* tdbb) const override; + void nullRecords(thread_db* tdbb) const override; + + protected: + const DeclareLocalTableNode* m_localTable; + const bool m_outerDecl; + const ULONG m_localImpure; + }; + + class LocalTableRecordSource final : public RecordSource + { + struct Impure : public RecordSource::Impure + { + Request* localTableRequest = nullptr; + jrd_tra* cursorTransaction = nullptr; + SavNumber cursorSavepoint; + }; + + public: + LocalTableRecordSource(CompilerScratch* csb, StreamType stream, RecordSource* next, + const DeclareLocalTableNode* table, bool outerDecl); + + public: + void close(thread_db* tdbb) const override; + + bool refetchRecord(thread_db* tdbb) const override; + WriteLockResult lockRecord(thread_db* tdbb) const override; + + void getLegacyPlan(thread_db* tdbb, Firebird::string& plan, unsigned level) const override; + + void markRecursive() override; + void invalidateRecords(Request* request) const override; + + void findUsedStreams(StreamList& streams, bool expandAll = false) const override; + bool isDependent(const StreamList& streams) const override; + void nullRecords(thread_db* tdbb) const override; + + void setAnyBoolean(BoolExprNode* anyBoolean, bool ansiAny, bool ansiNot) override + { + m_next->setAnyBoolean(anyBoolean, ansiAny, ansiNot); + } + + protected: + void internalGetPlan(thread_db* tdbb, PlanEntry& planEntry, unsigned level, bool recurse) const override; + void internalOpen(thread_db* tdbb) const override; + bool internalGetRecord(thread_db* tdbb) const override; + + private: + const StreamType m_stream; + NestConst m_next; + const DeclareLocalTableNode* m_localTable; + const bool m_outerDecl; + }; + class FullTableScan final : public RecordStream { struct Impure : public RecordSource::Impure @@ -1449,7 +1568,7 @@ namespace Jrd Firebird::Array m_keys; }; - class LocalTableStream final : public RecordStream + class LocalTableStream final : public LocalTableScan { public: LocalTableStream(CompilerScratch* csb, StreamType stream, const DeclareLocalTableNode* table, @@ -1463,19 +1582,11 @@ namespace Jrd void getLegacyPlan(thread_db* tdbb, Firebird::string& plan, unsigned level) const override; protected: + using Impure = LocalTableScan::LocalImpure; + void internalGetPlan(thread_db* tdbb, PlanEntry& planEntry, unsigned level, bool recurse) const override; void internalOpen(thread_db* tdbb) const override; bool internalGetRecord(thread_db* tdbb) const override; - - struct Impure : public RecordSource::Impure - { - Request* localTableRequest; - SavNumber cursorSavepoint; - }; - - private: - const DeclareLocalTableNode* m_table; - bool m_outerDecl = false; }; class Union final : public RecordStream diff --git a/src/yvalve/gds.cpp b/src/yvalve/gds.cpp index 62ce43e3c33..adf40ec7e05 100644 --- a/src/yvalve/gds.cpp +++ b/src/yvalve/gds.cpp @@ -4035,7 +4035,8 @@ static void blr_print_verb(gds_ctl* control, SSHORT level) nullptr, "format", "ltt", - "field_names" + "field_names", + "index" }; while ((blr_operator = control->ctl_blr_reader.getByte()) != blr_end) @@ -4083,6 +4084,23 @@ static void blr_print_verb(gds_ctl* control, SSHORT level) --level; break; + case blr_dcl_local_table_index: + blr_print_name(control); + blr_print_byte(control); + n = blr_print_byte(control); + offset = blr_print_line(control, offset); + ++level; + + while (--n >= 0) + { + blr_indent(control, level); + blr_print_word(control); + offset = blr_print_line(control, offset); + } + + --level; + break; + default: fb_assert(false); }